diff --git a/.claude/skills/incremental-build/SKILL.md b/.claude/skills/incremental-build/SKILL.md new file mode 100644 index 00000000000..739e21563ae --- /dev/null +++ b/.claude/skills/incremental-build/SKILL.md @@ -0,0 +1,38 @@ +--- +name: incremental-build +description: > + Work on the incremental (delta) build system in @ui5/project: build caching, + resource indexing, hash trees, stage caching, build server, file watching, + task execution caching, and delta builds. +when_to_use: > + TRIGGER when: the user asks about or wants to modify code related to incremental builds, + build caching, resource indexing, hash trees, stage caching, build server, file watching, + task execution caching, delta builds, resource tags in build context, or any component + listed in the Component Map in architecture.md. + DO NOT TRIGGER when: the user is working on unrelated CLI commands, general tooling, + or non-build features. +user-invocable: true +--- + +# Incremental Build Skill + +You are working on the incremental (delta) build system in `@ui5/project`. Read `architecture.md` in this skill directory for the full architecture reference, including the component map, key flows, caching architecture, and data structures. Read `performance-investigation.md` for guidance on profiling builds, reading perf logs, and known performance peculiarities. + +## Guidelines for Working on This Code + +1. **Always read the source file before modifying it.** The Component Map in `architecture.md` tells you where each piece lives. +2. **Understand the cache flow direction.** Changes propagate: source change -> index update -> signature change -> cache miss -> task re-execution. +3. **Be careful with tag side effects.** `getTags()` is not a pure read -- it triggers lazy tag application. Avoid calling it in unexpected contexts. +4. **Respect abort signals.** Any long-running operation should check `signal?.throwIfAborted()` periodically. +5. **Test with incremental rebuilds.** A single build passing is not enough; the interesting bugs appear on the second and third builds after file changes. +6. **Watch for stale stage readers after abort.** If a build is aborted, stage writers may contain partial results that shadow source files. +7. **Signature stability matters.** Any change to how hashes are computed (e.g., adding new fields to hash input) invalidates all existing caches. +8. **SharedHashTree operations must go through TreeRegistry.** Never mutate a SharedHashTree directly; always schedule via the registry and flush. + +## Known Constraints + +- `StageCache` (in-memory) has no `clear()` method -- stage entries persist for the lifetime of the `ProjectBuildCache` instance +- `ProjectBuildContext` instances (including their caches) are reused across sequential builds of the same project within a session +- `resource.getTags()` has side effects: it triggers `#applyCachedResourceTags()` and creates `MonitoredResourceTagCollection` instances, which modify tag collection state. This means calling `getTags()` during hash tree operations (e.g., in `TreeRegistry.flush()`) can affect the stage pipeline state. +- `updateProjectIndices` uses `project.getReader()` (stage pipeline reader) to read resources. After an aborted build, stage writers may contain in-memory resources that shadow updated source content, causing stale reads. +- `getSourcePaths()` and `getVirtualPath()` are NOT consistent with `getSourceReader()` for all project types. Module has no `getVirtualPath()` (base class throws). ThemeLibrary's `getSourcePaths()` returns only `[src/]` and `getVirtualPath()` only maps `src/`, but `_getReader()` also includes `test/` when `_testPathExists`. Any optimization that replaces `sourceReader.byGlob()` with direct filesystem enumeration via `getSourcePaths()` + `getVirtualPath()` must handle these mismatches. diff --git a/.claude/skills/incremental-build/architecture.md b/.claude/skills/incremental-build/architecture.md new file mode 100644 index 00000000000..779d7b2c68e --- /dev/null +++ b/.claude/skills/incremental-build/architecture.md @@ -0,0 +1,369 @@ +# Incremental Build Architecture + +## High-Level Overview + +The incremental build system enables fast development feedback loops by: +1. Building projects **lazily** -- only when their output is requested +2. **Caching** task results keyed by content hashes of input resources +3. **Skipping** tasks whose inputs haven't changed since the last build +4. Running **differential** (delta) builds that only process changed resources +5. **Watching** source files and automatically rebuilding on changes + +``` + +----------------+ + Resource Request > | BuildServer | --- file watcher ---> invalidate + abort + +-------+--------+ + | enqueue project + v + +----------------+ + | ProjectBuilder | --- for each project in dependency order + +-------+--------+ + | + +------------+------------+ + v v v + +-----------+ +----------+ +-------------------+ + |BuildContext| |TaskRunner| |ProjectBuildCache | + +-----------+ +----------+ +-------------------+ +``` + +## Component Map + +Use this table to locate source files. ALWAYS read the relevant source file before making changes. + +| Component | Location | Role | +|-----------|----------|------| +| `BuildServer` | `lib/build/BuildServer.js` | Development server, file watching, build orchestration | +| `ProjectBuilder` | `lib/build/ProjectBuilder.js` | Builds projects in dependency order | +| `BuildContext` | `lib/build/helpers/BuildContext.js` | Global build config, project context cache | +| `getBuildSignature` | `lib/build/helpers/getBuildSignature.js` | Build signature computation: `BUILD_SIG_VERSION` + build config + project config | +| `ProjectBuildContext` | `lib/build/helpers/ProjectBuildContext.js` | Per-project bridge between builder, tasks, and cache | +| `TaskRunner` | `lib/build/TaskRunner.js` | Task composition, execution loop, abort handling | +| `ProjectBuildCache` | `lib/build/cache/ProjectBuildCache.js` | Cache orchestration per project: index management, stage lookup, result recording | +| `BuildTaskCache` | `lib/build/cache/BuildTaskCache.js` | Per-task resource request tracking and index management | +| `StageCache` | `lib/build/cache/StageCache.js` | In-memory cache of stage results keyed by signature | +| `BuildCacheStorage` | `lib/build/cache/BuildCacheStorage.js` | Unified SQLite storage for content (CAS) and metadata | +| `CacheManager` | `lib/build/cache/CacheManager.js` | Persistent cache I/O, delegates to BuildCacheStorage | +| `ResourceRequestManager` | `lib/build/cache/ResourceRequestManager.js` | Request graph, resource index updates, signature computation | +| `ResourceRequestGraph` | `lib/build/cache/ResourceRequestGraph.js` | DAG of request sets with delta encoding and best-parent optimization | +| `ResourceIndex` | `lib/build/cache/index/ResourceIndex.js` | Wrapper around hash trees with delta detection | +| `HashTree` | `lib/build/cache/index/HashTree.js` | Directory-based Merkle tree for resource hashing | +| `SharedHashTree` | `lib/build/cache/index/SharedHashTree.js` | HashTree with structural sharing via TreeRegistry | +| `TreeRegistry` | `lib/build/cache/index/TreeRegistry.js` | Batch update coordinator for shared trees | +| `TreeNode` | `lib/build/cache/index/TreeNode.js` | Merkle tree node (resource or directory) | +| `ProjectResources` | `lib/resources/ProjectResources.js` | Stage management, readers/writers, tag collections | +| `Stage` | `lib/resources/Stage.js` | Writer + cached tag operations per build stage | +| `MonitoredResourceTagCollection` | In `@ui5/fs` (separate repo) | Proxy tracking tag operations during task execution | +| `ResourceTagCollection` | In `@ui5/fs` (separate repo) | Base storage for resource tags | +| `Resource` | In `@ui5/fs` (separate repo) | `getTags()` delegates to project's tag collection | + +## Key Flows + +### Build Request Flow + +``` +reader.byPath("/test.js") + -> BuildServer checks ProjectBuildStatus + -> If not fresh: #enqueueBuild(projectName) + -> Debounced (10ms): #processBuildRequests() + -> Batch all pending projects + -> projectBuilder.build({projects, signal}) + -> On success: setReader(project.getReader({style: "runtime"})) + -> Resolve queued reader promises +``` + +### File Watch and Abort + +When a source file changes: +1. `WatchHandler` emits change event with project name and resource path +2. `_projectResourceChanged()` queues the change and calls `ProjectBuildStatus.invalidate()` on the affected project and all its dependents +3. `invalidate()` triggers the project's `AbortController`, which aborts the running build via `AbortSignal` +4. The build loop catches `AbortBuildError` and re-enqueues projects that aren't fresh +5. Queued resource changes are flushed via `#flushResourceChanges()` before the next build starts + +### State Machine (per project) + +``` +INITIAL -> (first build requested, invalidate()) -> INVALIDATED -> (build completes, setReader()) -> FRESH + | + (file change detected) + v + INVALIDATED + (abort + re-enqueue) +``` + +Note: There is no separate `BUILDING` state; `INVALIDATED` covers both "needs build" and "building in progress". The abort controller on `ProjectBuildStatus` cancels the running build on re-invalidation. + +## Caching Architecture + +### Cache Layers + +``` ++---------------------------------------------+ +| In-Memory (StageCache) | <- Fast, per-session +| signature -> {stage, writtenPaths, tagOps} | ++---------------------------------------------+ +| Persistent (CacheManager) | <- Across sessions +| ~/.ui5/buildCache/v0_3/ | +| cas/ (custom CAS, gzip) | +| stageMetadata/ (stage results by sig) | +| taskMetadata/ (resource requests) | +| resultMetadata/(build result metadata) | +| index/ (resource index trees) | +| buildManifests/(project build metadata) | ++---------------------------------------------+ +``` + +### Signatures + +The cache uses content-based signatures at multiple levels: + +| Level | What it captures | Where computed | +|-------|-----------------|----------------| +| **Build signature** | `BUILD_SIG_VERSION` + build config via `getBaseSignature()`, per-project via `getProjectSignature()` (base + projectId + project config). Task-provided signatures planned but not yet integrated. | `getBuildSignature.js`, `ProjectBuildContext.create()` | +| **Source signature** | Merkle root of all source resources | `ResourceIndex` (source index) | +| **Task stage signature** | `projectIndexSignature-dependencyIndexSignature` | `ProjectBuildCache.prepareTaskExecutionAndValidateCache()` | +| **Result signature** | Combined source signature + last task stage signature | `ProjectBuildCache.#getResultStageSignature()` | + +### Source File CAS Storage (Frozen Sources) + +To prevent race conditions where a dependency's source files change between project builds in a multi-project build, untransformed source files are stored in CAS after each build completes: + +1. `#freezeUntransformedSources()` identifies source files not overlaid by any build task +2. Stores them in CAS and persists metadata as a stage cache entry keyed by the source index signature +3. Creates a CAS-backed reader and sets it via `ProjectResources.setFrozenSourceReader()` +4. On subsequent builds where the result cache is valid, `#restoreFrozenSources()` loads metadata from cache and recreates the CAS-backed reader without rebuilding + +At build completion, `#revalidateSourceIndex()` re-reads all source files and compares them against the source index. If any file was modified during the build, an error is thrown and the cache is not stored, preventing inconsistent results. In watch mode this triggers a rebuild. + +### First Build (no cache) + +``` +#initSourceIndex() + -> No index cache on disk + -> Create fresh ResourceIndex from all source resources + -> #combinedIndexState = INITIAL + +prepareProjectBuildAndValidateCache() + -> State is INITIAL -> return false (no cache to validate) + +For each task: + prepareTaskExecutionAndValidateCache(taskName) + -> No task cache exists (#taskCache empty) + -> return false (task must execute) + + [task executes] + + recordTaskResult(taskName, workspace, dependencies, cacheInfo) + -> Records resource requests -> creates hash trees (ResourceRequestManager.addRequests) + -> Creates BuildTaskCache with request patterns and indices + -> Reads stage writer for produced resources + -> Gets tag operations from MonitoredResourceTagCollection + -> Computes stage signature + -> Stores in StageCache (in-memory) via stageCache.addSignature() + +allTasksCompleted() + -> Sets #combinedIndexState = FRESH + -> Computes result signature + -> Resets #writtenResultResourcePaths +``` + +### Subsequent Build (with cache) + +``` +projectSourcesChanged(changedPaths) / dependencyResourcesChanged(changedPaths) + -> Records changed paths + -> Sets #combinedIndexState = REQUIRES_UPDATE + +prepareProjectBuildAndValidateCache() + -> State is REQUIRES_UPDATE + -> #flushPendingChanges(): + #updateSourceIndex(changedPaths) -> reads resources from source reader + -> upserts into source ResourceIndex + -> Adds changed paths to #writtenResultResourcePaths + Updates dependency indices for all task caches + -> #findResultCache() -> checks if overall result is still valid + -> If result cache valid: return true (skip entire project build) + +For each task: + prepareTaskExecutionAndValidateCache(taskName) + -> Task cache EXISTS (from previous build's recordTaskResult) + -> updateProjectIndices(reader, writtenResultResourcePaths) + -> ResourceRequestManager.updateIndices(): + Match changed paths against request graph + Read resources from reader + Upsert into task hash trees (via TreeRegistry.flush for shared trees) + -> Compute stage signatures from updated hash trees + -> #findStageCache(stageName, stageSignatures) + 1. Check in-memory StageCache first + 2. Fall back to persistent cache (CacheManager) + -> If found: apply cached stage, return true (skip task) + -> If not found: try delta signatures (previous signature -> new signature) + -> If delta found: return {cacheInfo} for differential execution + -> If nothing found: return false (full execution) +``` + +## Resource Indexing (Merkle Trees) + +### HashTree + +A directory-based Merkle tree where: +- **Leaf nodes** (resources): hash = `SHA-256(resource:{name}:{integrity}[:tags(...)])` +- **Directory nodes**: hash = `SHA-256(sorted child hashes concatenated)` +- **Root hash** = tree signature (used as cache key) + +Each resource node stores: `name`, `integrity`, `lastModified`, `size`, `inode`, `tags` + +Key operations: +- `upsertResources(resources, timestamp)`: Insert or update resources, recompute affected hashes +- `removeResources(paths)`: Remove resources, recompute affected hashes +- `_computeHash(node)`: Recursive hash computation + +The `matchResourceMetadataStrict` utility (`utils.js`) determines if a resource is "unchanged" using a tiered comparison (cheapest first): + +1. If `lastModified` matches cached value AND differs from `indexTimestamp`: unchanged (fast path) +2. If `lastModified` equals `indexTimestamp`: racy-git edge case -- file may have changed during indexing, fall through to integrity check +3. Compare `size` -- if different, changed +4. Compare `integrity` hash -- expensive, last resort + +Note: inode comparison is defined but currently commented out. `inode` is still stored in TreeNode for future use. + +### SharedHashTree and TreeRegistry + +Tasks make multiple resource requests (e.g., `byGlob("/**/*.js")`, `byPath("/manifest.json")`). Each request set gets its own hash tree, but they share common subtrees via `SharedHashTree`. + +``` +Task reads: + byGlob("/**/*.js") -> RequestSet A -> SharedHashTree A (all JS files) + byPath("/test.js") -> RequestSet B -> SharedHashTree B (derived from A, adds test.js) +``` + +**TreeRegistry** coordinates batch updates across all shared trees: +1. Changes scheduled via `scheduleUpsert()` / `scheduleRemoval()` +2. `flush()` applies all pending operations atomically +3. Shared nodes modified once, changes propagate to all trees referencing them + +### ResourceRequestManager + +Manages the request graph for a task -- delegates to `ResourceRequestGraph` for DAG storage of request sets with delta encoding. Each graph node stores only the requests added relative to its parent, and `addRequestSet()` automatically finds the best parent (largest subset) to minimize delta size. + +At runtime, each materialized request set references a `SharedHashTree` representing the resources currently matching that set. + +- `addRequests(recording, reader)`: Records path/glob requests, creates or reuses a request set in the graph, builds a resource index (SharedHashTree), returns signature +- `updateIndices(reader, changedPaths)`: Traverses graph breadth-first, matches changed paths against request patterns per node, batch-fetches resources, upserts into affected resource indices via TreeRegistry +- `getIndexSignatures()`: Returns current signatures for all request sets +- `getDeltas()`: Returns map of original -> new signature for changed request sets + +## Resource Tags + +### Tag Types + +| Tag | Scope | Persists across builds | Example | +|-----|-------|----------------------|---------| +| `ui5:IsDebugVariant` | Project | Yes | Set by minify task on `-dbg.js` files | +| `ui5:HasDebugVariant` | Project | Yes | Set by minify task on original `.js` files | +| `ui5:OmitFromBuildResult` | Build | No (cleared after each build) | Exclude resources from output | +| `ui5:IsBundle` | Build | No | Mark bundled resources | + +### Tag Flow Through Stages + +``` +initStages([stage1, stage2, ...]) + -> Creates Stage objects with empty writers and no cached tag ops + +useStage(stageId) + -> Sets #currentStageReadIndex = stageIdx - 1 + -> Resets monitored tag collections + +#applyCachedResourceTags() [called lazily from getResourceTagCollection()] + -> Imports cached tag operations from stages[#lastTagCacheImportIndex+1 .. #currentStageReadIndex] + -> Advances #lastTagCacheImportIndex + +getResourceTagCollection(resource, tag) + -> Calls #applyCachedResourceTags() + -> Creates MonitoredResourceTagCollection wrapping the live collection + -> MonitoredResourceTagCollection clones the collection at creation time + (so getAllTagsForResource returns INPUT state, before task modifies) + +resource.getTags() + -> Calls project.getResourceTagCollection(this).getAllTagsForResource(this) + -> Returns tags as {key: value} object or null +``` + +### Tags in Hash Trees + +Resource hashes incorporate tags when present: +``` +hashInput = `resource:${name}:${integrity}` +if (tags && Object.keys(tags).length > 0) { + hashInput += `:tags(${sortedTagString})` +} +``` + +This ensures that tag-only changes (e.g., a resource gaining `IsDebugVariant` after the minify task runs) invalidate the cache signature for downstream tasks. + +## Stage Pipeline + +Each task has its own **stage** with a writer. Resources written by a task go into that stage's writer. + +### Reader Construction + +`ProjectResources.getReader()` creates a prioritized reader stack: +1. Current stage writer (highest priority) +2. Previous stage writers (in reverse order) +3. Frozen source reader (CAS-backed, if set) +4. Source reader (lowest priority, reads from filesystem) + +This means a task sees the cumulative output of all previous tasks, with its own writes taking highest priority. The frozen source reader ensures downstream consumers read an immutable CAS snapshot rather than the live filesystem. + +### Stage Cache + +When a task is skipped (cache hit), its cached stage is restored: +```javascript +project.getProjectResources().setStage(stageName, stageCache.stage, + stageCache.projectTagOperations, stageCache.buildTagOperations); +``` + +## Persistent Cache Format + +### On Disk (CacheManager) + +``` +~/.ui5/buildCache/v0_6/ ++-- cache.db # Single SQLite database (WAL mode) + Tables: + - content(integrity TEXT PK, data BLOB) # Gzip-compressed CAS BLOBs + - index_cache(project_id, build_signature, kind, data) + - stage_metadata(project_id, build_signature, stage_id, stage_signature, data) + - task_metadata(project_id, build_signature, task_name, type, data) + - result_metadata(project_id, build_signature, stage_signature, data) +``` + +Note: Only CAS content (resource bodies) is gzip-compressed. All metadata files are plain JSON. + +#### Index Cache Contents + +The index cache (`{kind}-{buildSignature}.json`) contains: +- `indexTimestamp`: creation timestamp (used for racy-git detection) +- `root`: serialized Merkle tree (TreeNode hierarchy) +- `tasks`: array of `[taskName, supportsDifferentialBuilds ? 1 : 0]` recording the task execution order and differential build capability + +#### Stage Metadata Format + +Stage metadata stored on disk includes: +- `resourceMetadata`: resource paths mapped to `{integrity, lastModified, size, inode}` +- `resourceMapping` (optional, for WriterCollection stages): virtual path prefixes mapped to indices in the `resourceMetadata` array, supporting project types where multiple virtual paths map to the same physical path +- `projectTagOperations` / `buildTagOperations`: tag operations to apply when restoring the cached stage + +## Key Architectural Patterns + +1. **Lazy building**: Projects built on-demand when readers are requested +2. **Request batching**: Multiple pending build requests processed in single batch (10ms debounce) +3. **Abort/retry**: File changes abort running builds; projects re-queued automatically +4. **Structural sharing**: Derived hash trees share unchanged subtrees, reducing memory +5. **Content-addressed storage**: Resources deduplicated via integrity hashes in custom CAS (synchronous path resolution, gzip-compressed) +6. **Differential caching**: Tasks track resource requests; delta builds only re-process changed resources +7. **Tag propagation**: Resource tags flow through stages via cached tag operations, included in hash signatures +8. **Two-tier cache**: Fast in-memory StageCache + persistent filesystem cache via CacheManager +9. **Two-phase invalidation**: Changes queued via `projectSourcesChanged()` / `dependencyResourcesChanged()` (state -> `REQUIRES_UPDATE`), applied only during `#flushPendingChanges()` at next build start. "Definitely invalidated" only after content comparison confirms actual differences. +10. **Source index revalidation**: At build completion, `#revalidateSourceIndex()` re-reads source files and compares against the source index. If any file changed during the build, an error is thrown and the cache is not stored. +11. **Frozen sources**: Untransformed source files stored in CAS after build, providing immutable snapshots for downstream dependency consumers (prevents filesystem race conditions) diff --git a/.claude/skills/incremental-build/performance-investigation.md b/.claude/skills/incremental-build/performance-investigation.md new file mode 100644 index 00000000000..5742ef644a8 --- /dev/null +++ b/.claude/skills/incremental-build/performance-investigation.md @@ -0,0 +1,377 @@ +# Performance Investigation Guide + +Reference for investigating and analyzing performance of the incremental build system. + +## Test Setup + +### Test library + +Use sap.m from the OpenUI5 repository. It's a large library (~12,700 source resources) with 3 dependency projects (sap.ui.core, sap.ui.layout, sap.ui.unified) and 9 build tasks, making it a good stress test. + +### Build command + +```bash +# Within openui5/src/sap.m: +UI5_CLI_NO_LOCAL=X UI5_BUILD_NO_WRITE_DEST=X UI5_CACHE_PERF=1 ui5 build --log-level perf 2>&1 +``` + +Flags: +- `UI5_CLI_NO_LOCAL=X` — Use the globally linked CLI (i.e., the development version) +- `UI5_BUILD_NO_WRITE_DEST=X` — Skip writing output to `./dist` (isolates cache/build overhead from disk I/O) +- `UI5_CACHE_PERF=1` — Enable low-level `matchResourceMetadataStrict` counters (see below) +- `--log-level perf` — Show all perf-level log statements + +### Three build scenarios + +| Scenario | How to trigger | What it tests | +|----------|---------------|---------------| +| **Cold cache** | Delete `~/.ui5/buildCache/` and run | Full build + cache creation from scratch | +| **Warm cache** | Run twice with no file changes | Cache validation + result restoration | +| **Stale cache** | Edit a file, then run | Cache invalidation + partial rebuild | + +**Creating a stale cache scenario:** +Change a variable value inside a JS file (do NOT replace the first line — if it contains a license comment or `/*!`, replacing it can break minification): +```bash +# Within openui5/src/sap.m: +sed -i '' 's/BADGE_MAX_VALUE = 9999;/BADGE_MAX_VALUE = 9998;/' src/sap/m/Button.js +``` +Then run the build command. Restore with `git checkout -- src/sap/m/Button.js` when done. + +## Performance Logging API + +The `@ui5/logger` package provides a `perf` log level for performance instrumentation. This is the primary mechanism for adding timing measurements to build code. + +### Log levels (least to most restrictive) + +`silly` → `verbose` → `perf` → `info` (default) → `warn` → `error` → `silent` + +Setting `--log-level perf` shows `perf`, `info`, `warn`, and `error` messages. At the default `info` level, `perf` messages are suppressed. + +### Usage pattern + +```javascript +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:cache:MyModule"); + +// Guard expensive timing computation behind isLevelEnabled +if (log.isLevelEnabled("perf")) { + log.perf(`Operation completed in ${(performance.now() - start).toFixed(2)} ms`); +} +``` + +Key points: +- **Always guard with `log.isLevelEnabled("perf")`** when the log message involves computation (e.g., `performance.now()` calls, string interpolation with counts). Without the guard, the timing overhead runs even at default log level. +- `log.perf(msg)` works like `log.info(msg)` — accepts any number of arguments, joins them with spaces. +- `log.isLevelEnabled(level)` is a static check comparing the requested level's index against the current global level. +- Logger names use colon-separated namespaces (e.g., `"build:cache:ProjectBuildCache"`). + +### Enabling perf logging + +Two CLI options (defined in `packages/cli/lib/cli/base.js`): +- `--log-level perf` — Set log level directly +- `--perf` — Shorthand boolean flag, equivalent to `--log-level perf` + +### Adding new perf instrumentation + +Standard pattern for timing a code section: + +```javascript +const start = log.isLevelEnabled("perf") ? performance.now() : 0; +// ... operation to measure ... +if (log.isLevelEnabled("perf")) { + log.perf(`Operation X for project ${name} completed in ${(performance.now() - start).toFixed(2)} ms`); +} +``` + +When the guarded block is large or complex, prefer the two-check pattern above. For simple one-liners where the start timestamp is cheap, a single check around the log call suffices. + +## Reading the Perf Log + +The log follows the build lifecycle. Here is the anatomy of a stale-cache build: + +### Phase 1: Source Index Initialization (parallel across all projects) + +``` +perf #initSourceIndex fromCacheWithDelta for project sap.m completed in 68 ms: 12677 resources, 1 changed +perf Initialized source index for project sap.m in 691 ms +``` + +- The first line shows the delta detection: how many resources exist and how many changed. + A high `changed` count with few actual edits suggests a timestamp-related false positive. +- The total init time includes `byGlob("/**/*")` (stat all files), reading the cached index from disk, and delta detection. The `fromCacheWithDelta` time is a subset of the total. +- If `fromCacheWithDelta` is small relative to total, the bottleneck is in the glob (reading file metadata from disk). If `fromCacheWithDelta` is large, the bottleneck is in hashing. + +### Phase 2: Per-project cache validation (sequential) + +Each project goes through this sequence: + +``` +perf getDependenciesReader completed in 0.12 ms +perf Initialized dependency indices for project sap.ui.core in 0.63 ms + OR +perf Skipping dependency index refresh for project sap.ui.core (no dependency changes propagated) + OR +perf #flushPendingChanges updateDependencyIndices for project sap.ui.layout completed in 61 ms (3064 changed paths, 2/10 tasks, changed=true) +perf Flushed pending changes for project sap.ui.layout in 61 ms +``` + +- **"Initialized dependency indices"** → first-time dependency index refresh (`_refreshDependencyIndices`). Runs when dependency changes were propagated from upstream projects. +- **"Skipping dependency index refresh"** → no dependency changes were propagated; cached indices are already correct. +- **"#flushPendingChanges"** → updating existing indices with changed paths (BuildServer subsequent builds). The `N/M tasks` shows how many tasks had dependency requests vs. total. The `changed paths` count is the number of dependency resource paths propagated as potentially changed. + +Then result cache validation: + +``` +perf #findResultCache importStages for project sap.ui.core completed in 7 ms with 10 stages +perf #findResultCache restoreFrozenSources for project sap.ui.core completed in 5 ms +perf Validated result cache for project sap.ui.core in 13 ms +``` + +- `importStages` — Loads cached stage readers from CAS for each task. +- `restoreFrozenSources` — Creates a CAS-backed reader for source files. +- The overall line tells you if the result cache was valid. + +After validation: + +``` +perf prepareProjectBuildAndValidateCache for sap.ui.core completed in 34 ms (usesCache=true) +info ✔ Skipping build of library project sap.ui.core + OR +perf prepareProjectBuildAndValidateCache for sap.m completed in 0.48 ms (usesCache=false) +info ❯ Building library project sap.m... +``` + +- `usesCache=true` → Project is fully served from cache, skip all tasks. +- `usesCache=false` → Project needs (re-)building, tasks will be evaluated individually. + +### Phase 3: Task execution (for projects that need building) + +Per-task index update: + +``` +perf updateIndices for task 'replaceVersion' of project 'sap.m' resource fetch completed in 3.89 ms: 0 cache hits, 783 cache misses +perf TreeRegistry.flush completed: phase1(removals)=0.00 ms, phase2(upserts)=7.13 ms, phase3(rehash)=9.56 ms | matchMetadataStrictCalls=783, matchMetadataUnchanged=782, modifiedNodesSkips=0 +perf #flushTreeChanges for task 'replaceVersion' completed in 16.79 ms across 1 registries +perf Updated project indices for task replaceVersion in project sap.m in 21.71 ms +``` + +Key metrics: +- **cache hits / cache misses**: In the resource fetch, "cache hits" means the resource was already in the task's request index (from a previous task sharing the same tree). "Cache misses" means a new fetch was needed. +- **phase1(removals)**: Time removing deleted resources from the Merkle tree. +- **phase2(upserts)**: Time inserting/updating resources in the tree. +- **phase3(rehash)**: Time propagating hash changes up from modified leaves to root. High rehash with few changed resources indicates deep tree structure. +- **matchMetadataStrictCalls / matchMetadataUnchanged**: How many resources were compared, and how many were unchanged (short-circuited by lastModified check). If `calls >> unchanged`, many resources needed content hashing. + +Then task execution or skip: + +``` +info ✔ Skipping task escapeNonAsciiCharacters ← Cache hit +info ◇ Running task replaceCopyright... ← Delta/differential run +info › Running task generateLibraryPreload... ← Full re-execution +``` + +- `✔ Skipping` — exact cache match for this task's signature. +- `◇ Running` — differential execution (using `changedProjectResourcePaths`). +- `› Running` — full execution (no cache match, no delta available). + +After task execution, `recordTaskResult` runs (logged per task): + +``` +perf recordTaskResult delta merge for task replaceCopyright in project sap.m completed in 6.67 ms (783 previous, 782 merged) +perf recordTaskResult for task replaceCopyright in project sap.m completed in 7.22 ms (1 written resources, delta=true) +``` + +### Phase 3b: Build finalization (`allTasksCompleted`) + +After all tasks complete, `allTasksCompleted()` runs two operations: + +``` +perf #revalidateSourceIndex byGlob for project sap.m completed in 284.91 ms (12677 resources) +perf allTasksCompleted #revalidateSourceIndex for project sap.m completed in 326.33 ms (changed=false) +perf #freezeUntransformedSources for project sap.m: reused all 11821 entries from previous metadata +perf allTasksCompleted #freezeUntransformedSources for project sap.m completed in 30.95 ms +perf allTasksCompleted for project sap.m completed in 357.45 ms (2235 changed paths) +``` + +Key metrics: +- **`#revalidateSourceIndex` byGlob**: Time to re-read all source file metadata from disk. I/O-bound. +- **`#revalidateSourceIndex` total**: Includes metadata comparison loop. If much larger than byGlob, the comparison is expensive (racy-git fallback to integrity check). +- **`#freezeUntransformedSources` metadata reuse**: When `#cachedFrozenSourceMetadata` is populated (stale cache), untransformed paths found in the previous metadata are reused directly with no I/O. The log shows "reused all N entries" for the fast path, or "M of N resources" for the delta path (where M paths needed re-reading). +- **`#freezeUntransformedSources` byPath reads**: Only shown when some paths need reading (delta or cold-cache path). Format: `(M of N resources)` for delta, `(N resources)` for cold cache. +- **`#freezeUntransformedSources` writeStageResources**: Only shown when `byPath` reads are needed. Time to write delta resources to CAS and collect metadata. +- **CAS skipped / CAS written**: Shows how many resources had their CAS write skipped because `#knownCasIntegrities` already contained the integrity hash. + +### Phase 4: Cache write + +In CLI builds, cache writes are **deferred** — they run in the background after `Build succeeded` is printed and don't block process exit. In BuildServer mode, cache writes are still awaited. + +``` +info ProjectBuilder Build succeeded in 5.04 s +info ProjectBuilder Executing cleanup tasks... +perf Wrote build cache for project sap.m in 301 ms +``` + +The four sub-operations (stages, requests, sourceIndex, result) run in parallel (`Promise.all`), so wall time ≈ max of all four. When run in the background (CLI), they no longer compete with the build for I/O bandwidth and typically complete faster (~300ms vs ~1400ms when blocking). + +**Important:** The source stage CAS write (`#freezeUntransformedSources`) is NOT part of Phase 4. It runs during `allTasksCompleted()` in Phase 3b, before `Build succeeded` is logged. For sap.m this processes ~11,821 untransformed source resources. Look for the `#freezeUntransformedSources` and `allTasksCompleted #freezeUntransformedSources` log lines — they appear before `Build succeeded`. In the stale-cache scenario this takes ~31ms (metadata reused from previous build via `#cachedFrozenSourceMetadata`). In the cold-cache scenario it takes ~8s because there is no previous metadata and every resource requires `byPath()` + `cacache.get.info()` (see Known Peculiarities #7). + +## UI5_CACHE_PERF Counters + +Setting `UI5_CACHE_PERF=1` enables low-level counters in `utils.js` for `matchResourceMetadataStrict`: + +- `calls` — Total invocations +- `shortCircuitTrue` — Fast-path returns (lastModified matches cached and ≠ indexTimestamp → file unchanged, no I/O needed) +- `sizeMismatch` — Changes detected via size check (cheap) +- `integrityFallback` — Full SHA256 content hash required (expensive) + +These counters appear in the `TreeRegistry.flush` log as `matchMetadataStrictCalls` and `matchMetadataUnchanged`. + +A high `integrityFallback` count means many files have the same size but different content — this is expensive and may indicate that the `lastModified` short-circuit is not working (e.g., due to timestamp resolution issues). + +## Perf Log Reference + +All `log.perf()` statements by source file: + +| File | Log pattern | What it measures | +|------|-------------|------------------| +| `BuildContext.js` | `Parallel source index initialization completed` | Total time for parallel `initSourceIndex` across all projects | +| `BuildContext.js` | `getRequiredProjectContexts completed` | Discovery + index init for all required projects | +| `ProjectBuildContext.js` | `getDependenciesReader completed` | Creating the dependency reader for a project | +| `ProjectBuildContext.js` | `ProjectBuildCache.prepareProjectBuildAndValidateCache completed` | Full cache validation for one project | +| `ProjectBuilder.js` | `getRequiredProjectContexts completed` | Top-level timing (includes context creation) | +| `ProjectBuilder.js` | `prepareProjectBuildAndValidateCache for {project} completed` | Per-project validation with `usesCache` flag | +| `TaskRunner.js` | `Task {name} finished in {N} ms` | Individual task execution time | +| `ProjectBuildCache.js` | `#initSourceIndex fromCacheWithDelta` | Delta detection: resource count, changed count | +| `ProjectBuildCache.js` | `Initialized source index for project` | Total source index init (glob + cache read + delta) | +| `ProjectBuildCache.js` | `Initialized dependency indices` | First-time dependency index refresh | +| `ProjectBuildCache.js` | `Skipping dependency index refresh` | Cached indices reused without refresh | +| `ProjectBuildCache.js` | `Flushed pending changes for project` | Source + dependency index update with pending changes | +| `ProjectBuildCache.js` | `#flushPendingChanges updateSourceIndex` | Source index update only | +| `ProjectBuildCache.js` | `#flushPendingChanges updateDependencyIndices` | Dependency index update with task/path counts | +| `ProjectBuildCache.js` | `#importStages: Initial import` | First-time stage import with suppressed propagation count | +| `ProjectBuildCache.js` | `#findResultCache importStages` | Loading cached stages from CAS | +| `ProjectBuildCache.js` | `#findResultCache restoreFrozenSources` | Creating CAS-backed source reader | +| `ProjectBuildCache.js` | `Validated result cache for project` | Overall result cache validation | +| `ProjectBuildCache.js` | `Updated project indices for task` | Per-task index update during build | +| `ProjectBuildCache.js` | `#writeStageResources for stage` | CAS write with skipped/written counts | +| `ProjectBuildCache.js` | `Wrote build cache for project` | Cache persistence with sub-operation breakdown | +| `ProjectBuildCache.js` | `allTasksCompleted for project` | Total time for allTasksCompleted including revalidation and source freeze | +| `ProjectBuildCache.js` | `allTasksCompleted #revalidateSourceIndex` | Source file revalidation (checks no files changed during build) | +| `ProjectBuildCache.js` | `allTasksCompleted #freezeUntransformedSources` | Writing untransformed source files to CAS | +| `ProjectBuildCache.js` | `#revalidateSourceIndex byGlob` | Re-reading all source files for revalidation | +| `ProjectBuildCache.js` | `#freezeUntransformedSources ... reused all N entries` | Fast path: all metadata reused from previous build | +| `ProjectBuildCache.js` | `#freezeUntransformedSources byPath reads` | Reading only delta untransformed source files | +| `ProjectBuildCache.js` | `#freezeUntransformedSources writeStageResources` | CAS writes during source freeze (delta or cold cache) | +| `ProjectBuildCache.js` | `recordTaskResult for task` | Total time to record task result including merge/signature | +| `ProjectBuildCache.js` | `recordTaskResult delta merge` | Merging previous stage cache with delta results | +| `ProjectBuildCache.js` | `recordTaskResult recordRequests` | Recording resource requests and building hash trees | +| `ResourceRequestManager.js` | `refreshIndices for task` | Full dependency index refresh | +| `ResourceRequestManager.js` | `updateIndices for task ... resource fetch` | Resource fetch phase with cache hit/miss counts | +| `ResourceRequestManager.js` | `#flushTreeChanges for task` | Merkle tree flush with registry count | +| `TreeRegistry.js` | `TreeRegistry.flush completed` | Detailed tree operation breakdown (removals/upserts/rehash) | + +## Known Peculiarities + +### 1. Source index init dominated by byGlob, not delta detection + +For sap.m: `fromCacheWithDelta` takes ~68ms but the total `Initialized source index` takes ~691ms. The remaining ~620ms is `sourceReader.byGlob("/**/*")` which stats all ~12,677 files. The delta detection itself (comparing metadata, hashing only changed files) is fast. + +**Implication:** Source index init performance is I/O-bound (filesystem stat calls), not CPU-bound. Optimizations targeting hash computation won't help much; reducing the number of stat calls would. + +**Validated (2026-04):** Replacing `byGlob("/**/*")` with `fs.readdir(recursive)` + per-file `fs.stat()` using lightweight proxy objects (avoiding Resource construction) was tested and produced a ~300ms *regression* for sap.m. Both approaches must stat every file; globby integrates stat into its directory walk more efficiently than a separate readdir + 12K stat calls. The only way to meaningfully reduce source index init time is to **avoid statting unchanged files entirely** (e.g., via directory mtime heuristics or filesystem change notifications). + +### 2. "cache misses" in resource fetch don't mean cache corruption + +In `updateIndices` logs, "cache misses" means the resource wasn't found in the task's SharedHashTree node cache (a performance optimization for avoiding redundant metadata reads). It does NOT mean the persistent cache is missing. On the first build from cache, all resources are "cache misses" because the in-memory node cache starts empty. + +### 3. Task execution appears twice in the log + +``` +perf Build Task replaceCopyright finished in 1 ms +perf Build Task replaceCopyright finished in 8 ms +``` + +The first line is the task function's execution time. The second includes cache recording overhead (computing signatures, recording resource requests). This is the wall time from the TaskRunner's perspective. + +### 4. writeCache sub-timings all look similar because of Promise.all + +``` +perf Wrote build cache ... (stages=1422 ms, requests=1427 ms, sourceIndex=1395 ms, result=1381 ms) +``` + +These four operations run in parallel. They share I/O bandwidth, so their individual timings overlap. The wall time is ~max of all four, not the sum. To identify the true bottleneck, you'd need to run them sequentially (temporarily modify `writeCache`). + +**Note:** In CLI mode, cache writes are deferred to the background. The `Wrote build cache` log line appears after `Build succeeded`. The sub-timings tend to be lower (~250-300ms vs ~1400ms) because background writes don't compete with the build for I/O bandwidth. + +### 5. matchMetadataUnchanged vs modifiedNodesSkips + +In `TreeRegistry.flush` logs: +- `matchMetadataUnchanged` — Resources whose metadata (lastModified, size, integrity) matched the cached version. No tree modification needed. +- `modifiedNodesSkips` — Tree nodes already marked as modified by a previous flush in the same build. Skipped to avoid redundant work. + +When `matchMetadataUnchanged` equals `matchMetadataStrictCalls`, ALL resources were unchanged — the flush was pure overhead. + +### 6. Dependency index "changed=true" doesn't always mean task re-execution + +The `changed=true` in `#flushPendingChanges updateDependencyIndices` means at least one dependency index signature changed. But the *result* cache validation (`#findResultCache`) may still find a matching cached result if the combined project+dependency signature matches a previous build. + +### 7. `#freezeUntransformedSources` CAS overhead varies dramatically by scenario + +`#freezeUntransformedSources` writes untransformed source files to CAS and collects metadata. Its cost depends on two factors: whether `#knownCasIntegrities` contains the resource integrities (CAS skip), and whether `#cachedFrozenSourceMetadata` contains previous metadata for reuse (delta skip). + +| Scenario | `#knownCasIntegrities` state | `#cachedFrozenSourceMetadata` state | `#freezeUntransformedSources` time (sap.m) | Why | +|----------|------------------------------|--------------------------------------|---------------------------------------------|-----| +| **Stale cache** | Populated from cached source stage | Populated from cached source stage | ~31ms | All entries reused from previous metadata; only JSON write | +| **Cold cache** | Empty (no `indexCache`) | null | ~8s | Every resource: `byPath()` + `cacache.get.info()` + `getBuffer()` + gzip + `cacache.put()` | +| **Warm cache** | N/A (result cache valid, freeze skipped) | N/A | 0 | `#findResultCache` succeeds, `#restoreFrozenSources` used instead | + +**History:** Before the `#knownCasIntegrities` pre-population fix (2026-04), stale-cache builds suffered the ~10s cold-cache penalty because the set was not populated from the source index. The fix populates the set during `#initSourceIndex` from the cached source stage metadata. + +Subsequently, the delta freeze optimization (2026-04) added `#cachedFrozenSourceMetadata`: during `#initSourceIndex`, the previous build's frozen source `resourceMetadata` is retained. In `#freezeUntransformedSources`, untransformed paths found in the previous metadata are reused directly (no `byPath()`, no `getIntegrity()`, no CAS write). Only genuinely new or newly-untransformed files require I/O. For the common stale-cache case (1 file changed), all ~11,821 untransformed entries are reused, reducing freeze time from ~1.2s to ~31ms. + +**Cold-cache CAS write breakdown:** On cold cache, each resource that doesn't exist in CAS incurs: `getIntegrity()` (hash computation), `cacache.get.info()` (index lookup, ~0.8ms), `getBuffer()` (read file), `gzip()` (compression), `cacache.put()` (write). For ~12K resources this totals ~8s. This is a one-time cost since subsequent stale-cache builds reuse frozen metadata entirely. + +### 8. `recordTaskResult` overhead is small for delta builds + +In delta builds, `recordTaskResult()` is called after each task but is fast (~2-15ms per task). The main cost is the delta merge (`reader.byGlob("/**/*")` on the previous stage cache), which scales with the number of resources in the cached stage. For the minify task (2,193 resources), the merge takes ~13ms. + +For full (non-delta) task re-execution, `recordRequests()` is called instead, which is also fast (~0.2ms) because the hash tree building is deferred to the next task's `prepareTaskExecutionAndValidateCache`. + +### 9. Cache writes are fast in background mode + +When cache writes are deferred (CLI mode), `#writeTaskStageCache` + `#writeSourceIndex` + `#writeResultCache` complete in ~60ms total because `#knownCasIntegrities` was populated during `#freezeUntransformedSources`, allowing CAS skip for most resources. + +### 10. `#knownCasIntegrities` population sources + +`#knownCasIntegrities` is a `Set` that tracks integrity hashes known to exist in CAS. When `#writeStageResources` encounters an integrity in this set, it skips the `cacache.get.info()` lookup entirely. The set is populated from three sources: + +1. **Source index cache** (in `#initSourceIndex`): When loading from `indexCache`, all resource integrities from the Merkle tree are added. These correspond to resources written to CAS by the previous build's `#freezeUntransformedSources`. +2. **Imported stage metadata** (in `#importStageMetadata` via `#collectKnownIntegrities`): When restoring cached task stages (e.g., for dependency projects or result cache hits), the resource integrities from stage metadata are added. +3. **Newly written stages** (in `#writeTaskStageCache` / `#freezeUntransformedSources` via `#collectKnownIntegrities`): After writing stage resources, the metadata integrities are added for subsequent stage writes. + +When diagnosing slow `writeStageResources`, check the `CAS skipped` vs `CAS written` counts in the log. If most resources are being written (not skipped), `#knownCasIntegrities` is not being populated from one of these sources — trace which source is missing for the scenario. + +## Investigation Workflow + +1. **Establish a baseline.** Run the build 2-3 times to get stable warm-cache timings. Note the total time and per-phase breakdown. + +2. **Identify the scenario.** Are you investigating warm cache (no changes), stale cache (file edit), or cold cache (no cache at all)? + +3. **Find the dominant phase.** In the perf log, look for the largest times: + - Source index init? → Check `fromCacheWithDelta` vs total to see if it's I/O or hash-bound + - Dependency index flush? → Check "changed paths" count and "cache misses" + - Task execution? → Check which tasks run and whether they support differential builds (◇ vs ›) + - `allTasksCompleted`? → Check `#revalidateSourceIndex` and `#freezeUntransformedSources` sub-timings + - Cache write? → Check the sub-operation breakdown + +4. **Drill down.** For the dominant phase: + - Add more granular `log.perf()` statements if needed + - Use `console.time()`/`console.timeEnd()` for quick local profiling + - Check `matchMetadataStrictCalls` vs `matchMetadataUnchanged` to understand if resources are being unnecessarily re-hashed + +5. **Validate optimization ideas.** When proposing optimizations: + - Consider warm vs stale vs cold cache impact separately + - Check if the optimization target already has an early-exit path (e.g., `ResourceRequestManager.updateIndices` exits early when `requestGraph.getSize() === 0`) + - Measure with the test build before and after + +6. **Watch for measurement artifacts.** The first run after a system boot or after a long idle period will be slower due to OS filesystem cache being cold. Run 2-3 times to get stable readings. diff --git a/.claude/skills/ui5-fs/SKILL.md b/.claude/skills/ui5-fs/SKILL.md new file mode 100644 index 00000000000..24baf6f3a23 --- /dev/null +++ b/.claude/skills/ui5-fs/SKILL.md @@ -0,0 +1,67 @@ +--- +name: ui5-fs +description: > + Work on the @ui5/fs package: the virtual file system abstraction layer providing + Resource, Adapters (FileSystem, Memory), Reader Collections (ReaderCollection, + ReaderCollectionPrioritized, DuplexCollection, WriterCollection), specialized readers + (Filter, Link, Proxy), resource tagging, monitoring, and the resourceFactory API. +when_to_use: > + TRIGGER when: the user asks about or wants to modify code related to @ui5/fs, + the virtual file system, resources, adapters, reader collections, writer collections, + DuplexCollection, resourceFactory, ResourceTagCollection, MonitoredReader, fsInterface, + ResourceFacade, or any file within packages/fs/. + DO NOT TRIGGER when: the user is working on builder tasks/processors, CLI commands, + server middleware, or project graph resolution that does not touch the FS layer. +user-invocable: true +--- + +# @ui5/fs Skill + +You are working on the `@ui5/fs` package — the virtual file system abstraction layer for UI5 CLI. Read `architecture.md` in this skill directory for the full architecture reference, including the class hierarchy, API surface, adapter internals, and collection patterns. + +## Package Location + +Source: `packages/fs/lib/` +Tests: `packages/fs/test/lib/` +Fixtures: `packages/fs/test/fixtures/` + +## Guidelines for Working on This Code + +1. **Always read the source file before modifying it.** The Component Map in `architecture.md` tells you where each piece lives. +2. **Understand the content type state machine.** Resources have internal content types (BUFFER, STREAM, FACTORY, DRAINED_STREAM, IN_TRANSFORMATION) with strict transitions. Never bypass `modifyStream()` for content transformations — it handles mutex locking and state management. +3. **Respect the mutex.** Resource content access is protected by `async-mutex`. Concurrent `getBuffer()` / `getString()` calls are safe, but `modifyStream()` acquires an exclusive lock. Never hold a reference to content across an `await` that could trigger a transformation. +4. **Virtual paths are POSIX-absolute.** All virtual paths must be absolute POSIX paths (start with `/`). Base paths must end with `/`. Adapters normalize patterns relative to their `virBasePath`. +5. **Content parameters are mutually exclusive.** When creating a Resource, only one of `buffer`, `string`, `stream`, `createStream`, or `createBuffer` can be provided. The `createBuffer`/`createStream` factories enable lazy loading. +6. **byGlob randomizes result order.** `AbstractReader.byGlob()` intentionally shuffles results to prevent consumers from relying on ordering. Do not assume or depend on glob result order. +7. **Clone semantics matter.** Resources are cloned on retrieval from adapters. `resource.clone()` creates an independent copy including content. When modifying resources from collections, understand whether you're working on the original or a clone. +8. **ResourceFacade is immutable.** `ResourceFacade.setPath()` throws. The facade wraps a resource with a different virtual path (used by Link reader). Use `getOriginalPath()` to get the underlying path. +9. **Tag format is strict.** Tags follow the pattern `"namespace:Name"` — namespace is lowercase alphanumeric, name is PascalCase. Tags are validated on set. Use `ResourceTagCollection` or `MonitoredResourceTagCollection` for tag operations. +10. **Test with both FileSystem and Memory adapters.** Behavior can differ between adapters (e.g., FileSystem uses `globby` while Memory uses `micromatch`; FileSystem has lazy content loading via factories, Memory clones on read). + +## Known Constraints + +- `Resource.getStream()` is deprecated — use `getStreamAsync()` or `getBuffer()` / `getString()` instead. The deprecation warning is only logged once per process. +- `Resource.getStatInfo()` is deprecated — use `getLastModified()`, `getSize()`, `getInode()` instead. +- FileSystem adapter's `write()` uses `fs.copyFile` for unmodified resources (optimization). It also detects same-source-same-target writes and skips them, but switches to buffer-based writes if the content was modified (to avoid stream read-during-write conflicts). +- Memory adapter auto-creates virtual directory entries in its hierarchy on `write()`. +- `WriterCollection` matches the **longest prefix** (greedy match) when routing writes to writers. +- `DuplexCollection` uses an internal `ReaderCollectionPrioritized` with the writer first, so written resources shadow the reader's resources. +- `fsInterface` provides a Node.js `fs`-compatible wrapper but only implements `readFile`, `stat`, and `readdir`. `mkdir` is a no-op. +- `Resource.getIntegrity()` computes SHA-256 via `ssri` — this triggers full content loading if not already loaded. +- Monitored readers/writers (`MonitoredReader`, `MonitoredReaderWriter`) prevent reads/writes after being sealed. They track all access patterns for build caching analysis. + +## Running Tests + +```bash +# All tests +npm run unit --workspace=@ui5/fs + +# Single file +cd packages/fs && npx ava test/lib/Resource.js + +# Verbose with logging +npm run unit-verbose --workspace=@ui5/fs + +# Coverage +npm run coverage --workspace=@ui5/fs +``` diff --git a/.claude/skills/ui5-fs/architecture.md b/.claude/skills/ui5-fs/architecture.md new file mode 100644 index 00000000000..84d9aa92b7e --- /dev/null +++ b/.claude/skills/ui5-fs/architecture.md @@ -0,0 +1,381 @@ +# @ui5/fs Architecture Reference + +## Component Map + +| Component | File | Purpose | +|-----------|------|---------| +| **AbstractReader** | `lib/AbstractReader.js` | Abstract base for all readers. Defines `byPath()`, `byGlob()` | +| **AbstractReaderWriter** | `lib/AbstractReaderWriter.js` | Extends AbstractReader with `write()` | +| **Resource** | `lib/Resource.js` | Core file representation: content + metadata | +| **ResourceFacade** | `lib/ResourceFacade.js` | Wraps Resource with remapped virtual path | +| **ResourceTagCollection** | `lib/ResourceTagCollection.js` | Tag storage and validation (`"namespace:Name"`) | +| **MonitoredResourceTagCollection** | `lib/MonitoredResourceTagCollection.js` | Wraps ResourceTagCollection with access tracking | +| **MonitoredReader** | `lib/MonitoredReader.js` | Wraps reader with access tracking (seals after use) | +| **MonitoredReaderWriter** | `lib/MonitoredReaderWriter.js` | Wraps reader/writer with access tracking | +| **ReaderCollection** | `lib/ReaderCollection.js` | Parallel multi-reader aggregation | +| **ReaderCollectionPrioritized** | `lib/ReaderCollectionPrioritized.js` | Sequential prioritized multi-reader | +| **DuplexCollection** | `lib/DuplexCollection.js` | Combined reader + writer (writer shadows reader) | +| **WriterCollection** | `lib/WriterCollection.js` | Path-prefix-routed multi-writer | +| **FileSystem adapter** | `lib/adapters/FileSystem.js` | Real filesystem adapter (globby, graceful-fs) | +| **Memory adapter** | `lib/adapters/Memory.js` | In-memory virtual adapter (micromatch) | +| **AbstractAdapter** | `lib/adapters/AbstractAdapter.js` | Base adapter: virBasePath, excludes, path normalization | +| **Filter reader** | `lib/readers/Filter.js` | Callback-based resource filtering | +| **Link reader** | `lib/readers/Link.js` | Virtual path remapping (returns ResourceFacade) | +| **Proxy reader** | `lib/readers/Proxy.js` | Custom getResource/listResourcePaths callbacks | +| **resourceFactory** | `lib/resourceFactory.js` | Factory functions for all components | +| **fsInterface** | `lib/fsInterface.js` | Node.js fs-compatible wrapper over readers | +| **Trace** | `lib/tracing/Trace.js` | Performance tracing (active at log level "silly") | +| **traceSummary** | `lib/tracing/traceSummary.js` | Aggregates trace reports | + +## Class Hierarchy + +``` +AbstractReader +├── AbstractReaderWriter +│ ├── AbstractAdapter +│ │ ├── adapters/FileSystem +│ │ └── adapters/Memory +│ ├── DuplexCollection +│ ├── WriterCollection +│ └── MonitoredReaderWriter +├── ReaderCollection +├── ReaderCollectionPrioritized +├── readers/Filter +├── readers/Link +├── readers/Proxy +└── MonitoredReader + +Resource (standalone) +ResourceFacade (wraps Resource) +ResourceTagCollection (standalone) +MonitoredResourceTagCollection (wraps ResourceTagCollection) +``` + +## Resource Content Model + +### Content Types (internal state machine) + +``` +FACTORY ──getBuffer()/getString()/getStreamAsync()──> BUFFER +BUFFER ──setStream()──> STREAM +STREAM ──getBuffer()/getString()──> BUFFER (consumes stream) +BUFFER ──setBuffer()/setString()──> BUFFER +* ──modifyStream()──> IN_TRANSFORMATION ──callback done──> BUFFER +* ──write({drain:true})──> DRAINED_STREAM +``` + +- **FACTORY**: Lazy — content created on demand via `createBuffer`/`createStream` factories +- **BUFFER**: Content fully materialized in memory +- **STREAM**: Readable stream (single-consume; converted to BUFFER on read) +- **DRAINED_STREAM**: Content was released after write; further reads throw +- **IN_TRANSFORMATION**: Locked during `modifyStream()` callback + +### Content Creation (mutually exclusive parameters) + +```javascript +new Resource({ + path: "/resources/my/File.js", // Required, absolute POSIX + buffer: Buffer.from("..."), // OR + string: "...", // OR + stream: readableStream, // OR + createBuffer: async () => buf, // OR + createStream: async () => stream // (factories for lazy loading) +}); +``` + +### Content Access + +| Method | Returns | Notes | +|--------|---------|-------| +| `getBuffer()` | `Promise` | Materializes content if needed | +| `getString()` | `Promise` | UTF-8 decoded buffer | +| `getStreamAsync()` | `Promise` | Preferred over deprecated `getStream()` | +| `getStream()` | `stream.Readable` | **Deprecated** — sync, logs warning once | +| `setBuffer(buf)` | — | Marks resource as modified | +| `setString(str)` | — | Marks resource as modified | +| `setStream(stream\|fn)` | — | Accepts stream or factory function | +| `modifyStream(cb)` | `Promise` | Acquires mutex; `cb(stream) => stream\|Buffer` | + +### Metadata + +| Method | Returns | Notes | +|--------|---------|-------| +| `getPath()` | `string` | Absolute POSIX virtual path | +| `setPath(path)` | — | Updates path (throws on ResourceFacade) | +| `getOriginalPath()` | `string` | For ResourceFacade: underlying path | +| `getName()` | `string` | Basename of path | +| `isDirectory()` | `boolean` | — | +| `getLastModified()` | `number` | ms since epoch | +| `getInode()` | `number` | Filesystem inode | +| `getSize()` | `Promise` | Byte size (triggers content load) | +| `hasSize()` | `boolean` | True if size known without content load | +| `getIntegrity()` | `Promise` | SHA-256 via ssri (triggers content load) | +| `isModified()` | `boolean` | True if content changed since creation | +| `getProject()` | `object` | Associated @ui5/project | +| `getSourceMetadata()` | `object` | `{adapter, fsPath, contentModified}` | +| `clone()` | `Promise` | Independent deep copy | +| `getTags()` | `ResourceTagCollection` | Tag operations for this resource | +| `pushCollection(name)` | — | Track retrieval collection (tracing) | + +## Adapter Internals + +### AbstractAdapter + +Base class for FileSystem and Memory adapters. Extends `AbstractReaderWriter`. + +**Constructor params:** `{name, virBasePath, excludes, project}` + +**Key internal methods:** +- `_isPathHandled(virPath)` — checks if path falls under `virBasePath` +- `_isPathExcluded(virPath)` — checks if path matches exclude patterns +- `_resolveVirtualPathToBase(virPath, writeMode)` — strips `virBasePath` prefix, returns relative path +- `_normalizePattern(virPattern)` — makes glob patterns relative to adapter base +- `_createResource(params)` — creates Resource with project and source metadata injected + +### FileSystem Adapter + +Maps a `virBasePath` to a `fsBasePath` on the real filesystem. + +**Additional constructor params:** `{fsBasePath, useGitignore}` + +**Glob:** Uses `globby` with `gitignore` support. Glob patterns are resolved relative to `fsBasePath`. + +**Content loading:** Uses factory functions for lazy loading: +```javascript +createStream: () => createReadStream(fsPath) +createBuffer: () => readFile(fsPath) +``` + +**Write optimization:** +1. Unmodified resource with same source adapter → `fs.copyFile` (fast path) +2. Source path === target path and content unmodified → skip write entirely +3. Source path === target path but content modified → read to buffer first (avoids read-during-write) +4. Otherwise → pipe stream to write stream + +**Read-only:** When `write({readOnly: true})`, sets `chmod 0o444` after writing. + +### Memory Adapter + +Virtual in-memory storage. No filesystem access. + +**Internal data structures:** +- `_virFiles` — Map of virtual path → Resource +- `_virDirs` — Map of virtual path → Resource (directory entries) + +**Glob:** Uses `micromatch` against keys of `_virFiles`/`_virDirs`. + +**Write:** Clones resource, auto-creates parent directory hierarchy entries. + +**Read:** Returns clones of stored resources (never the originals). + +## Collection Patterns + +### ReaderCollection (parallel) + +```javascript +// byGlob: Promise.all across all readers, concat results +// byPath: Promise.all, return first non-null +``` + +Use when sources are independent and results should be aggregated. + +### ReaderCollectionPrioritized (sequential priority) + +```javascript +// byGlob: runs all readers, deduplicates by path (first reader wins) +// byPath: tries readers in order, returns first match +``` + +Use when one source should shadow another (e.g., local overrides). + +### DuplexCollection (reader + writer) + +Internally creates `ReaderCollectionPrioritized([writer, reader])`: +- Reads check writer first (written resources shadow originals) +- Writes go to the writer adapter + +**Primary use case:** Build workspace — write intermediate results to memory, which then take priority over source files on read. + +### WriterCollection (path-routed writes) + +Routes `write()` calls based on longest-matching path prefix: +```javascript +new WriterCollection({ + writerMapping: { + "/": defaultWriter, + "/resources/my-lib/": libWriter, // matches /resources/my-lib/** + } +}); +``` + +For reading, creates an internal `ReaderCollection` from all unique writers. + +## Specialized Readers + +### Filter + +Wraps a reader with a predicate callback: +```javascript +new Filter({ + reader: sourceReader, + callback: (resource) => !resource.getPath().endsWith(".map") +}); +``` +- `byGlob()` filters results after reader returns them +- `byPath()` returns null if callback rejects the resource + +### Link + +Remaps virtual paths between `linkPath` and `targetPath`: +```javascript +new Link({ + reader: sourceReader, + pathMapping: { linkPath: "/app", targetPath: "/resources/my-app/" } +}); +``` +- Returns `ResourceFacade` instances with remapped paths +- `byPath("/app/Component.js")` → reads `/resources/my-app/Component.js` from source reader +- `byGlob("/app/**")` → resolves patterns against target path, returns facades + +### Proxy + +Generic callback-based reader for custom resource sources: +```javascript +new Proxy({ + name: "my-proxy", + getResource: async (virPath) => resource, + listResourcePaths: async () => ["/path/a.js", "/path/b.js"] +}); +``` + +## resourceFactory API + +The primary entry point for creating @ui5/fs components. + +| Function | Returns | Purpose | +|----------|---------|---------| +| `createAdapter({name, virBasePath, fsBasePath?, ...})` | FileSystem or Memory adapter | FileSystem if `fsBasePath` given, else Memory | +| `createReader({fsBasePath, virBasePath, ...})` | ReaderCollection | FS adapter wrapped in ReaderCollection | +| `createReaderCollection({name, readers})` | ReaderCollection | Parallel collection | +| `createReaderCollectionPrioritized({name, readers})` | ReaderCollectionPrioritized | Priority collection | +| `createWriterCollection({name, writerMapping})` | WriterCollection | Path-routed writer | +| `createWorkspace({reader, writer?, virBasePath?, name?})` | DuplexCollection | Build workspace pattern | +| `createResource(params)` | Resource | Resource instance | +| `createFilterReader({reader, callback})` | Filter | Filtered reader | +| `createLinkReader({reader, pathMapping})` | Link | Path-remapping reader | +| `createFlatReader({name, reader, namespace})` | Link | Shorthand: maps `/` → `/resources//` | +| `createProxy({name, getResource, listResourcePaths})` | Proxy | Custom source reader | +| `createMonitor(readerWriter)` | MonitoredReaderWriter | Access-tracking wrapper | +| `prefixGlobPattern(virPattern, virBaseDir)` | string | Prepend base directory to glob pattern | + +## Monitoring System + +Used by the incremental build system to track what resources were accessed during a build task. + +### MonitoredReader / MonitoredReaderWriter + +Wraps a reader/writer and records all `byPath()`, `byGlob()`, and `write()` calls. + +```javascript +const monitored = createMonitor(adapter); +// ... perform operations ... +const requests = monitored.getResourceRequests(); +// { paths: ["/resources/a.js"], patterns: ["**/*.xml"] } +``` + +After sealing, further access throws an error. + +### MonitoredResourceTagCollection + +Wraps `ResourceTagCollection` and tracks all `setTag()`, `getTag()`, `clearTag()` calls. + +```javascript +const operations = monitoredTags.getTagOperations(); +// Map { "/resources/a.js" => { "build:IsDebug": true } } +``` + +## Tag System + +Tags follow the format `"namespace:Name"`: +- **Namespace**: lowercase alphanumeric, starts with letter (e.g., `build`, `theme`) +- **Name**: PascalCase alphanumeric (e.g., `IsDebugVariant`, `IsPartOfBuild`) +- **Values**: `string | number | boolean` (default: `true`) + +Tags are stored per-resource-path in `ResourceTagCollection._pathTags`. + +Allowed tags/namespaces are validated on `setTag()`. Tags can be initialized from a serialized object via the `tags` constructor parameter. + +## Public Exports + +```javascript +import AbstractReader from "@ui5/fs/AbstractReader"; +import AbstractReaderWriter from "@ui5/fs/AbstractReaderWriter"; +import DuplexCollection from "@ui5/fs/DuplexCollection"; +import ReaderCollection from "@ui5/fs/ReaderCollection"; +import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; +import Resource from "@ui5/fs/Resource"; +import fsInterface from "@ui5/fs/fsInterface"; +import {createAdapter, createResource, ...} from "@ui5/fs/resourceFactory"; +import FileSystem from "@ui5/fs/adapters/FileSystem"; +import Memory from "@ui5/fs/adapters/Memory"; +import Filter from "@ui5/fs/readers/Filter"; +import Link from "@ui5/fs/readers/Link"; +import Proxy from "@ui5/fs/readers/Proxy"; + +// Internal (not part of public API contract): +import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; +import MonitoredResourceTagCollection from "@ui5/fs/internal/MonitoredResourceTagCollection"; +``` + +## Dependencies + +| Package | Purpose | +|---------|---------| +| `@ui5/logger` | Logging (`getLogger("fs:Resource")`, etc.) | +| `async-mutex` | Mutex for concurrent content access in Resource | +| `clone` | Deep cloning resources in Memory adapter | +| `globby` | File globbing in FileSystem adapter | +| `graceful-fs` | Robust fs operations in FileSystem adapter | +| `micromatch` | Pattern matching in Memory adapter | +| `minimatch` | Glob parsing in resourceFactory | +| `ssri` | SHA-256 integrity hashing in Resource | +| `pretty-hrtime` | Human-readable time in Trace | +| `random-int` | Result order randomization in AbstractReader | + +## Test Structure + +Tests mirror the lib structure under `packages/fs/test/lib/`: + +``` +test/ +├── lib/ +│ ├── AbstractReader.js +│ ├── AbstractReaderWriter.js +│ ├── Resource.js +│ ├── ResourceFacade.js +│ ├── ResourceTagCollection.js +│ ├── MonitoredReader.js +│ ├── MonitoredReaderWriter.js +│ ├── MonitoredResourceTagCollection.js +│ ├── ReaderCollection.js +│ ├── ReaderCollectionPrioritized.js +│ ├── DuplexCollection.js +│ ├── WriterCollection.js +│ ├── fsInterface.js +│ ├── resourceFactory.js +│ ├── adapters/ +│ │ ├── AbstractAdapter.js +│ │ ├── FileSystem.js +│ │ ├── FileSystem_write.js +│ │ └── Memory.js +│ ├── readers/ +│ │ ├── Filter.js +│ │ ├── Link.js +│ │ └── Proxy.js +│ └── tracing/ +│ └── Trace.js +└── fixtures/ + └── ... +``` + +Framework: AVA with `esmock` for ESM module mocking and `sinon` for stubs/spies. diff --git a/.gitignore b/.gitignore index 69f95b1b683..b5616c9274b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,7 @@ internal/documentation/.vitepress/cache internal/documentation/dist internal/documentation/schema/* internal/documentation/docs/api -internal/documentation/tmp \ No newline at end of file +internal/documentation/tmp + +# E2E tests +internal/e2e-tests/tmp \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..72248c362e0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +UI5 CLI v5 — an open, modular toolchain for developing UI5 framework applications. This is a **monorepo** using npm workspaces containing 6 public packages and 2 internal packages. All code uses **ESM** (`"type": "module"`). + +**Node requirement**: `^22.20.0 || >=24.0.0` + +## Commands + +### Install +```bash +npm ci --engine-strict +``` + +### Run all tests (lint + coverage + license checks + knip) +```bash +npm test +``` + +### Lint +```bash +npm run lint # All workspaces +npm run lint --workspace=@ui5/cli # Single package +``` + +### Unit tests +```bash +npm run unit # All workspaces +npm run unit --workspace=@ui5/builder # Single package +npx ava test/lib/some/test.js # Single test file (run from package dir) +``` + +### Unit tests with verbose logging +```bash +npm run unit-verbose --workspace=@ui5/cli +``` + +### Coverage +```bash +npm run coverage # All workspaces +npm run coverage --workspace=@ui5/server # Single package +``` + +## Architecture + +``` +@ui5/cli CLI entry point (yargs). Commands in lib/cli/commands/ + ├── @ui5/project Project graph, config loading, dependency resolution (AJV schemas) + ├── @ui5/builder Build tasks & processors (JS bundling, minification, CSS, JSDoc) + ├── @ui5/server Express dev server with middleware architecture + ├── @ui5/fs Virtual file system with AbstractReader/AbstractReaderWriter + └── @ui5/logger Logging with configurable levels and progress bars +``` + +- **Entry point**: `packages/cli/bin/ui5.cjs` (CJS wrapper for Node compatibility) → loads `packages/cli/lib/cli/cli.js` (ESM) +- **Builder** uses a task/processor pattern: tasks orchestrate processors, both are loaded from repositories (`taskRepository.js`) +- **Server** uses an Express middleware pipeline; middlewares are loaded dynamically via `MiddlewareManager` +- **FS** provides a resource abstraction layer — `ReaderCollection` composes multiple readers; adapters handle real FS backends +- **Project** builds a `ProjectGraph` of dependencies from npm packages and UI5 config files (YAML/XML); validates configs with AJV JSON schemas + +Internal packages: +- `internal/documentation` — VitePress docs + JSDoc + JSON schema generation +- `internal/shrinkwrap-extractor` — npm shrinkwrap utilities + +## Code Style + +- **Indentation**: tabs +- **Quotes**: double +- **Max line length**: 120 +- **Semicolons**: required +- **No `console.log()`** — use `@ui5/logger` instead +- ESLint flat config with Google style base + JSDoc + AVA plugins + +## Testing + +- **Framework**: AVA with `esmock` for ESM module mocking, `sinon` for stubs/spies +- **Test location**: `packages/*/test/lib/**/*.js` +- **Helpers** (excluded from test runs): `test/**/__helper__/**` +- **Fixtures**: `test/fixtures/`, **Expected outputs**: `test/expected/` +- **Coverage**: NYC (Istanbul) — thresholds vary per package (70-90%) +- **Temp files**: `test/tmp/` (cleaned before each run via `rimraf`) + +## Commit Convention + +Conventional commits enforced via commitlint + husky. Subject must be sentence-case. + +**Types**: `build`, `ci`, `deps`, `docs`, `feat`, `fix`, `perf`, `refactor`, `release`, `revert`, `style`, `test` + +**Scopes** are package names: `builder`, `cli`, `documentation`, `fs`, `logger`, `project`, `server`, `shrinkwrap-extractor`. Some types restrict which scopes are valid (e.g., `feat` and `fix` only allow public package scopes). + +Examples: `feat(builder): Add CSS source map support`, `fix(server): Correct middleware ordering` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..eef4bd20cf9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file diff --git a/internal/documentation/docs/updates/migrate-v5.md b/internal/documentation/docs/updates/migrate-v5.md index b4de575af10..e5f9fe91748 100644 --- a/internal/documentation/docs/updates/migrate-v5.md +++ b/internal/documentation/docs/updates/migrate-v5.md @@ -15,6 +15,8 @@ Or update your global install via: `npm i --global @ui5/cli@next` - **@ui5/cli: `ui5 init` defaults to Specification Version 5.0** +- **Rename: Command Option `--cache-mode` is now `--snapshot-cache`** + ## Node.js and npm Version Support @@ -27,6 +29,12 @@ UI5 CLI 5.x introduces **Specification Version 5.0**, which enables the new Comp Projects using older **Specification Versions** are expected to be **fully compatible with UI5 CLI v5**. +## Rename of Command Option + +With Specification Version 5.0, the option `--cache-mode` (for commands `ui5 build` and `ui5 serve`) has been renamed to `--snapshot-cache`. + +The behavior remains the same. When `--cache-mode` is used, a deprecation warning is logged and `--snapshot-cache` is set to `Default`. + ## UI5 CLI Init Command The `ui5 init` command now generates projects with Specification Version 5.0 by default. diff --git a/package-lock.json b/package-lock.json index 0c94af61718..9b356124cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,8 +97,6 @@ }, "internal/shrinkwrap-extractor/node_modules/@npmcli/arborist": { "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-9.4.2.tgz", - "integrity": "sha512-omJgPyzt11cEGrxzgrECoOyxAunmPMgBFTcAB/FbaB+9iOYhGmRdsQqySV8o0LWQ/l2kTeASUIMR4xJufVwmtw==", "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", @@ -145,44 +143,36 @@ }, "internal/shrinkwrap-extractor/node_modules/lru-cache": { "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "internal/shrinkwrap-extractor/node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "license": "ISC", + "internal/shrinkwrap-extractor/node_modules/minimatch": { + "version": "10.2.5", + "license": "BlueOak-1.0.0", "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@adobe/css-tools": { "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "license": "MIT" }, "node_modules/@algolia/abtesting": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.16.1.tgz", - "integrity": "sha512-Xxk4l00pYI+jE0PNw8y0MvsQWh5278WRtZQav8/BMMi3HKi2xmeuqe11WJ3y8/6nuBHdv39w76OpJb09TMfAVQ==", + "version": "1.16.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" @@ -190,8 +180,6 @@ }, "node_modules/@algolia/autocomplete-core": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", - "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", "license": "MIT", "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", @@ -200,8 +188,6 @@ }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", - "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", "license": "MIT", "dependencies": { "@algolia/autocomplete-shared": "1.17.7" @@ -212,8 +198,6 @@ }, "node_modules/@algolia/autocomplete-preset-algolia": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", - "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", "license": "MIT", "dependencies": { "@algolia/autocomplete-shared": "1.17.7" @@ -225,8 +209,6 @@ }, "node_modules/@algolia/autocomplete-shared": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", - "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", "license": "MIT", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -234,180 +216,154 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.50.1.tgz", - "integrity": "sha512-4peZlPXMwTOey9q1rQKMdCnwZb/E95/1e+7KujXpLLSh0FawJzg//U2NM+r4AiJy4+naT2MTBhj0K30yshnVTA==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.50.1.tgz", - "integrity": "sha512-i+aWHHG8NZvGFHtPeMZkxL2Loc6Fm7iaRo15lYSMx8gFL+at9vgdWxhka7mD1fqxkrxXsQstUBCIsSY8FvkEOw==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.50.1.tgz", - "integrity": "sha512-Hw52Fwapyk/7hMSV/fI4+s3H9MGZEUcRh4VphyXLAk2oLYdndVUkc6KBi0zwHSzwPAr+ZBwFPe2x6naUt9mZGw==", + "version": "5.50.2", "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.50.1.tgz", - "integrity": "sha512-Bn/wtwhJ7p1OD/6pY+Zzn+zlu2N/SJnH46md/PAbvqIzmjVuwjNwD4y0vV5Ov8naeukXdd7UU9v550+v8+mtlg==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.50.1.tgz", - "integrity": "sha512-0V4Tu0RWR8YxkgI9EPVOZHGE4K5pEIhkLNN0CTkP/rnPsqaaSQpNMYW3/mGWdiKOWbX0iVmwLB9QESk3H0jS5g==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.50.1.tgz", - "integrity": "sha512-jofcWNYMXJDDr87Z2eivlWY6o71Zn7F7aOvQCXSDAo9QTlyf7BhXEsZymLUvF0O1yU9Q9wvrjAWn8uVHYnAvgw==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.50.1.tgz", - "integrity": "sha512-OteRb8WubcmEvU0YlMJwCXs3Q6xrdkb0v50/qZBJP1TF0CvujFZQM++9BjEkTER/Jr9wbPHvjSFKnbMta0b4dQ==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.50.1.tgz", - "integrity": "sha512-0GmfSgDQK6oiIVXnJvGxtNFOfosBspRTR7csCOYCTL1P8QtxX2vDCIKwTM7xdSAEbJaZ43QlWg25q0Qdsndz8Q==", + "version": "1.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.50.1.tgz", - "integrity": "sha512-ySuigKEe4YjYV3si8NVk9BHQpFj/1B+ON7DhhvTvbrZJseHQQloxzq0yHwKmznSdlO6C956fx4pcfOKkZClsyg==", + "version": "1.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.50.1.tgz", - "integrity": "sha512-Cp8T/B0gVmjFlzzp6eP47hwKh5FGyeqQp1N48/ANDdvdiQkPqLyFHQVDwLBH0LddfIPQE+yqmZIgmKc82haF4A==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "@algolia/client-common": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.50.1.tgz", - "integrity": "sha512-XKdGGLikfrlK66ZSXh/vWcXZZ8Vg3byDFbJD8pwEvN1FoBRGxhxya476IY2ohoTymLa4qB5LBRlIa+2TLHx3Uw==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1" + "@algolia/client-common": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.50.1.tgz", - "integrity": "sha512-mBAU6WyVsDwhHyGM+nodt1/oebHxgvuLlOAoMGbj/1i6LygDHZWDgL1t5JEs37x9Aywv7ZGhqbM1GsfZ54sU6g==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1" + "@algolia/client-common": "5.50.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.50.1.tgz", - "integrity": "sha512-qmo1LXrNKLHvJE6mdQbLnsZAoZvj7VyF2ft4xmbSGWI2WWm87fx/CjUX4kEExt4y0a6T6nEts6ofpUfH5TEE1A==", + "version": "5.50.2", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.50.1" + "@algolia/client-common": "5.50.2" }, "engines": { "node": ">= 14.0.0" @@ -415,8 +371,6 @@ }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", - "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==", "dev": true, "license": "MIT", "dependencies": { @@ -434,8 +388,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -448,8 +400,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -458,8 +408,6 @@ }, "node_modules/@babel/core": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { @@ -489,8 +437,6 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -499,8 +445,6 @@ }, "node_modules/@babel/generator": { "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { @@ -516,8 +460,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { @@ -529,8 +471,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { @@ -546,8 +486,6 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -556,8 +494,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "dev": true, "license": "MIT", "dependencies": { @@ -578,8 +514,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -588,8 +522,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -598,8 +530,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { @@ -612,8 +542,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { @@ -626,8 +554,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { @@ -644,8 +570,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "license": "MIT", "dependencies": { @@ -657,8 +581,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -667,8 +589,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dev": true, "license": "MIT", "dependencies": { @@ -685,8 +605,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { @@ -699,8 +617,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -708,8 +624,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -717,8 +631,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -727,8 +639,6 @@ }, "node_modules/@babel/helpers": { "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { @@ -741,8 +651,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -756,8 +664,6 @@ }, "node_modules/@babel/plugin-syntax-decorators": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", - "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -772,8 +678,6 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { @@ -788,8 +692,6 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, "license": "MIT", "dependencies": { @@ -804,8 +706,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "dev": true, "license": "MIT", "dependencies": { @@ -821,8 +721,6 @@ }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "dev": true, "license": "MIT", "dependencies": { @@ -841,8 +739,6 @@ }, "node_modules/@babel/preset-typescript": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "dev": true, "license": "MIT", "dependencies": { @@ -861,8 +757,6 @@ }, "node_modules/@babel/template": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { @@ -876,8 +770,6 @@ }, "node_modules/@babel/traverse": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { @@ -895,8 +787,6 @@ }, "node_modules/@babel/types": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -908,21 +798,15 @@ }, "node_modules/@blueoak/list": { "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@blueoak/list/-/list-15.0.0.tgz", - "integrity": "sha512-xW5Xb9Fr3WtYAOwavxxWL0CaJK/ReT+HKb5/R6dR1p9RVJ55MTdaxPdeTKY2ukhFchv2YHPMM8YuZyfyLqxedg==", "dev": true, "license": "CC0-1.0" }, "node_modules/@colordx/core": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.0.3.tgz", - "integrity": "sha512-xBQ0MYRTNNxW3mS2sJtlQTT7C3Sasqgh1/PsHva7fyDb5uqYY+gv9V0utDdX8X80mqzbGz3u/IDJdn2d/uW09g==", "license": "MIT" }, "node_modules/@commitlint/cli": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", - "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -943,8 +827,6 @@ }, "node_modules/@commitlint/config-conventional": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", - "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", "dev": true, "license": "MIT", "dependencies": { @@ -957,8 +839,6 @@ }, "node_modules/@commitlint/config-validator": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", - "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -971,8 +851,6 @@ }, "node_modules/@commitlint/ensure": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", - "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", "dev": true, "license": "MIT", "dependencies": { @@ -989,8 +867,6 @@ }, "node_modules/@commitlint/execute-rule": { "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", - "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", "dev": true, "license": "MIT", "engines": { @@ -999,8 +875,6 @@ }, "node_modules/@commitlint/format": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", - "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1013,8 +887,6 @@ }, "node_modules/@commitlint/is-ignored": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", - "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", "dev": true, "license": "MIT", "dependencies": { @@ -1027,8 +899,6 @@ }, "node_modules/@commitlint/lint": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", - "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", "dev": true, "license": "MIT", "dependencies": { @@ -1043,8 +913,6 @@ }, "node_modules/@commitlint/load": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", - "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", "dev": true, "license": "MIT", "dependencies": { @@ -1064,8 +932,6 @@ }, "node_modules/@commitlint/message": { "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", - "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==", "dev": true, "license": "MIT", "engines": { @@ -1074,8 +940,6 @@ }, "node_modules/@commitlint/parse": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", - "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", "dev": true, "license": "MIT", "dependencies": { @@ -1089,8 +953,6 @@ }, "node_modules/@commitlint/read": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", - "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", "dev": true, "license": "MIT", "dependencies": { @@ -1106,8 +968,6 @@ }, "node_modules/@commitlint/resolve-extends": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", - "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", "dev": true, "license": "MIT", "dependencies": { @@ -1124,8 +984,6 @@ }, "node_modules/@commitlint/rules": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", - "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1140,8 +998,6 @@ }, "node_modules/@commitlint/to-lines": { "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", - "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", "dev": true, "license": "MIT", "engines": { @@ -1150,8 +1006,6 @@ }, "node_modules/@commitlint/top-level": { "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz", - "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1163,8 +1017,6 @@ }, "node_modules/@commitlint/types": { "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", - "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", "dev": true, "license": "MIT", "dependencies": { @@ -1176,9 +1028,7 @@ } }, "node_modules/@conventional-changelog/git-client": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.6.0.tgz", - "integrity": "sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg==", + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { @@ -1191,7 +1041,7 @@ }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.3.0" + "conventional-commits-parser": "^6.4.0" }, "peerDependenciesMeta": { "conventional-commits-filter": { @@ -1204,14 +1054,10 @@ }, "node_modules/@docsearch/css": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", - "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", "license": "MIT" }, "node_modules/@docsearch/js": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", - "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", "license": "MIT", "dependencies": { "@docsearch/react": "3.8.2", @@ -1220,8 +1066,6 @@ }, "node_modules/@docsearch/react": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", - "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", "license": "MIT", "dependencies": { "@algolia/autocomplete-core": "1.17.7", @@ -1251,47 +1095,43 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@epic-web/invariant": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", "dev": true, "license": "MIT" }, "node_modules/@es-joy/jsdoccomment": { "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", - "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1307,629 +1147,183 @@ }, "node_modules/@es-joy/resolve.exports": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", - "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", "dev": true, "license": "MIT", "engines": { "node": ">=10" } }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ - "ppc64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "aix" + "darwin" ], "engines": { "node": ">=12" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@gar/promise-retry": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", - "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", "license": "MIT", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -1937,8 +1331,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1947,8 +1339,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1961,8 +1351,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1975,8 +1363,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1988,9 +1374,7 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.76", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.76.tgz", - "integrity": "sha512-lLRlA8yaf+1L5VCPRvR9lynoSklsddKHEylchmZJKdj/q2xVQ1ZAEJ8SCQlv9cbgtMefnlyM98U+8Si2aoFZPA==", + "version": "1.2.78", "license": "CC0-1.0", "dependencies": { "@iconify/types": "*" @@ -1998,14 +1382,10 @@ }, "node_modules/@iconify/types": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "license": "MIT" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -2021,14 +1401,10 @@ }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2042,10 +1418,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -2056,14 +1445,10 @@ }, "node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", - "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "license": "ISC" }, "node_modules/@istanbuljs/esm-loader-hook": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/esm-loader-hook/-/esm-loader-hook-0.3.0.tgz", - "integrity": "sha512-lEnYroBUYfNQuJDYrPvre8TSwPZnyIQv9qUT3gACvhr3igZr+BbrdyIcz4+2RnEXZzi12GqkUW600+QQPpIbVg==", "dev": true, "license": "ISC", "dependencies": { @@ -2081,8 +1466,6 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2098,18 +1481,26 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -2120,10 +1511,44 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", "dev": true, "license": "MIT", "engines": { @@ -2132,8 +1557,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2142,8 +1565,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2152,8 +1573,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2161,8 +1580,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2171,14 +1588,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2186,12 +1599,10 @@ } }, "node_modules/@jsdoc/salty": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.11.tgz", - "integrity": "sha512-luR/TZqgru6gNjBQnRIbzNPOmDG62VIFQO7QyEjc1/zk3VP3yoGfuecwP2uOlAmKz+t6aq9bwsBV3FgiyHTT7Q==", + "version": "0.2.12", "license": "Apache-2.0", "dependencies": { - "lodash": "^4.17.23" + "lodash": "^4.18.1" }, "engines": { "node": ">=v12.0.0" @@ -2199,8 +1610,6 @@ }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", - "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2219,10 +1628,31 @@ "node": ">=18" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.4", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2239,8 +1669,6 @@ }, "node_modules/@noble/hashes": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", "engines": { @@ -2252,8 +1680,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2265,8 +1691,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", "engines": { "node": ">= 8" @@ -2274,8 +1698,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2287,8 +1709,6 @@ }, "node_modules/@npmcli/agent": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", - "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", "license": "ISC", "dependencies": { "agent-base": "^7.1.0", @@ -2302,9 +1722,7 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.5", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2312,8 +1730,6 @@ }, "node_modules/@npmcli/arborist": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.4.tgz", - "integrity": "sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g==", "dev": true, "license": "ISC", "dependencies": { @@ -2362,8 +1778,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/agent": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", "dev": true, "license": "ISC", "dependencies": { @@ -2379,8 +1793,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/fs": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, "license": "ISC", "dependencies": { @@ -2392,8 +1804,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/git": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", - "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2413,8 +1823,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/installed-package-contents": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", - "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", "dev": true, "license": "ISC", "dependencies": { @@ -2430,8 +1838,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/map-workspaces": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", - "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", "dev": true, "license": "ISC", "dependencies": { @@ -2446,8 +1852,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/metavuln-calculator": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-7.1.1.tgz", - "integrity": "sha512-Nkxf96V0lAx3HCpVda7Vw4P23RILgdi/5K1fmj2tZkWIYLpXAN8k2UVVOsW16TsS5F8Ws2I7Cm+PU1/rsVF47g==", "dev": true, "license": "ISC", "dependencies": { @@ -2463,8 +1867,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", - "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", "dev": true, "license": "ISC", "engines": { @@ -2473,8 +1875,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/node-gyp": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", "dev": true, "license": "ISC", "engines": { @@ -2483,8 +1883,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/package-json": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", - "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2502,8 +1900,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/promise-spawn": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", - "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2515,8 +1911,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/query": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-3.1.0.tgz", - "integrity": "sha512-C/iR0tk7KSKGldibYIB9x8GtO/0Bd0I2mhOaDb8ucQL/bQVTmGoeREaFj64Z5+iCBRf3dQfed0CjJL7I8iTkiQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2528,8 +1922,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/redact": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", - "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", "dev": true, "license": "ISC", "engines": { @@ -2538,8 +1930,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@npmcli/run-script": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", - "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", "dev": true, "license": "ISC", "dependencies": { @@ -2556,8 +1946,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/bundle": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", - "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2569,8 +1957,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/core": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", - "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2579,8 +1965,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/protobuf-specs": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", - "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2589,8 +1973,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", - "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2607,8 +1989,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/@npmcli/agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, "license": "ISC", "dependencies": { @@ -2624,8 +2004,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/@npmcli/fs": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "license": "ISC", "dependencies": { @@ -2637,8 +2015,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/cacache": { "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2661,8 +2037,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/make-fetch-happen": { "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2684,8 +2058,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/minipass-fetch": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2702,8 +2074,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -2715,8 +2085,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -2725,8 +2093,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -2738,8 +2104,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/proc-log": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, "license": "ISC", "engines": { @@ -2748,8 +2112,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/ssri": { "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2761,8 +2123,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/sign/node_modules/unique-filename": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2774,8 +2134,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/tuf": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", - "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2788,8 +2146,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@sigstore/verify": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", - "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2803,8 +2159,6 @@ }, "node_modules/@npmcli/arborist/node_modules/@tufjs/models": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", - "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", "dev": true, "license": "MIT", "dependencies": { @@ -2817,8 +2171,6 @@ }, "node_modules/@npmcli/arborist/node_modules/abbrev": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", "dev": true, "license": "ISC", "engines": { @@ -2827,15 +2179,11 @@ }, "node_modules/@npmcli/arborist/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@npmcli/arborist/node_modules/bin-links": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz", - "integrity": "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==", "dev": true, "license": "ISC", "dependencies": { @@ -2849,9 +2197,7 @@ } }, "node_modules/@npmcli/arborist/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", "dev": true, "license": "MIT", "dependencies": { @@ -2860,8 +2206,6 @@ }, "node_modules/@npmcli/arborist/node_modules/cacache": { "version": "18.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", - "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2884,8 +2228,6 @@ }, "node_modules/@npmcli/arborist/node_modules/cmd-shim": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.3.tgz", - "integrity": "sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==", "dev": true, "license": "ISC", "engines": { @@ -2894,15 +2236,30 @@ }, "node_modules/@npmcli/arborist/node_modules/common-ancestor-path": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", - "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "dev": true, "license": "ISC" }, + "node_modules/@npmcli/arborist/node_modules/glob": { + "version": "10.5.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@npmcli/arborist/node_modules/hosted-git-info": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "license": "ISC", "dependencies": { @@ -2914,8 +2271,6 @@ }, "node_modules/@npmcli/arborist/node_modules/ignore-walk": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", - "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2927,8 +2282,6 @@ }, "node_modules/@npmcli/arborist/node_modules/ini": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "dev": true, "license": "ISC", "engines": { @@ -2937,8 +2290,6 @@ }, "node_modules/@npmcli/arborist/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2947,8 +2298,6 @@ }, "node_modules/@npmcli/arborist/node_modules/json-parse-even-better-errors": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, "license": "MIT", "engines": { @@ -2957,15 +2306,11 @@ }, "node_modules/@npmcli/arborist/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/@npmcli/arborist/node_modules/make-fetch-happen": { "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, "license": "ISC", "dependencies": { @@ -2988,8 +2333,6 @@ }, "node_modules/@npmcli/arborist/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -3004,8 +2347,6 @@ }, "node_modules/@npmcli/arborist/node_modules/minipass-fetch": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, "license": "MIT", "dependencies": { @@ -3022,8 +2363,6 @@ }, "node_modules/@npmcli/arborist/node_modules/minipass-sized": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, "license": "ISC", "dependencies": { @@ -3035,8 +2374,6 @@ }, "node_modules/@npmcli/arborist/node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -3048,8 +2385,6 @@ }, "node_modules/@npmcli/arborist/node_modules/minizlib": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "license": "MIT", "dependencies": { @@ -3062,8 +2397,6 @@ }, "node_modules/@npmcli/arborist/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -3075,8 +2408,6 @@ }, "node_modules/@npmcli/arborist/node_modules/negotiator": { "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "license": "MIT", "engines": { @@ -3085,8 +2416,6 @@ }, "node_modules/@npmcli/arborist/node_modules/node-gyp": { "version": "10.3.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", - "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3110,8 +2439,6 @@ }, "node_modules/@npmcli/arborist/node_modules/nopt": { "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, "license": "ISC", "dependencies": { @@ -3126,8 +2453,6 @@ }, "node_modules/@npmcli/arborist/node_modules/normalize-package-data": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3141,8 +2466,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-bundled": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3154,8 +2477,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-install-checks": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3167,8 +2488,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-normalize-package-bin": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", "dev": true, "license": "ISC", "engines": { @@ -3177,8 +2496,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-package-arg": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", - "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", "dev": true, "license": "ISC", "dependencies": { @@ -3193,8 +2510,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-packlist": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", - "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3206,8 +2521,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-pick-manifest": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", - "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", "dev": true, "license": "ISC", "dependencies": { @@ -3222,8 +2535,6 @@ }, "node_modules/@npmcli/arborist/node_modules/npm-registry-fetch": { "version": "17.1.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", - "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", "dev": true, "license": "ISC", "dependencies": { @@ -3242,8 +2553,6 @@ }, "node_modules/@npmcli/arborist/node_modules/p-map": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3258,8 +2567,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote": { "version": "20.0.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.1.tgz", - "integrity": "sha512-jTMLD/QK7JMUKg3g7K3M/DEqIbGm7sxclj12eQYIkL3viutSiefTs26IrqIqgGlFsviF/9dlDUZxnpGvkRXtjw==", "dev": true, "license": "ISC", "dependencies": { @@ -3290,8 +2597,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, "license": "ISC", "dependencies": { @@ -3307,8 +2612,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/fs": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "license": "ISC", "dependencies": { @@ -3320,8 +2623,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/git": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", - "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3340,8 +2641,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/installed-package-contents": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", - "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", "dev": true, "license": "ISC", "dependencies": { @@ -3357,8 +2656,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/node-gyp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", - "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", "dev": true, "license": "ISC", "engines": { @@ -3367,8 +2664,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/package-json": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", - "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", "dev": true, "license": "ISC", "dependencies": { @@ -3386,8 +2681,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/promise-spawn": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", - "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", "dev": true, "license": "ISC", "dependencies": { @@ -3399,8 +2692,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/redact": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", - "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", "dev": true, "license": "ISC", "engines": { @@ -3409,8 +2700,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/@npmcli/run-script": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", - "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", "dev": true, "license": "ISC", "dependencies": { @@ -3427,8 +2716,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/abbrev": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, "license": "ISC", "engines": { @@ -3437,8 +2724,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/cacache": { "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3461,8 +2746,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/hosted-git-info": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", "dev": true, "license": "ISC", "dependencies": { @@ -3474,8 +2757,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/ini": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", "dev": true, "license": "ISC", "engines": { @@ -3484,8 +2765,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/json-parse-even-better-errors": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, "license": "MIT", "engines": { @@ -3494,8 +2773,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/make-fetch-happen": { "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3517,8 +2794,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/minipass-fetch": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3535,8 +2810,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -3548,8 +2821,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -3558,8 +2829,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/node-gyp": { "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3583,8 +2852,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/nopt": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, "license": "ISC", "dependencies": { @@ -3599,8 +2866,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/npm-bundled": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", - "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", "dev": true, "license": "ISC", "dependencies": { @@ -3612,8 +2877,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/npm-install-checks": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", - "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3625,8 +2888,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/npm-normalize-package-bin": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, "license": "ISC", "engines": { @@ -3635,8 +2896,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/npm-package-arg": { "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", "dev": true, "license": "ISC", "dependencies": { @@ -3651,8 +2910,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/npm-pick-manifest": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", - "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3667,8 +2924,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/npm-registry-fetch": { "version": "18.0.2", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", - "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3687,8 +2942,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -3700,8 +2953,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/proc-log": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, "license": "ISC", "engines": { @@ -3710,8 +2961,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/ssri": { "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3723,8 +2972,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/unique-filename": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3736,8 +2983,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/validate-npm-package-name": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", - "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", "dev": true, "license": "ISC", "engines": { @@ -3746,8 +2991,6 @@ }, "node_modules/@npmcli/arborist/node_modules/pacote/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3762,8 +3005,6 @@ }, "node_modules/@npmcli/arborist/node_modules/parse-conflict-json": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", - "integrity": "sha512-01TvEktc68vwbJOtWZluyWeVGWjP+bZwXtPDMQVbBKzbJ/vZBif0L69KH1+cHv1SZ6e0FKLvjyHe8mqsIqYOmw==", "dev": true, "license": "ISC", "dependencies": { @@ -3775,10 +3016,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/arborist/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@npmcli/arborist/node_modules/postcss-selector-parser": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -3791,8 +3045,6 @@ }, "node_modules/@npmcli/arborist/node_modules/proc-log": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, "license": "ISC", "engines": { @@ -3801,8 +3053,6 @@ }, "node_modules/@npmcli/arborist/node_modules/proggy": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/proggy/-/proggy-2.0.0.tgz", - "integrity": "sha512-69agxLtnI8xBs9gUGqEnK26UfiexpHy+KUpBQWabiytQjnn5wFY8rklAi7GRfABIuPNnQ/ik48+LGLkYYJcy4A==", "dev": true, "license": "ISC", "engines": { @@ -3811,8 +3061,6 @@ }, "node_modules/@npmcli/arborist/node_modules/read-cmd-shim": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", - "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", "dev": true, "license": "ISC", "engines": { @@ -3821,8 +3069,6 @@ }, "node_modules/@npmcli/arborist/node_modules/sigstore": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", - "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3839,8 +3085,6 @@ }, "node_modules/@npmcli/arborist/node_modules/ssri": { "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3852,8 +3096,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", - "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", "dev": true, "license": "MIT", "dependencies": { @@ -3867,8 +3109,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/@npmcli/agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, "license": "ISC", "dependencies": { @@ -3884,8 +3124,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/@npmcli/fs": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "license": "ISC", "dependencies": { @@ -3897,8 +3135,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/cacache": { "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3921,8 +3157,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/make-fetch-happen": { "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3944,8 +3178,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/minipass-fetch": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3962,8 +3194,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -3975,8 +3205,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -3985,8 +3213,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -3998,8 +3224,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/proc-log": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, "license": "ISC", "engines": { @@ -4008,8 +3232,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/ssri": { "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4021,8 +3243,6 @@ }, "node_modules/@npmcli/arborist/node_modules/tuf-js/node_modules/unique-filename": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4034,8 +3254,6 @@ }, "node_modules/@npmcli/arborist/node_modules/unique-slug": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, "license": "ISC", "dependencies": { @@ -4047,8 +3265,6 @@ }, "node_modules/@npmcli/arborist/node_modules/validate-npm-package-name": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "dev": true, "license": "ISC", "engines": { @@ -4057,15 +3273,11 @@ }, "node_modules/@npmcli/arborist/node_modules/walk-up-path": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", - "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", "dev": true, "license": "ISC" }, "node_modules/@npmcli/arborist/node_modules/which": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "license": "ISC", "dependencies": { @@ -4080,8 +3292,6 @@ }, "node_modules/@npmcli/arborist/node_modules/write-file-atomic": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -4094,15 +3304,11 @@ }, "node_modules/@npmcli/arborist/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/@npmcli/config": { "version": "10.8.1", - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-10.8.1.tgz", - "integrity": "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ==", "license": "ISC", "dependencies": { "@npmcli/map-workspaces": "^5.0.0", @@ -4118,25 +3324,8 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/config/node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/fs": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", - "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "license": "ISC", "dependencies": { "semver": "^7.3.5" @@ -4147,8 +3336,6 @@ }, "node_modules/@npmcli/git": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", - "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", @@ -4165,9 +3352,7 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.5", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -4175,8 +3360,6 @@ }, "node_modules/@npmcli/installed-package-contents": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", - "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", "license": "ISC", "dependencies": { "npm-bundled": "^5.0.0", @@ -4191,8 +3374,6 @@ }, "node_modules/@npmcli/map-workspaces": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-5.0.3.tgz", - "integrity": "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==", "license": "ISC", "dependencies": { "@npmcli/name-from-folder": "^4.0.0", @@ -4204,40 +3385,11 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/map-workspaces/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/map-workspaces/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/map-workspaces/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "node_modules/@npmcli/map-workspaces/node_modules/minimatch": { + "version": "10.2.5", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -4248,8 +3400,6 @@ }, "node_modules/@npmcli/metavuln-calculator": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-9.0.3.tgz", - "integrity": "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==", "license": "ISC", "dependencies": { "cacache": "^20.0.0", @@ -4264,8 +3414,6 @@ }, "node_modules/@npmcli/name-from-folder": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-4.0.0.tgz", - "integrity": "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -4273,8 +3421,6 @@ }, "node_modules/@npmcli/node-gyp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", - "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -4282,8 +3428,6 @@ }, "node_modules/@npmcli/package-json": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", - "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", "license": "ISC", "dependencies": { "@npmcli/git": "^7.0.0", @@ -4294,56 +3438,12 @@ "semver": "^7.5.3", "spdx-expression-parse": "^4.0.0" }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/package-json/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", "license": "ISC", "dependencies": { "which": "^6.0.0" @@ -4354,8 +3454,6 @@ }, "node_modules/@npmcli/query": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-5.0.0.tgz", - "integrity": "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==", "license": "ISC", "dependencies": { "postcss-selector-parser": "^7.0.0" @@ -4366,8 +3464,6 @@ }, "node_modules/@npmcli/redact": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", - "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -4375,8 +3471,6 @@ }, "node_modules/@npmcli/run-script": { "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", - "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^5.0.0", @@ -4391,8 +3485,6 @@ }, "node_modules/@one-ini/wasm": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", "dev": true, "license": "MIT" }, @@ -4426,8 +3518,6 @@ }, "node_modules/@oxc-resolver/binding-darwin-arm64": { "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", - "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", "cpu": [ "arm64" ], @@ -4681,8 +3771,6 @@ }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "dev": true, "license": "MIT", "dependencies": { @@ -4691,8 +3779,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, "engines": { @@ -4701,8 +3787,6 @@ }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "license": "MIT", "engines": { "node": ">=12.22.0" @@ -4710,8 +3794,6 @@ }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" @@ -4722,14 +3804,10 @@ }, "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "license": "ISC" }, "node_modules/@pnpm/npm-conf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", - "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", @@ -4739,366 +3817,46 @@ "engines": { "node": ">=12" } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", - "cpu": [ - "x64" - ], + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ] }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "dev": true, "license": "MIT" }, "node_modules/@shikijs/core": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", - "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", "license": "MIT", "dependencies": { "@shikijs/engine-javascript": "2.5.0", @@ -5111,8 +3869,6 @@ }, "node_modules/@shikijs/engine-javascript": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", - "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0", @@ -5122,8 +3878,6 @@ }, "node_modules/@shikijs/engine-oniguruma": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", - "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0", @@ -5132,8 +3886,6 @@ }, "node_modules/@shikijs/langs": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", - "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0" @@ -5141,8 +3893,6 @@ }, "node_modules/@shikijs/themes": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", - "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0" @@ -5150,8 +3900,6 @@ }, "node_modules/@shikijs/transformers": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", - "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", "license": "MIT", "dependencies": { "@shikijs/core": "2.5.0", @@ -5160,8 +3908,6 @@ }, "node_modules/@shikijs/types": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", - "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -5170,14 +3916,10 @@ }, "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, "node_modules/@sigstore/bundle": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", - "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" @@ -5188,17 +3930,13 @@ }, "node_modules/@sigstore/core": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz", - "integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==", "license": "Apache-2.0", "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", - "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "version": "0.5.1", "license": "Apache-2.0", "engines": { "node": "^18.17.0 || >=20.5.0" @@ -5206,8 +3944,6 @@ }, "node_modules/@sigstore/sign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", - "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", "license": "Apache-2.0", "dependencies": { "@gar/promise-retry": "^1.0.2", @@ -5223,8 +3959,6 @@ }, "node_modules/@sigstore/tuf": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", - "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", @@ -5236,8 +3970,6 @@ }, "node_modules/@sigstore/verify": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", - "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", @@ -5250,8 +3982,6 @@ }, "node_modules/@simple-libs/child-process-utils": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", "dev": true, "license": "MIT", "dependencies": { @@ -5266,8 +3996,6 @@ }, "node_modules/@simple-libs/stream-utils": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", "dev": true, "license": "MIT", "engines": { @@ -5279,8 +4007,6 @@ }, "node_modules/@sindresorhus/base62": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", - "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", "dev": true, "license": "MIT", "engines": { @@ -5292,8 +4018,6 @@ }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "license": "MIT", "engines": { "node": ">=18" @@ -5304,8 +4028,6 @@ }, "node_modules/@sinonjs/commons": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5313,9 +4035,7 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", - "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", + "version": "15.3.2", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5323,9 +4043,7 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", - "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", + "version": "10.0.2", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5335,8 +4053,6 @@ }, "node_modules/@sinonjs/samsam/node_modules/type-detect": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "license": "MIT", "engines": { @@ -5345,8 +4061,6 @@ }, "node_modules/@tailwindcss/node": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -5360,8 +4074,6 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "license": "MIT", "engines": { "node": ">= 20" @@ -5381,26 +4093,8 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, "node_modules/@tailwindcss/oxide-darwin-arm64": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -5413,183 +4107,8 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, "node_modules/@tailwindcss/vite": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", - "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", "license": "MIT", "dependencies": { "@tailwindcss/node": "4.2.2", @@ -5602,15 +4121,11 @@ }, "node_modules/@tokenizer/token": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "dev": true, "license": "MIT" }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" @@ -5618,8 +4133,6 @@ }, "node_modules/@tufjs/models": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", - "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", @@ -5629,10 +4142,22 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.2.5", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5641,14 +4166,10 @@ }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -5656,21 +4177,15 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/linkify-it": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", "dependencies": { "@types/linkify-it": "^5", @@ -5679,8 +4194,6 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -5688,41 +4201,29 @@ }, "node_modules/@types/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.6.0", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", "license": "MIT" }, "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "version": "8.58.2", "dev": true, "license": "MIT", "engines": { @@ -5771,41 +4272,101 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@vercel/nft": { - "version": "0.29.4", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.4.tgz", - "integrity": "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==", + "node_modules/@vercel/nft": { + "version": "0.29.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^10.4.5", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/nft/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/nft/node_modules/brace-expansion": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vercel/nft/node_modules/glob": { + "version": "10.5.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vercel/nft/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/@vercel/nft/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vercel/nft/node_modules/path-scurry": { + "version": "1.11.1", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@mapbox/node-pre-gyp": "^2.0.0", - "@rollup/pluginutils": "^5.1.3", - "acorn": "^8.6.0", - "acorn-import-attributes": "^1.9.5", - "async-sema": "^3.1.1", - "bindings": "^1.4.0", - "estree-walker": "2.0.2", - "glob": "^10.4.5", - "graceful-fs": "^4.2.9", - "node-gyp-build": "^4.2.2", - "picomatch": "^4.0.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "nft": "out/cli.js" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -5817,8 +4378,6 @@ }, "node_modules/@vue/compiler-core": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", - "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", @@ -5830,8 +4389,6 @@ }, "node_modules/@vue/compiler-core/node_modules/entities": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5842,8 +4399,6 @@ }, "node_modules/@vue/compiler-dom": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", - "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "license": "MIT", "dependencies": { "@vue/compiler-core": "3.5.32", @@ -5852,8 +4407,6 @@ }, "node_modules/@vue/compiler-sfc": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", - "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", @@ -5869,8 +4422,6 @@ }, "node_modules/@vue/compiler-ssr": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", - "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.32", @@ -5879,8 +4430,6 @@ }, "node_modules/@vue/devtools-api": { "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", - "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", "license": "MIT", "dependencies": { "@vue/devtools-kit": "^7.7.9" @@ -5888,8 +4437,6 @@ }, "node_modules/@vue/devtools-kit": { "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", - "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", "license": "MIT", "dependencies": { "@vue/devtools-shared": "^7.7.9", @@ -5903,8 +4450,6 @@ }, "node_modules/@vue/devtools-shared": { "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", - "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", "license": "MIT", "dependencies": { "rfdc": "^1.4.1" @@ -5912,8 +4457,6 @@ }, "node_modules/@vue/reactivity": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", - "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "license": "MIT", "dependencies": { "@vue/shared": "3.5.32" @@ -5921,8 +4464,6 @@ }, "node_modules/@vue/runtime-core": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", - "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "license": "MIT", "dependencies": { "@vue/reactivity": "3.5.32", @@ -5931,8 +4472,6 @@ }, "node_modules/@vue/runtime-dom": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", - "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "license": "MIT", "dependencies": { "@vue/reactivity": "3.5.32", @@ -5943,8 +4482,6 @@ }, "node_modules/@vue/server-renderer": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", - "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "license": "MIT", "dependencies": { "@vue/compiler-ssr": "3.5.32", @@ -5956,14 +4493,10 @@ }, "node_modules/@vue/shared": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", - "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, "node_modules/@vueuse/core": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", - "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", @@ -5977,8 +4510,6 @@ }, "node_modules/@vueuse/integrations": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", - "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", "license": "MIT", "dependencies": { "@vueuse/core": "12.8.2", @@ -6043,8 +4574,6 @@ }, "node_modules/@vueuse/metadata": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", - "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -6052,8 +4581,6 @@ }, "node_modules/@vueuse/shared": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", - "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", "license": "MIT", "dependencies": { "vue": "^3.5.13" @@ -6064,8 +4591,6 @@ }, "node_modules/abbrev": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -6073,8 +4598,6 @@ }, "node_modules/abort-controller": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, "license": "MIT", "dependencies": { @@ -6086,8 +4609,6 @@ }, "node_modules/accepts": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -6099,8 +4620,6 @@ }, "node_modules/accepts/node_modules/negotiator": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6108,8 +4627,6 @@ }, "node_modules/acorn": { "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6120,8 +4637,6 @@ }, "node_modules/acorn-import-attributes": { "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6130,8 +4645,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -6139,8 +4652,6 @@ }, "node_modules/acorn-walk": { "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { @@ -6152,8 +4663,6 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -6161,8 +4670,6 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "license": "MIT", "dependencies": { @@ -6175,8 +4682,6 @@ }, "node_modules/aggregate-error/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -6185,8 +4690,6 @@ }, "node_modules/ajv": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6201,33 +4704,29 @@ }, "node_modules/ajv-errors": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", "license": "MIT", "peerDependencies": { "ajv": "^8.0.1" } }, "node_modules/algoliasearch": { - "version": "5.50.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.1.tgz", - "integrity": "sha512-/bwdue1/8LWELn/DBalGRfuLsXBLXULJo/yOeavJtDu8rBwxIzC6/Rz9Jg19S21VkJvRuZO1k8CZXBMS73mYbA==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.16.1", - "@algolia/client-abtesting": "5.50.1", - "@algolia/client-analytics": "5.50.1", - "@algolia/client-common": "5.50.1", - "@algolia/client-insights": "5.50.1", - "@algolia/client-personalization": "5.50.1", - "@algolia/client-query-suggestions": "5.50.1", - "@algolia/client-search": "5.50.1", - "@algolia/ingestion": "1.50.1", - "@algolia/monitoring": "1.50.1", - "@algolia/recommend": "5.50.1", - "@algolia/requester-browser-xhr": "5.50.1", - "@algolia/requester-fetch": "5.50.1", - "@algolia/requester-node-http": "5.50.1" + "version": "5.50.2", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.16.2", + "@algolia/client-abtesting": "5.50.2", + "@algolia/client-analytics": "5.50.2", + "@algolia/client-common": "5.50.2", + "@algolia/client-insights": "5.50.2", + "@algolia/client-personalization": "5.50.2", + "@algolia/client-query-suggestions": "5.50.2", + "@algolia/client-search": "5.50.2", + "@algolia/ingestion": "1.50.2", + "@algolia/monitoring": "1.50.2", + "@algolia/recommend": "5.50.2", + "@algolia/requester-browser-xhr": "5.50.2", + "@algolia/requester-fetch": "5.50.2", + "@algolia/requester-node-http": "5.50.2" }, "engines": { "node": ">= 14.0.0" @@ -6235,67 +4734,13 @@ }, "node_modules/ansi-align": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "license": "ISC", "dependencies": { "string-width": "^4.1.0" } }, - "node_modules/ansi-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -6306,8 +4751,6 @@ }, "node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -6316,10 +4759,29 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/append-transform": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", "dev": true, "license": "MIT", "dependencies": { @@ -6331,15 +4793,11 @@ }, "node_modules/archy": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true, "license": "MIT" }, "node_modules/are-docs-informative": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, "license": "MIT", "engines": { @@ -6348,14 +4806,10 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -6371,8 +4825,6 @@ }, "node_modules/array-find-index": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "dev": true, "license": "MIT", "engines": { @@ -6381,21 +4833,15 @@ }, "node_modules/array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, "node_modules/array-ify": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", "dev": true, "license": "MIT" }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6416,8 +4862,6 @@ }, "node_modules/arrgv": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz", - "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==", "dev": true, "license": "MIT", "engines": { @@ -6426,8 +4870,6 @@ }, "node_modules/arrify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz", - "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", "dev": true, "license": "MIT", "engines": { @@ -6439,15 +4881,11 @@ }, "node_modules/asap": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true, "license": "MIT" }, "node_modules/async": { "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "license": "MIT", "dependencies": { "lodash": "^4.17.14" @@ -6455,32 +4893,31 @@ }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/async-sema": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", - "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", "dev": true, "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/atomically": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", - "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", "license": "MIT", "dependencies": { "stubborn-fs": "^2.0.0", @@ -6488,9 +4925,7 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.27", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", - "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "version": "10.5.0", "funding": [ { "type": "opencollective", @@ -6507,8 +4942,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001774", + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -6525,8 +4960,6 @@ }, "node_modules/ava": { "version": "6.4.1", - "resolved": "https://registry.npmjs.org/ava/-/ava-6.4.1.tgz", - "integrity": "sha512-vxmPbi1gZx9zhAjHBgw81w/iEDKcrokeRk/fqDTyA2DQygZ0o+dUGRHFOtX8RA5N0heGJTTsIk7+xYxitDb61Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6586,10 +5019,19 @@ } } }, + "node_modules/ava/node_modules/chalk": { + "version": "5.6.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6604,8 +5046,6 @@ }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6621,8 +5061,6 @@ }, "node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -6630,8 +5068,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -6650,9 +5086,7 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", - "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "version": "2.10.19", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -6663,8 +5097,6 @@ }, "node_modules/bin-links": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz", - "integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==", "license": "ISC", "dependencies": { "cmd-shim": "^8.0.0", @@ -6679,8 +5111,6 @@ }, "node_modules/bin-links/node_modules/write-file-atomic": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", - "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", "license": "ISC", "dependencies": { "signal-exit": "^4.0.1" @@ -6689,10 +5119,18 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bindings": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6701,8 +5139,6 @@ }, "node_modules/birpc": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", - "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -6710,21 +5146,15 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "license": "MIT" }, "node_modules/blueimp-md5": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", "dev": true, "license": "MIT" }, "node_modules/body-parser": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -6747,14 +5177,10 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, "node_modules/boxen": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", - "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", @@ -6775,8 +5201,6 @@ }, "node_modules/boxen/node_modules/camelcase": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", "license": "MIT", "engines": { "node": ">=16" @@ -6785,39 +5209,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boxen/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "license": "MIT", "engines": { - "node": ">=16" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/boxen/node_modules/emoji-regex": { + "version": "10.6.0", + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" }, "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "4.41.0", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/brace-expansion": { "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -6828,8 +5260,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -6840,8 +5270,6 @@ }, "node_modules/browserslist": { "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -6873,8 +5301,6 @@ }, "node_modules/buffer": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, "funding": [ { @@ -6898,14 +5324,10 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, "node_modules/bundle-name": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -6919,8 +5341,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6928,8 +5348,6 @@ }, "node_modules/cacache": { "version": "20.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", - "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", "license": "ISC", "dependencies": { "@npmcli/fs": "^5.0.0", @@ -6947,52 +5365,15 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/cacache/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.5", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/cacache/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/caching-transform": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", "dev": true, "license": "MIT", "dependencies": { @@ -7007,8 +5388,6 @@ }, "node_modules/caching-transform/node_modules/make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "license": "MIT", "dependencies": { @@ -7023,8 +5402,6 @@ }, "node_modules/caching-transform/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -7033,15 +5410,11 @@ }, "node_modules/caching-transform/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/caching-transform/node_modules/write-file-atomic": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "license": "ISC", "dependencies": { @@ -7052,15 +5425,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -7072,8 +5443,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7085,8 +5454,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7101,8 +5468,6 @@ }, "node_modules/callsites": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", - "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", "dev": true, "license": "MIT", "engines": { @@ -7114,8 +5479,6 @@ }, "node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { @@ -7124,8 +5487,6 @@ }, "node_modules/caniuse-api": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", "license": "MIT", "dependencies": { "browserslist": "^4.0.0", @@ -7135,9 +5496,7 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001784", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", - "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "version": "1.0.30001788", "funding": [ { "type": "opencollective", @@ -7156,8 +5515,6 @@ }, "node_modules/catharsis": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", "license": "MIT", "dependencies": { "lodash": "^4.17.15" @@ -7168,8 +5525,6 @@ }, "node_modules/cbor": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", - "integrity": "sha512-exQDevYd7ZQLP4moMQcZkKCVZsXLAtUSflObr3xTh4xzFIv/xBCdvCd6L259kQOUP2kcTC0jvC6PpZIf/WmRXA==", "dev": true, "license": "MIT", "dependencies": { @@ -7181,8 +5536,6 @@ }, "node_modules/ccount": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", "funding": { "type": "github", @@ -7190,21 +5543,36 @@ } }, "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "4.1.2", + "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", "funding": { "type": "github", @@ -7213,8 +5581,6 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", "funding": { "type": "github", @@ -7223,8 +5589,6 @@ }, "node_modules/check-engine-light": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/check-engine-light/-/check-engine-light-0.4.0.tgz", - "integrity": "sha512-hZzy5cbJg52nDGgyiNDVpjzrIo6V49lpFVJ7hJiGpbWs9in5mtpRMqdM1VPptab2QTkwYdac98PaXSBmQqh1Tg==", "dev": true, "license": "ISC", "dependencies": { @@ -7243,8 +5607,6 @@ }, "node_modules/cheerio": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", @@ -7268,8 +5630,6 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -7283,10 +5643,40 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -7294,15 +5684,11 @@ }, "node_modules/chunkd": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz", - "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "funding": [ { "type": "github", @@ -7316,15 +5702,11 @@ }, "node_modules/ci-parallel-vars": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz", - "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==", "dev": true, "license": "MIT" }, "node_modules/clean-stack": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, "license": "MIT", "engines": { @@ -7333,8 +5715,6 @@ }, "node_modules/cli-boxes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "license": "MIT", "engines": { "node": ">=10" @@ -7343,72 +5723,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cli-progress/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-progress/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cli-progress/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-progress/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-progress/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/cli-progress": { + "version": "3.12.0", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "string-width": "^4.2.3" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/cli-truncate": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, "license": "MIT", "dependencies": { @@ -7422,10 +5748,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -7439,8 +5784,6 @@ }, "node_modules/cliui/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -7449,8 +5792,6 @@ }, "node_modules/cliui/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -7463,42 +5804,8 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -7510,8 +5817,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7528,8 +5833,6 @@ }, "node_modules/clone": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "license": "MIT", "engines": { "node": ">=0.8" @@ -7537,8 +5840,6 @@ }, "node_modules/cmd-shim": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz", - "integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -7546,8 +5847,6 @@ }, "node_modules/code-excerpt": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", "dev": true, "license": "MIT", "dependencies": { @@ -7559,8 +5858,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -7571,14 +5868,10 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -7590,8 +5883,6 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", "funding": { "type": "github", @@ -7600,14 +5891,10 @@ }, "node_modules/command-exists": { "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", "license": "MIT" }, "node_modules/commander": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "license": "MIT", "engines": { @@ -7616,8 +5903,6 @@ }, "node_modules/comment-parser": { "version": "1.4.6", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", - "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", "dev": true, "license": "MIT", "engines": { @@ -7626,8 +5911,6 @@ }, "node_modules/common-ancestor-path": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", - "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", "license": "BlueOak-1.0.0", "engines": { "node": ">= 18" @@ -7635,22 +5918,16 @@ }, "node_modules/common-path-prefix": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true, "license": "ISC" }, "node_modules/commondir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true, "license": "MIT" }, "node_modules/compare-func": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", "dev": true, "license": "MIT", "dependencies": { @@ -7660,8 +5937,6 @@ }, "node_modules/component-emitter": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true, "license": "MIT", "funding": { @@ -7670,8 +5945,6 @@ }, "node_modules/compressible": { "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" @@ -7682,8 +5955,6 @@ }, "node_modules/compression": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -7700,8 +5971,6 @@ }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -7709,14 +5978,10 @@ }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/compression/node_modules/negotiator": { "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7724,15 +5989,11 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/concordance": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", - "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", "dev": true, "license": "ISC", "dependencies": { @@ -7751,8 +6012,6 @@ }, "node_modules/config-chain": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -7761,14 +6020,10 @@ }, "node_modules/config-chain/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/configstore": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", - "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", "license": "BSD-2-Clause", "dependencies": { "atomically": "^2.0.3", @@ -7785,8 +6040,6 @@ }, "node_modules/configstore/node_modules/dot-prop": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", "license": "MIT", "dependencies": { "type-fest": "^4.18.2" @@ -7800,8 +6053,6 @@ }, "node_modules/configstore/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -7812,8 +6063,6 @@ }, "node_modules/consola": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, "license": "MIT", "engines": { @@ -7822,8 +6071,6 @@ }, "node_modules/content-disposition": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -7834,8 +6081,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7843,8 +6088,6 @@ }, "node_modules/conventional-changelog-angular": { "version": "8.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", - "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", "dev": true, "license": "ISC", "dependencies": { @@ -7856,8 +6099,6 @@ }, "node_modules/conventional-changelog-conventionalcommits": { "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", "dev": true, "license": "ISC", "dependencies": { @@ -7869,8 +6110,6 @@ }, "node_modules/conventional-commits-parser": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", - "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", "dev": true, "license": "MIT", "dependencies": { @@ -7886,15 +6125,11 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/convert-to-spaces": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "dev": true, "license": "MIT", "engines": { @@ -7903,8 +6138,6 @@ }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7912,21 +6145,15 @@ }, "node_modules/cookie-signature": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, "node_modules/cookiejar": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true, "license": "MIT" }, "node_modules/copy-anything": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", - "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "license": "MIT", "dependencies": { "is-what": "^5.2.0" @@ -7940,14 +6167,10 @@ }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/correct-license-metadata": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/correct-license-metadata/-/correct-license-metadata-1.5.0.tgz", - "integrity": "sha512-fVBH+P7EJvvzqQ1Jn7xrdAD7tKFrjeBDNawOgNELcSopCL70Ie8H9Cyn1nYO0E7jihunnpqjWdpEQinDhhKrzw==", "dev": true, "license": "MIT", "dependencies": { @@ -7956,8 +6179,6 @@ }, "node_modules/cors": { "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -7973,8 +6194,6 @@ }, "node_modules/cosmiconfig": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7999,13 +6218,11 @@ } }, "node_modules/cosmiconfig-typescript-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", - "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "version": "6.3.0", "dev": true, "license": "MIT", "dependencies": { - "jiti": "^2.6.1" + "jiti": "2.6.1" }, "engines": { "node": ">=v18" @@ -8018,8 +6235,6 @@ }, "node_modules/cross-env": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", "dependencies": { @@ -8036,8 +6251,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8050,14 +6263,10 @@ }, "node_modules/cross-spawn/node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -8071,8 +6280,6 @@ }, "node_modules/crypto-random-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", "dev": true, "license": "MIT", "dependencies": { @@ -8087,8 +6294,6 @@ }, "node_modules/crypto-random-string/node_modules/type-fest": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8099,9 +6304,7 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz", - "integrity": "sha512-gz6x+KkgNCjxq3Var03pRYLhyNfwhkKF1g/yoLgDNtFvVu0/fOLV9C8fFEZRjACp/XQLumjAYo7JVjzH3wLbxA==", + "version": "7.4.0", "license": "ISC", "engines": { "node": "^14 || ^16 || >=18" @@ -8112,8 +6315,6 @@ }, "node_modules/css-select": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -8128,8 +6329,6 @@ }, "node_modules/css-tree": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "license": "MIT", "dependencies": { "mdn-data": "2.27.1", @@ -8141,8 +6340,6 @@ }, "node_modules/css-what": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -8153,8 +6350,6 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -8164,12 +6359,10 @@ } }, "node_modules/cssnano": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.4.tgz", - "integrity": "sha512-T9PNS7y+5Nc9Qmu9mRONqfxG1RVY7Vuvky0XN6MZ+9hqplesTEwnj9r0ROtVuSwUVfaDhVlavuzWIVLUgm4hkQ==", + "version": "7.1.5", "license": "MIT", "dependencies": { - "cssnano-preset-default": "^7.0.12", + "cssnano-preset-default": "^7.0.13", "lilconfig": "^3.1.3" }, "engines": { @@ -8184,26 +6377,24 @@ } }, "node_modules/cssnano-preset-default": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.12.tgz", - "integrity": "sha512-B3Eoouzw/sl2zANI0AL9KbacummJTCww+fkHaDBMZad/xuVx8bUduPLly6hKVQAlrmvYkS1jB1CVQEKm3gn0AA==", + "version": "7.0.13", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.1", "postcss-calc": "^10.1.1", - "postcss-colormin": "^7.0.7", - "postcss-convert-values": "^7.0.9", + "postcss-colormin": "^7.0.8", + "postcss-convert-values": "^7.0.10", "postcss-discard-comments": "^7.0.6", "postcss-discard-duplicates": "^7.0.2", "postcss-discard-empty": "^7.0.1", "postcss-discard-overridden": "^7.0.1", "postcss-merge-longhand": "^7.0.5", - "postcss-merge-rules": "^7.0.8", + "postcss-merge-rules": "^7.0.9", "postcss-minify-font-values": "^7.0.1", - "postcss-minify-gradients": "^7.0.2", - "postcss-minify-params": "^7.0.6", + "postcss-minify-gradients": "^7.0.3", + "postcss-minify-params": "^7.0.7", "postcss-minify-selectors": "^7.0.6", "postcss-normalize-charset": "^7.0.1", "postcss-normalize-display-values": "^7.0.1", @@ -8211,11 +6402,11 @@ "postcss-normalize-repeat-style": "^7.0.1", "postcss-normalize-string": "^7.0.1", "postcss-normalize-timing-functions": "^7.0.1", - "postcss-normalize-unicode": "^7.0.6", + "postcss-normalize-unicode": "^7.0.7", "postcss-normalize-url": "^7.0.1", "postcss-normalize-whitespace": "^7.0.1", "postcss-ordered-values": "^7.0.2", - "postcss-reduce-initial": "^7.0.6", + "postcss-reduce-initial": "^7.0.7", "postcss-reduce-transforms": "^7.0.1", "postcss-svgo": "^7.1.1", "postcss-unique-selectors": "^7.0.5" @@ -8229,8 +6420,6 @@ }, "node_modules/cssnano-utils": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", - "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -8241,8 +6430,6 @@ }, "node_modules/csso": { "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "license": "MIT", "dependencies": { "css-tree": "~2.2.0" @@ -8254,8 +6441,6 @@ }, "node_modules/csso/node_modules/css-tree": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "license": "MIT", "dependencies": { "mdn-data": "2.0.28", @@ -8268,20 +6453,14 @@ }, "node_modules/csso/node_modules/mdn-data": { "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/currently-unhandled": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", "dev": true, "license": "MIT", "dependencies": { @@ -8293,8 +6472,6 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8311,8 +6488,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8329,8 +6504,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8347,8 +6520,6 @@ }, "node_modules/data-with-position": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/data-with-position/-/data-with-position-0.5.0.tgz", - "integrity": "sha512-GhsgEIPWk7WCAisjwBkOjvPqpAlVUOSl1CTmy9KyhVMG1wxl29Zj5+J71WhQ/KgoJS/Psxq6Cnioz3xdBjeIWQ==", "license": "BSD-3-Clause", "dependencies": { "yaml-ast-parser": "^0.0.43" @@ -8356,8 +6527,6 @@ }, "node_modules/date-time": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", - "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", "dev": true, "license": "MIT", "dependencies": { @@ -8369,8 +6538,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8386,8 +6553,6 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "license": "MIT", "engines": { @@ -8396,8 +6561,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -8405,15 +6568,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/default-browser": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -8428,8 +6587,6 @@ }, "node_modules/default-browser-id": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "license": "MIT", "engines": { "node": ">=18" @@ -8440,8 +6597,6 @@ }, "node_modules/default-require-extensions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", "dev": true, "license": "MIT", "dependencies": { @@ -8456,8 +6611,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { @@ -8474,8 +6627,6 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "license": "MIT", "engines": { "node": ">=12" @@ -8486,8 +6637,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -8504,8 +6653,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -8514,8 +6661,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -8523,8 +6668,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -8532,8 +6675,6 @@ }, "node_modules/destroy": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", "engines": { "node": ">= 0.8", @@ -8542,8 +6683,6 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8551,14 +6690,10 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, "node_modules/devcert-sanscache": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/devcert-sanscache/-/devcert-sanscache-0.5.1.tgz", - "integrity": "sha512-9ePmMvWItstun0c35V5WXUlNU4MCHtpXWxKUJcDiZvyKkcA3FxkL6PFHKqTd446mXMmvLpOGBxVD6GjBXeMA5A==", "license": "MIT", "dependencies": { "command-exists": "^1.2.9", @@ -8570,10 +6705,68 @@ "node": "^14.13.1 || >=16.0.0" } }, + "node_modules/devcert-sanscache/node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/devcert-sanscache/node_modules/brace-expansion": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/devcert-sanscache/node_modules/glob": { + "version": "10.5.0", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/devcert-sanscache/node_modules/lru-cache": { + "version": "10.4.3", + "license": "ISC" + }, + "node_modules/devcert-sanscache/node_modules/minimatch": { + "version": "9.0.9", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/devcert-sanscache/node_modules/path-scurry": { + "version": "1.11.1", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/devcert-sanscache/node_modules/rimraf": { "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "license": "ISC", "dependencies": { "glob": "^10.3.7" @@ -8587,8 +6780,6 @@ }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -8600,8 +6791,6 @@ }, "node_modules/dezalgo": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, "license": "ISC", "dependencies": { @@ -8611,8 +6800,6 @@ }, "node_modules/diff": { "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -8621,8 +6808,6 @@ }, "node_modules/docopt": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/docopt/-/docopt-0.6.2.tgz", - "integrity": "sha512-NqTbaYeE4gA/wU1hdKFdU+AFahpDOpgGLzHP42k6H6DKExJd0A55KEVWYhL9FEmHmgeLvEU2vuKXDuU+4yToOw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8630,8 +6815,6 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -8644,8 +6827,6 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", @@ -8656,8 +6837,6 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -8671,8 +6850,6 @@ }, "node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -8685,8 +6862,6 @@ }, "node_modules/dot-prop": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8698,8 +6873,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -8712,14 +6885,10 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/editorconfig": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", - "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", "dev": true, "license": "MIT", "dependencies": { @@ -8737,15 +6906,11 @@ }, "node_modules/editorconfig/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/editorconfig/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", "dev": true, "license": "MIT", "dependencies": { @@ -8754,8 +6919,6 @@ }, "node_modules/editorconfig/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -8770,20 +6933,14 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.331", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", - "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "version": "1.5.339", "license": "ISC" }, "node_modules/emittery": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.2.1.tgz", - "integrity": "sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw==", "dev": true, "license": "MIT", "engines": { @@ -8794,21 +6951,15 @@ } }, "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "version": "8.0.0", "license": "MIT" }, "node_modules/emoji-regex-xs": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -8816,8 +6967,6 @@ }, "node_modules/encoding": { "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "license": "MIT", "optional": true, @@ -8827,8 +6976,6 @@ }, "node_modules/encoding-sniffer": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", @@ -8840,8 +6987,6 @@ }, "node_modules/encoding-sniffer/node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -8852,8 +6997,6 @@ }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "optional": true, @@ -8866,8 +7009,6 @@ }, "node_modules/enhance-visitors": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/enhance-visitors/-/enhance-visitors-1.0.0.tgz", - "integrity": "sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA==", "dev": true, "license": "MIT", "dependencies": { @@ -8879,8 +7020,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -8892,8 +7031,6 @@ }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -8904,8 +7041,6 @@ }, "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", "engines": { "node": ">=6" @@ -8913,15 +7048,11 @@ }, "node_modules/err-code": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8929,9 +7060,7 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", "dev": true, "license": "MIT", "dependencies": { @@ -8999,8 +7128,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -9008,8 +7135,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -9017,8 +7142,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -9029,8 +7152,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -9045,8 +7166,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -9063,15 +7182,11 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -9108,8 +7223,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -9117,8 +7230,6 @@ }, "node_modules/escape-goat": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "license": "MIT", "engines": { "node": ">=12" @@ -9129,17 +7240,14 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9147,8 +7255,6 @@ }, "node_modules/escape-unicode": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/escape-unicode/-/escape-unicode-0.3.0.tgz", - "integrity": "sha512-4Lr9Prysw8FBwpW8dURr4T3/VRU4RYlhayLgy34zavplBG9bUsTtaCuM7Lw3szWTuidQvkZ2a1qJxG3e5+o99w==", "funding": [ { "type": "individual", @@ -9163,8 +7269,6 @@ }, "node_modules/escope": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-4.0.0.tgz", - "integrity": "sha512-E36qlD/r6RJHVpPKArgMoMlNJzoRJFH8z/cAZlI9lbc45zB3+S7i9k6e/MNb+7bZQzNEa6r8WKN3BovpeIBwgA==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.1.0", @@ -9176,8 +7280,6 @@ }, "node_modules/escope/node_modules/estraverse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -9185,8 +7287,6 @@ }, "node_modules/eslint": { "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9245,8 +7345,6 @@ }, "node_modules/eslint-config-google": { "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", - "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9258,8 +7356,6 @@ }, "node_modules/eslint-plugin-ava": { "version": "15.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-ava/-/eslint-plugin-ava-15.1.0.tgz", - "integrity": "sha512-+6Zxk1uYW3mf7lxCLWIQsFYgn3hfuCMbsKc0MtqfloOz1F6fiV5/PaWEaLgkL1egrSQmnyR7vOFP1wSPJbVUbw==", "dev": true, "license": "MIT", "dependencies": { @@ -9281,8 +7377,6 @@ }, "node_modules/eslint-plugin-ava/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9294,8 +7388,6 @@ }, "node_modules/eslint-plugin-ava/node_modules/espree": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9312,8 +7404,6 @@ }, "node_modules/eslint-plugin-jsdoc": { "version": "62.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", - "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9339,278 +7429,105 @@ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-jsdoc/node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/eslint-scope": { + "version": "8.4.0", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "p-locate": "^5.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/eslint-utils": { + "version": "3.0.0", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "eslint-visitor-keys": "^2.0.0" }, "engines": { - "node": "*" + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, + "license": "Apache-2.0", "engines": { "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, "node_modules/esmock": { "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz", - "integrity": "sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==", "dev": true, "license": "ISC", "engines": { @@ -9619,8 +7536,6 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", @@ -9636,8 +7551,6 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", "bin": { @@ -9650,15 +7563,11 @@ }, "node_modules/espurify": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/espurify/-/espurify-2.1.1.tgz", - "integrity": "sha512-zttWvnkhcDyGOhSH4vO2qCBILpdCMv/MX8lp4cqgRkQoDRGK2oZxi2GfWhlP2dIXmk7BaKeOTuzbHhyC68o8XQ==", "dev": true, "license": "MIT" }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9670,8 +7579,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -9682,8 +7589,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -9691,14 +7596,10 @@ }, "node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -9707,8 +7608,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9716,8 +7615,6 @@ }, "node_modules/event-target-shim": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "dev": true, "license": "MIT", "engines": { @@ -9726,8 +7623,6 @@ }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", "engines": { @@ -9736,8 +7631,6 @@ }, "node_modules/execa": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", "dev": true, "license": "MIT", "dependencies": { @@ -9763,14 +7656,10 @@ }, "node_modules/exponential-backoff": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, "node_modules/express": { "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -9815,8 +7704,6 @@ }, "node_modules/express/node_modules/body-parser": { "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -9839,8 +7726,6 @@ }, "node_modules/express/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -9848,8 +7733,6 @@ }, "node_modules/express/node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -9860,8 +7743,6 @@ }, "node_modules/express/node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9869,14 +7750,10 @@ }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/express/node_modules/qs": { "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -9890,8 +7767,6 @@ }, "node_modules/express/node_modules/raw-body": { "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -9905,8 +7780,6 @@ }, "node_modules/express/node_modules/type-is": { "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -9918,21 +7791,15 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-diff": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, "license": "Apache-2.0" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -9947,8 +7814,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9959,29 +7824,21 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -9996,8 +7853,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -10005,8 +7860,6 @@ }, "node_modules/fd-package-json": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", - "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10015,8 +7868,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -10032,8 +7883,6 @@ }, "node_modules/figures": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" @@ -10047,8 +7896,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10060,8 +7907,6 @@ }, "node_modules/file-type": { "version": "18.7.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", - "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", "dev": true, "license": "MIT", "dependencies": { @@ -10078,15 +7923,11 @@ }, "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "dev": true, "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -10097,8 +7938,6 @@ }, "node_modules/finalhandler": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -10115,8 +7954,6 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -10124,14 +7961,10 @@ }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/find-cache-dir": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, "license": "MIT", "dependencies": { @@ -10146,10 +7979,31 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-cache-dir/node_modules/make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "license": "MIT", "dependencies": { @@ -10162,10 +8016,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-cache-dir/node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10177,8 +8054,6 @@ }, "node_modules/find-cache-dir/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -10186,22 +8061,22 @@ } }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/find-up-simple": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", "license": "MIT", "engines": { "node": ">=18" @@ -10212,8 +8087,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -10226,15 +8099,11 @@ }, "node_modules/flatted": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/focus-trap": { "version": "7.8.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", - "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "license": "MIT", "dependencies": { "tabbable": "^6.4.0" @@ -10242,8 +8111,6 @@ }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -10258,8 +8125,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -10274,8 +8139,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -10291,8 +8154,6 @@ }, "node_modules/formatly": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", - "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", "dev": true, "license": "MIT", "dependencies": { @@ -10307,8 +8168,6 @@ }, "node_modules/formidable": { "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, "license": "MIT", "dependencies": { @@ -10325,8 +8184,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -10334,8 +8191,6 @@ }, "node_modules/fraction.js": { "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "license": "MIT", "engines": { "node": "*" @@ -10347,8 +8202,6 @@ }, "node_modules/fresh": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -10356,8 +8209,6 @@ }, "node_modules/fromentries": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true, "funding": [ { @@ -10377,8 +8228,6 @@ }, "node_modules/fs-minipass": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" @@ -10389,16 +8238,11 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -10410,8 +8254,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10419,8 +8261,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10440,8 +8280,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -10450,8 +8288,6 @@ }, "node_modules/generator-function": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "dev": true, "license": "MIT", "engines": { @@ -10460,8 +8296,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { @@ -10470,8 +8304,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -10479,8 +8311,6 @@ }, "node_modules/get-east-asian-width": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "license": "MIT", "engines": { "node": ">=18" @@ -10491,8 +8321,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -10515,8 +8343,6 @@ }, "node_modules/get-package-type": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { @@ -10525,8 +8351,6 @@ }, "node_modules/get-port": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", - "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -10537,8 +8361,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -10550,8 +8372,6 @@ }, "node_modules/get-stdin": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", - "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", "dev": true, "license": "MIT", "engines": { @@ -10563,8 +8383,6 @@ }, "node_modules/get-stream": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, "license": "MIT", "dependencies": { @@ -10580,8 +8398,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -10598,8 +8414,6 @@ }, "node_modules/git-raw-commits": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10614,21 +8428,15 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "13.0.6", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10636,8 +8444,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -10647,31 +8453,14 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", + "version": "10.2.5", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10679,8 +8468,6 @@ }, "node_modules/global-directory": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", "license": "MIT", "dependencies": { "ini": "4.1.1" @@ -10694,17 +8481,13 @@ }, "node_modules/global-directory/node_modules/ini": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.5.0", "dev": true, "license": "MIT", "engines": { @@ -10716,8 +8499,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10733,8 +8514,6 @@ }, "node_modules/globby": { "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", @@ -10753,8 +8532,6 @@ }, "node_modules/globby/node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "license": "MIT", "engines": { "node": ">=18" @@ -10765,8 +8542,6 @@ }, "node_modules/globby/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { "node": ">= 4" @@ -10774,8 +8549,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10786,20 +8559,14 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/handle-thing": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "license": "MIT" }, "node_modules/handlebars": { "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10820,8 +8587,6 @@ }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -10833,8 +8598,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -10843,8 +8606,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { @@ -10856,8 +8617,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10872,8 +8631,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10884,8 +8641,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -10900,8 +8655,6 @@ }, "node_modules/hasha": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10917,8 +8670,6 @@ }, "node_modules/hasha/node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { @@ -10930,8 +8681,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10942,8 +8691,6 @@ }, "node_modules/hast-util-to-html": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -10965,8 +8712,6 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -10978,14 +8723,10 @@ }, "node_modules/hookable": { "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, "node_modules/hosted-git-info": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "license": "ISC", "dependencies": { "lru-cache": "^11.1.0" @@ -10995,9 +8736,7 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.5", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -11005,8 +8744,6 @@ }, "node_modules/hpack.js": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -11017,8 +8754,6 @@ }, "node_modules/hpack.js/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -11032,14 +8767,10 @@ }, "node_modules/hpack.js/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -11047,8 +8778,6 @@ }, "node_modules/html-entities": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "dev": true, "funding": [ { @@ -11064,15 +8793,11 @@ }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/html-void-elements": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "license": "MIT", "funding": { "type": "github", @@ -11081,8 +8806,6 @@ }, "node_modules/htmlparser2": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -11100,20 +8823,14 @@ }, "node_modules/http-cache-semantics": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", "license": "MIT" }, "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -11132,8 +8849,6 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -11145,8 +8860,6 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -11158,8 +8871,6 @@ }, "node_modules/human-signals": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -11168,8 +8879,6 @@ }, "node_modules/husky": { "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -11184,8 +8893,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11200,8 +8907,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -11221,8 +8926,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -11231,8 +8934,6 @@ }, "node_modules/ignore-by-default": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz", - "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==", "dev": true, "license": "ISC", "engines": { @@ -11241,8 +8942,6 @@ }, "node_modules/ignore-walk": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", - "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", "license": "ISC", "dependencies": { "minimatch": "^10.0.3" @@ -11251,10 +8950,21 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "10.2.5", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11270,8 +8980,6 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -11280,8 +8988,6 @@ }, "node_modules/import-local": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -11297,10 +9003,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/import-local/node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -11311,8 +9059,6 @@ }, "node_modules/import-meta-resolve": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", "dev": true, "license": "MIT", "funding": { @@ -11322,8 +9068,6 @@ }, "node_modules/import-modules": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.1.0.tgz", - "integrity": "sha512-8HEWcnkbGpovH9yInoisxaSoIg9Brbul+Ju3Kqe2UsYDUBJD/iQjSgEj0zPcTDPKfPp2fs5xlv1i+JSye/m1/A==", "dev": true, "license": "MIT", "engines": { @@ -11335,8 +9079,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -11345,8 +9087,6 @@ }, "node_modules/indent-string": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, "license": "MIT", "engines": { @@ -11358,8 +9098,6 @@ }, "node_modules/index-to-position": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "license": "MIT", "engines": { "node": ">=18" @@ -11370,9 +9108,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -11382,14 +9117,10 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -11397,8 +9128,6 @@ }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -11412,8 +9141,6 @@ }, "node_modules/ip-address": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -11421,8 +9148,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -11430,8 +9155,6 @@ }, "node_modules/irregular-plurals": { "version": "3.5.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", - "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", "dev": true, "license": "MIT", "engines": { @@ -11440,8 +9163,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -11458,15 +9179,11 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11485,8 +9202,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11499,10 +9214,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -11518,8 +9241,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -11531,8 +9252,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -11546,8 +9265,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -11564,8 +9281,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -11581,8 +9296,6 @@ }, "node_modules/is-docker": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", "bin": { "is-docker": "cli.js" @@ -11596,8 +9309,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11605,8 +9316,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -11621,8 +9330,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, "license": "MIT", "engines": { @@ -11634,8 +9341,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { @@ -11654,8 +9359,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -11666,8 +9369,6 @@ }, "node_modules/is-in-ci": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", - "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", "license": "MIT", "bin": { "is-in-ci": "cli.js" @@ -11681,8 +9382,6 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -11699,8 +9398,6 @@ }, "node_modules/is-installed-globally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", "license": "MIT", "dependencies": { "global-directory": "^4.0.1", @@ -11715,15 +9412,11 @@ }, "node_modules/is-lambda": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true, "license": "MIT" }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -11735,8 +9428,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -11748,8 +9439,6 @@ }, "node_modules/is-npm": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", - "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -11760,8 +9449,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -11769,8 +9456,6 @@ }, "node_modules/is-number-like": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", "license": "ISC", "dependencies": { "lodash.isfinite": "^3.3.2" @@ -11778,8 +9463,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -11795,8 +9478,6 @@ }, "node_modules/is-obj": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true, "license": "MIT", "engines": { @@ -11805,8 +9486,6 @@ }, "node_modules/is-path-inside": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "license": "MIT", "engines": { "node": ">=12" @@ -11817,8 +9496,6 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true, "license": "MIT", "engines": { @@ -11830,8 +9507,6 @@ }, "node_modules/is-plain-object": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, "license": "MIT", "engines": { @@ -11840,14 +9515,10 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -11865,8 +9536,6 @@ }, "node_modules/is-regexp": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "dev": true, "license": "MIT", "engines": { @@ -11875,8 +9544,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -11888,8 +9555,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -11904,8 +9569,6 @@ }, "node_modules/is-stream": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, "license": "MIT", "engines": { @@ -11917,8 +9580,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -11934,8 +9595,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -11952,8 +9611,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11968,15 +9625,11 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true, "license": "MIT" }, "node_modules/is-unicode-supported": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "license": "MIT", "engines": { "node": ">=18" @@ -11987,8 +9640,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -12000,8 +9651,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -12016,8 +9665,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12033,8 +9680,6 @@ }, "node_modules/is-what": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", - "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", "license": "MIT", "engines": { "node": ">=18" @@ -12045,8 +9690,6 @@ }, "node_modules/is-windows": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, "license": "MIT", "engines": { @@ -12055,8 +9698,6 @@ }, "node_modules/is-wsl": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -12070,14 +9711,10 @@ }, "node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/isexe": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "license": "BlueOak-1.0.0", "engines": { "node": ">=20" @@ -12085,8 +9722,6 @@ }, "node_modules/isobject": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", "dev": true, "license": "MIT", "dependencies": { @@ -12098,8 +9733,6 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12108,8 +9741,6 @@ }, "node_modules/istanbul-lib-hook": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12121,8 +9752,6 @@ }, "node_modules/istanbul-lib-instrument": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12138,8 +9767,6 @@ }, "node_modules/istanbul-lib-instrument/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -12148,45 +9775,22 @@ }, "node_modules/istanbul-lib-processinfo": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "license": "ISC", "dependencies": { "archy": "^1.0.0", "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/istanbul-lib-processinfo/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" } }, "node_modules/istanbul-lib-processinfo/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -12204,23 +9808,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/istanbul-lib-processinfo/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/istanbul-lib-processinfo/node_modules/p-map": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12232,9 +9821,6 @@ }, "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -12249,8 +9835,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12264,8 +9848,6 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12279,8 +9861,6 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12293,8 +9873,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -12308,8 +9886,6 @@ }, "node_modules/jiti": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -12317,8 +9893,6 @@ }, "node_modules/js-beautify": { "version": "1.15.4", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", - "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", "dev": true, "license": "MIT", "dependencies": { @@ -12339,18 +9913,65 @@ }, "node_modules/js-beautify/node_modules/abbrev": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", "dev": true, "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/js-beautify/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-beautify/node_modules/nopt": { "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, "license": "ISC", "dependencies": { @@ -12363,10 +9984,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/js-beautify/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-cookie": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", "dev": true, "license": "MIT", "engines": { @@ -12375,8 +10009,6 @@ }, "node_modules/js-string-escape": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", "dev": true, "license": "MIT", "engines": { @@ -12385,14 +10017,10 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -12403,8 +10031,6 @@ }, "node_modules/js2xmlparser": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", "license": "Apache-2.0", "dependencies": { "xmlcreate": "^2.0.4" @@ -12412,8 +10038,6 @@ }, "node_modules/jsdoc": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", - "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "license": "Apache-2.0", "dependencies": { "@babel/parser": "^7.20.15", @@ -12441,8 +10065,6 @@ }, "node_modules/jsdoc-type-pratt-parser": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", - "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", "dev": true, "license": "MIT", "engines": { @@ -12451,8 +10073,6 @@ }, "node_modules/jsdoc/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "license": "MIT", "engines": { "node": ">=8" @@ -12460,8 +10080,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -12473,15 +10091,11 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", - "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", "license": "MIT", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -12489,21 +10103,15 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-nice": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", - "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", "license": "ISC", "funding": { "url": "https://github.com/sponsors/isaacs" @@ -12511,8 +10119,6 @@ }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -12524,8 +10130,6 @@ }, "node_modules/jsonparse": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "engines": [ "node >= 0.2.0" ], @@ -12533,20 +10137,14 @@ }, "node_modules/just-diff": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", - "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", "license": "MIT" }, "node_modules/just-diff-apply": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", - "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -12555,8 +10153,6 @@ }, "node_modules/klaw": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", "license": "MIT", "dependencies": { "graceful-fs": "^4.1.9" @@ -12564,8 +10160,6 @@ }, "node_modules/knip": { "version": "5.88.1", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.88.1.tgz", - "integrity": "sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg==", "dev": true, "funding": [ { @@ -12600,346 +10194,130 @@ "engines": { "node": ">=18.18.0" }, - "peerDependencies": { - "@types/node": ">=18", - "typescript": ">=5.0.4 <7" - } - }, - "node_modules/knip/node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ky": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", - "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" - } - }, - "node_modules/latest-version": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", - "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", - "license": "MIT", - "dependencies": { - "package-json": "^10.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/less-openui5": { - "version": "0.11.6", - "resolved": "https://registry.npmjs.org/less-openui5/-/less-openui5-0.11.6.tgz", - "integrity": "sha512-sQmU+G2pJjFfzRI+XtXkk+T9G0s6UmWWUfOW0utPR46C9lfhNr4DH1lNJuImj64reXYi+vOwyNxPRkj0F3mofA==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/css-tools": "^4.0.2", - "clone": "^2.1.2", - "mime": "^1.6.0" - }, - "engines": { - "node": ">= 10", - "npm": ">= 5" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/licensee": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/licensee/-/licensee-11.1.1.tgz", - "integrity": "sha512-FpgdKKjvJULlBqYiKtrK7J4Oo7sQO1lHQTUOcxxE4IPQccx6c0tJWMgwVdG46+rPnLPSV7EWD6eWUtAjGO52Lg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@blueoak/list": "^15.0.0", - "@npmcli/arborist": "^7.2.1", - "correct-license-metadata": "^1.4.0", - "docopt": "^0.6.2", - "hasown": "^2.0.0", - "npm-license-corrections": "^1.6.2", - "semver": "^7.6.0", - "spdx-expression-parse": "^4.0.0", - "spdx-expression-validate": "^2.0.0", - "spdx-osi": "^3.0.0", - "spdx-whitelisted": "^1.0.0" - }, - "bin": { - "licensee": "licensee" - }, - "engines": { - "node": "^18.12 || ^20.9 || >= 22.7" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "@types/node": ">=18", + "typescript": ">=5.0.4 <7" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=14.16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/ky": { + "version": "1.14.3", + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sindresorhus/ky?sponsor=1" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/latest-version": { + "version": "9.0.0", + "license": "MIT", + "dependencies": { + "package-json": "^10.0.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/less-openui5": { + "version": "0.11.6", + "license": "Apache-2.0", + "dependencies": { + "@adobe/css-tools": "^4.0.2", + "clone": "^2.1.2", + "mime": "^1.6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 10", + "npm": ">= 5" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">= 0.8.0" + } + }, + "node_modules/licensee": { + "version": "11.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@blueoak/list": "^15.0.0", + "@npmcli/arborist": "^7.2.1", + "correct-license-metadata": "^1.4.0", + "docopt": "^0.6.2", + "hasown": "^2.0.0", + "npm-license-corrections": "^1.6.2", + "semver": "^7.6.0", + "spdx-expression-parse": "^4.0.0", + "spdx-expression-validate": "^2.0.0", + "spdx-osi": "^3.0.0", + "spdx-whitelisted": "^1.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "licensee": "licensee" + }, + "engines": { + "node": "^18.12 || ^20.9 || >= 22.7" } }, - "node_modules/lightningcss-win32-arm64-msvc": { + "node_modules/lightningcss": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/lightningcss-win32-x64-msvc": { + "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ - "x64" + "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">= 12.0.0" @@ -12951,8 +10329,6 @@ }, "node_modules/lilconfig": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { "node": ">=14" @@ -12963,8 +10339,6 @@ }, "node_modules/line-column": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", - "integrity": "sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==", "dev": true, "license": "MIT", "dependencies": { @@ -12974,15 +10348,11 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, "node_modules/linkify-it": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -12990,8 +10360,6 @@ }, "node_modules/load-json-file": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", - "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==", "dev": true, "license": "MIT", "engines": { @@ -13002,21 +10370,21 @@ } }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lockfile": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", "license": "ISC", "dependencies": { "signal-exit": "^3.0.2" @@ -13024,94 +10392,66 @@ }, "node_modules/lockfile/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true, "license": "MIT" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.isfinite": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", - "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", "license": "MIT" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", "dev": true, "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.mergewith": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.snakecase": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "dev": true, "license": "MIT" }, "node_modules/lodash.startcase": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true, "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, "node_modules/lodash.upperfirst": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -13120,8 +10460,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -13129,8 +10467,6 @@ }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -13145,8 +10481,6 @@ }, "node_modules/make-fetch-happen": { "version": "15.0.5", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", - "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", @@ -13168,14 +10502,10 @@ }, "node_modules/mark.js": { "version": "8.11.1", - "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "license": "MIT" }, "node_modules/markdown-it": { "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1", @@ -13191,8 +10521,6 @@ }, "node_modules/markdown-it-anchor": { "version": "8.6.7", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", - "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", "license": "Unlicense", "peerDependencies": { "@types/markdown-it": "*", @@ -13201,8 +10529,6 @@ }, "node_modules/markdown-it-implicit-figures": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/markdown-it-implicit-figures/-/markdown-it-implicit-figures-0.12.0.tgz", - "integrity": "sha512-IeD2V74f3ZBYrZ+bz/9uEGii0S61BYoD2731qsHTgYLlENUrTevlgODScScS1CK44/TV9ddlufGHCYCQueh1rw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13210,8 +10536,6 @@ }, "node_modules/marked": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -13222,8 +10546,6 @@ }, "node_modules/matcher": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz", - "integrity": "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==", "dev": true, "license": "MIT", "dependencies": { @@ -13236,10 +10558,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -13247,8 +10578,6 @@ }, "node_modules/md5-hex": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", - "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", "dev": true, "license": "MIT", "dependencies": { @@ -13260,8 +10589,6 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -13281,20 +10608,14 @@ }, "node_modules/mdn-data": { "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, "node_modules/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13302,8 +10623,6 @@ }, "node_modules/memoize": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", - "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", "dev": true, "license": "MIT", "dependencies": { @@ -13318,8 +10637,6 @@ }, "node_modules/meow": { "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, "license": "MIT", "engines": { @@ -13331,8 +10648,6 @@ }, "node_modules/merge-descriptors": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13340,8 +10655,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { "node": ">= 8" @@ -13349,8 +10662,6 @@ }, "node_modules/methods": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -13358,15 +10669,11 @@ }, "node_modules/micro-spelling-correcter": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/micro-spelling-correcter/-/micro-spelling-correcter-1.1.1.tgz", - "integrity": "sha512-lkJ3Rj/mtjlRcHk6YyCbvZhyWTOzdBvTHsxMmZSk5jxN1YyVSQ+JETAom55mdzfcyDrY/49Z7UCW760BK30crg==", "dev": true, "license": "CC0-1.0" }, "node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13385,8 +10692,6 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -13401,8 +10706,6 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13422,8 +10725,6 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13438,8 +10739,6 @@ }, "node_modules/micromark-util-types": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -13454,8 +10753,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -13467,8 +10764,6 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -13479,8 +10774,6 @@ }, "node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", "bin": { "mime": "cli.js" @@ -13491,8 +10784,6 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -13500,8 +10791,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -13512,8 +10801,6 @@ }, "node_modules/mime-types/node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -13521,8 +10808,6 @@ }, "node_modules/mimic-function": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { @@ -13534,29 +10819,35 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "license": "ISC" }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", + "version": "3.1.5", + "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" + } + }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.14", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13564,8 +10855,6 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -13573,8 +10862,6 @@ }, "node_modules/minipass-collect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" @@ -13585,8 +10872,6 @@ }, "node_modules/minipass-fetch": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", - "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "license": "MIT", "dependencies": { "minipass": "^7.0.3", @@ -13602,8 +10887,6 @@ }, "node_modules/minipass-flush": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "license": "BlueOak-1.0.0", "dependencies": { "minipass": "^3.0.0" @@ -13614,8 +10897,6 @@ }, "node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -13626,14 +10907,10 @@ }, "node_modules/minipass-flush/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/minipass-pipeline": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -13644,8 +10921,6 @@ }, "node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -13656,14 +10931,10 @@ }, "node_modules/minipass-pipeline/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/minipass-sized": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", - "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", "license": "ISC", "dependencies": { "minipass": "^7.1.2" @@ -13674,14 +10945,10 @@ }, "node_modules/minisearch": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", - "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", "license": "MIT" }, "node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -13692,14 +10959,10 @@ }, "node_modules/mitt": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, "node_modules/mkdirp": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -13710,14 +10973,10 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -13734,15 +10993,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -13750,15 +11005,11 @@ }, "node_modules/neo-async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT" }, "node_modules/node-fetch": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", "dependencies": { @@ -13778,8 +11029,6 @@ }, "node_modules/node-gyp": { "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", "license": "MIT", "dependencies": { "env-paths": "^2.2.0", @@ -13802,8 +11051,6 @@ }, "node_modules/node-gyp-build": { "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true, "license": "MIT", "bin": { @@ -13812,25 +11059,8 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/node-preload": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13842,14 +11072,10 @@ }, "node_modules/node-releases": { "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "license": "MIT" }, "node_modules/node-stream-zip": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13861,8 +11087,6 @@ }, "node_modules/nofilter": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true, "license": "MIT", "engines": { @@ -13870,35 +11094,20 @@ } }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, + "version": "9.0.0", "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/nopt/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-package-data": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", - "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^9.0.0", @@ -13909,10 +11118,15 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-bundled": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", - "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "license": "ISC", "dependencies": { "npm-normalize-package-bin": "^5.0.0" @@ -13923,8 +11137,6 @@ }, "node_modules/npm-install-checks": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", - "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" @@ -13935,15 +11147,11 @@ }, "node_modules/npm-license-corrections": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/npm-license-corrections/-/npm-license-corrections-1.9.0.tgz", - "integrity": "sha512-9Tq6y6zop5lsZy6dInbgrCLnqtuN+3jBc9NCusKjbeQL4LRudDkvmCYyInsDOaKN7GIVbBSvDto5MnEqYXVhxQ==", "dev": true, "license": "CC0-1.0" }, "node_modules/npm-normalize-package-bin": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", - "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -13951,8 +11159,6 @@ }, "node_modules/npm-package-arg": { "version": "13.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", - "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "license": "ISC", "dependencies": { "hosted-git-info": "^9.0.0", @@ -13966,8 +11172,6 @@ }, "node_modules/npm-packlist": { "version": "10.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", - "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "license": "ISC", "dependencies": { "ignore-walk": "^8.0.0", @@ -13979,8 +11183,6 @@ }, "node_modules/npm-pick-manifest": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", - "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", "license": "ISC", "dependencies": { "npm-install-checks": "^8.0.0", @@ -13994,8 +11196,6 @@ }, "node_modules/npm-registry-fetch": { "version": "19.1.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", - "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", "license": "ISC", "dependencies": { "@npmcli/redact": "^4.0.0", @@ -14013,8 +11213,6 @@ }, "node_modules/npm-run-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "license": "MIT", "dependencies": { @@ -14030,8 +11228,6 @@ }, "node_modules/npm-run-path/node_modules/path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, "license": "MIT", "engines": { @@ -14043,8 +11239,6 @@ }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -14055,8 +11249,6 @@ }, "node_modules/nyc": { "version": "17.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", - "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", "dev": true, "license": "ISC", "dependencies": { @@ -14097,8 +11289,6 @@ }, "node_modules/nyc/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -14107,8 +11297,6 @@ }, "node_modules/nyc/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -14121,28 +11309,8 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/nyc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/nyc/node_modules/cliui": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "license": "ISC", "dependencies": { @@ -14153,23 +11321,23 @@ }, "node_modules/nyc/node_modules/convert-source-map": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true, "license": "MIT" }, - "node_modules/nyc/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/nyc/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -14187,20 +11355,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/nyc/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/nyc/node_modules/istanbul-lib-instrument": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -14214,10 +11370,19 @@ "node": ">=10" } }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nyc/node_modules/make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "license": "MIT", "dependencies": { @@ -14232,31 +11397,39 @@ }, "node_modules/nyc/node_modules/make-dir/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/nyc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "p-try": "^2.0.0" }, "engines": { - "node": "*" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/nyc/node_modules/p-map": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14268,9 +11441,6 @@ }, "node_modules/nyc/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -14285,30 +11455,11 @@ }, "node_modules/nyc/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, - "node_modules/nyc/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nyc/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -14320,8 +11471,6 @@ }, "node_modules/nyc/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -14335,15 +11484,11 @@ }, "node_modules/nyc/node_modules/y18n": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true, "license": "ISC" }, "node_modules/nyc/node_modules/yargs": { "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "license": "MIT", "dependencies": { @@ -14365,8 +11510,6 @@ }, "node_modules/nyc/node_modules/yargs-parser": { "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "license": "ISC", "dependencies": { @@ -14379,8 +11522,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14388,15 +11529,11 @@ }, "node_modules/object-deep-merge": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.0.tgz", - "integrity": "sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==", "dev": true, "license": "MIT" }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -14407,8 +11544,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -14417,8 +11552,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -14438,14 +11571,10 @@ }, "node_modules/obuf": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -14456,8 +11585,6 @@ }, "node_modules/on-headers": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -14465,8 +11592,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -14475,8 +11600,6 @@ }, "node_modules/oniguruma-to-es": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", - "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", "license": "MIT", "dependencies": { "emoji-regex-xs": "^1.0.0", @@ -14486,8 +11609,6 @@ }, "node_modules/open": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "license": "MIT", "dependencies": { "default-browser": "^5.2.1", @@ -14504,8 +11625,6 @@ }, "node_modules/open-cli": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-8.0.0.tgz", - "integrity": "sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==", "dev": true, "license": "MIT", "dependencies": { @@ -14527,8 +11646,6 @@ }, "node_modules/open-cli/node_modules/meow": { "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", "dev": true, "license": "MIT", "engines": { @@ -14540,8 +11657,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -14558,8 +11673,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -14576,8 +11689,6 @@ }, "node_modules/oxc-resolver": { "version": "11.19.1", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", - "integrity": "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==", "dev": true, "license": "MIT", "funding": { @@ -14607,36 +11718,35 @@ } }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "license": "MIT", "engines": { "node": ">=18" @@ -14647,8 +11757,6 @@ }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -14656,8 +11764,6 @@ }, "node_modules/package-config": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/package-config/-/package-config-5.0.0.tgz", - "integrity": "sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==", "dev": true, "license": "MIT", "dependencies": { @@ -14673,8 +11779,6 @@ }, "node_modules/package-hash": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", "dev": true, "license": "ISC", "dependencies": { @@ -14689,8 +11793,6 @@ }, "node_modules/package-json": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", - "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", "license": "MIT", "dependencies": { "ky": "^1.2.0", @@ -14707,14 +11809,10 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/pacote": { "version": "21.5.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.0.tgz", - "integrity": "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ==", "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", @@ -14744,8 +11842,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -14757,8 +11853,6 @@ }, "node_modules/parent-module/node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -14767,8 +11861,6 @@ }, "node_modules/parse-conflict-json": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-5.0.1.tgz", - "integrity": "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==", "license": "ISC", "dependencies": { "json-parse-even-better-errors": "^5.0.0", @@ -14781,8 +11873,6 @@ }, "node_modules/parse-imports-exports": { "version": "0.2.4", - "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", - "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14791,8 +11881,6 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -14810,15 +11898,11 @@ }, "node_modules/parse-json/node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, "node_modules/parse-ms": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "dev": true, "license": "MIT", "engines": { @@ -14830,15 +11914,11 @@ }, "node_modules/parse-statements": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", - "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", "dev": true, "license": "MIT" }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -14849,8 +11929,6 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "license": "MIT", "dependencies": { "domhandler": "^5.0.3", @@ -14862,8 +11940,6 @@ }, "node_modules/parse5-parser-stream": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "license": "MIT", "dependencies": { "parse5": "^7.0.0" @@ -14874,8 +11950,6 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -14886,8 +11960,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -14895,8 +11967,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -14904,8 +11974,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -14914,8 +11982,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -14923,42 +11989,35 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.3.5", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/path-to-regexp": { "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "license": "MIT", "engines": { "node": ">=18" @@ -14969,8 +12028,6 @@ }, "node_modules/peek-readable": { "version": "5.4.2", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", - "integrity": "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==", "dev": true, "license": "MIT", "engines": { @@ -14983,20 +12040,14 @@ }, "node_modules/perfect-debounce": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -15007,8 +12058,6 @@ }, "node_modules/pkg-dir": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", "dev": true, "license": "MIT", "dependencies": { @@ -15018,75 +12067,8 @@ "node": ">=10" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/plur": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", - "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==", "dev": true, "license": "MIT", "dependencies": { @@ -15101,8 +12083,6 @@ }, "node_modules/portscanner": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", - "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", "license": "MIT", "dependencies": { "async": "^2.6.0", @@ -15115,8 +12095,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -15124,9 +12102,7 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.10", "funding": [ { "type": "opencollective", @@ -15153,8 +12129,6 @@ }, "node_modules/postcss-calc": { "version": "10.1.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", - "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", "license": "MIT", "dependencies": { "postcss-selector-parser": "^7.0.0", @@ -15168,13 +12142,11 @@ } }, "node_modules/postcss-colormin": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.7.tgz", - "integrity": "sha512-sBQ628lSj3VQpDquQel8Pen5mmjFPsO4pH9lDLaHB1AVkMRHtkl0pRB5DCWznc9upWsxint/kV+AveSj7W1tew==", + "version": "7.0.8", "license": "MIT", "dependencies": { - "@colordx/core": "^5.0.0", - "browserslist": "^4.28.1", + "@colordx/core": "^5.0.3", + "browserslist": "^4.28.2", "caniuse-api": "^3.0.0", "postcss-value-parser": "^4.2.0" }, @@ -15186,12 +12158,10 @@ } }, "node_modules/postcss-convert-values": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.9.tgz", - "integrity": "sha512-l6uATQATZaCa0bckHV+r6dLXfWtUBKXxO3jK+AtxxJJtgMPD+VhhPCCx51I4/5w8U5uHV67g3w7PXj+V3wlMlg==", + "version": "7.0.10", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -15203,8 +12173,6 @@ }, "node_modules/postcss-discard-comments": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.6.tgz", - "integrity": "sha512-Sq+Fzj1Eg5/CPf1ERb0wS1Im5cvE2gDXCE+si4HCn1sf+jpQZxDI4DXEp8t77B/ImzDceWE2ebJQFXdqZ6GRJw==", "license": "MIT", "dependencies": { "postcss-selector-parser": "^7.1.1" @@ -15218,8 +12186,6 @@ }, "node_modules/postcss-discard-duplicates": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", - "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -15230,8 +12196,6 @@ }, "node_modules/postcss-discard-empty": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", - "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -15242,8 +12206,6 @@ }, "node_modules/postcss-discard-overridden": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", - "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -15254,8 +12216,6 @@ }, "node_modules/postcss-merge-longhand": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", - "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", @@ -15269,12 +12229,10 @@ } }, "node_modules/postcss-merge-rules": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.8.tgz", - "integrity": "sha512-BOR1iAM8jnr7zoQSlpeBmCsWV5Uudi/+5j7k05D0O/WP3+OFMPD86c1j/20xiuRtyt45bhxw/7hnhZNhW2mNFA==", + "version": "7.0.9", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.1", "postcss-selector-parser": "^7.1.1" @@ -15288,8 +12246,6 @@ }, "node_modules/postcss-minify-font-values": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", - "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15302,12 +12258,10 @@ } }, "node_modules/postcss-minify-gradients": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.2.tgz", - "integrity": "sha512-fVY3AB8Um7SJR5usHqTY2Ngf9qh8IRN+FFzrBP0ONJy6yYXsP7xyjK2BvSAIrpgs1cST+H91V0TXi3diHLYJtw==", + "version": "7.0.3", "license": "MIT", "dependencies": { - "@colordx/core": "^5.0.0", + "@colordx/core": "^5.0.3", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, @@ -15319,12 +12273,10 @@ } }, "node_modules/postcss-minify-params": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.6.tgz", - "integrity": "sha512-YOn02gC68JijlaXVuKvFSCvQOhTpblkcfDre2hb/Aaa58r2BIaK4AtE/cyZf2wV7YKAG+UlP9DT+By0ry1E4VQ==", + "version": "7.0.7", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, @@ -15337,8 +12289,6 @@ }, "node_modules/postcss-minify-selectors": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.6.tgz", - "integrity": "sha512-lIbC0jy3AAwDxEgciZlBullDiMBeBCT+fz5G8RcA9MWqh/hfUkpOI3vNDUNEZHgokaoiv0juB9Y8fGcON7rU/A==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15353,8 +12303,6 @@ }, "node_modules/postcss-normalize-charset": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", - "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -15365,8 +12313,6 @@ }, "node_modules/postcss-normalize-display-values": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", - "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15380,8 +12326,6 @@ }, "node_modules/postcss-normalize-positions": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", - "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15395,8 +12339,6 @@ }, "node_modules/postcss-normalize-repeat-style": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", - "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15410,8 +12352,6 @@ }, "node_modules/postcss-normalize-string": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", - "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15425,8 +12365,6 @@ }, "node_modules/postcss-normalize-timing-functions": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", - "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15439,12 +12377,10 @@ } }, "node_modules/postcss-normalize-unicode": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.6.tgz", - "integrity": "sha512-z6bwTV84YW6ZvvNoaNLuzRW4/uWxDKYI1iIDrzk6D2YTL7hICApy+Q1LP6vBEsljX8FM7YSuV9qI79XESd4ddQ==", + "version": "7.0.7", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -15456,8 +12392,6 @@ }, "node_modules/postcss-normalize-url": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", - "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15471,8 +12405,6 @@ }, "node_modules/postcss-normalize-whitespace": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", - "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15486,8 +12418,6 @@ }, "node_modules/postcss-ordered-values": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", - "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", "license": "MIT", "dependencies": { "cssnano-utils": "^5.0.1", @@ -15501,12 +12431,10 @@ } }, "node_modules/postcss-reduce-initial": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.6.tgz", - "integrity": "sha512-G6ZyK68AmrPdMB6wyeA37ejnnRG2S8xinJrZJnOv+IaRKf6koPAVbQsiC7MfkmXaGmF1UO+QCijb27wfpxuRNg==", + "version": "7.0.7", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "caniuse-api": "^3.0.0" }, "engines": { @@ -15518,8 +12446,6 @@ }, "node_modules/postcss-reduce-transforms": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", - "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -15533,8 +12459,6 @@ }, "node_modules/postcss-selector-parser": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15546,8 +12470,6 @@ }, "node_modules/postcss-svgo": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.1.tgz", - "integrity": "sha512-zU9H9oEDrUFKa0JB7w+IYL7Qs9ey1mZyjhbf0KLxwJDdDRtoPvCmaEfknzqfHj44QS9VD6c5sJnBAVYTLRg/Sg==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", @@ -15562,8 +12484,6 @@ }, "node_modules/postcss-unique-selectors": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.5.tgz", - "integrity": "sha512-3QoYmEt4qg/rUWDn6Tc8+ZVPmbp4G1hXDtCNWDx0st8SjtCbRcxRXDDM1QrEiXGG3A45zscSJFb4QH90LViyxg==", "license": "MIT", "dependencies": { "postcss-selector-parser": "^7.1.1" @@ -15577,14 +12497,10 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, "node_modules/preact": { - "version": "10.29.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", - "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", + "version": "10.29.1", "license": "MIT", "funding": { "type": "opencollective", @@ -15593,8 +12509,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -15603,17 +12517,10 @@ }, "node_modules/pretty-data": { "version": "0.40.0", - "resolved": "https://registry.npmjs.org/pretty-data/-/pretty-data-0.40.0.tgz", - "integrity": "sha512-YFLnEdDEDnkt/GEhet5CYZHCvALw6+Elyb/tp8kQG03ZSIuzeaDWpZYndCXwgqu4NAjh1PI534dhDS1mHarRnQ==", - "license": "MIT", - "engines": { - "node": "*" - } + "license": "MIT" }, "node_modules/pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -15621,8 +12528,6 @@ }, "node_modules/pretty-ms": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15637,8 +12542,6 @@ }, "node_modules/proc-log": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -15646,8 +12549,6 @@ }, "node_modules/process": { "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, "license": "MIT", "engines": { @@ -15656,14 +12557,10 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/process-on-spawn": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", - "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -15675,8 +12572,6 @@ }, "node_modules/proggy": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz", - "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -15684,8 +12579,6 @@ }, "node_modules/promise-all-reject-late": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", - "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", "license": "ISC", "funding": { "url": "https://github.com/sponsors/isaacs" @@ -15693,8 +12586,6 @@ }, "node_modules/promise-call-limit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", - "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", "license": "ISC", "funding": { "url": "https://github.com/sponsors/isaacs" @@ -15702,15 +12593,11 @@ }, "node_modules/promise-inflight": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "dev": true, "license": "ISC" }, "node_modules/promise-retry": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", "dependencies": { @@ -15723,8 +12610,6 @@ }, "node_modules/property-information": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -15733,14 +12618,10 @@ }, "node_modules/proto-list": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "license": "ISC" }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -15752,8 +12633,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -15762,8 +12641,6 @@ }, "node_modules/punycode.js": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", "engines": { "node": ">=6" @@ -15771,8 +12648,6 @@ }, "node_modules/pupa": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", - "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" @@ -15785,9 +12660,7 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -15801,8 +12674,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -15821,8 +12692,6 @@ }, "node_modules/quibble": { "version": "0.9.2", - "resolved": "https://registry.npmjs.org/quibble/-/quibble-0.9.2.tgz", - "integrity": "sha512-BrL7hrZcbyyt5ZDfePkGFDc3m82uUtxCPOnpRUrkOdtBnmV9ldQKxXORkKL8eIzToRNaCpIPyKyfdfq/tBlFAA==", "dev": true, "license": "MIT", "dependencies": { @@ -15835,8 +12704,6 @@ }, "node_modules/random-int": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/random-int/-/random-int-3.1.0.tgz", - "integrity": "sha512-h8CRz8cpvzj0hC/iH/1Gapgcl2TQ6xtnCpyOI5WvWfXf/yrDx2DOU+tD9rX23j36IF11xg1KqB9W11Z18JPMdw==", "license": "MIT", "engines": { "node": ">=12" @@ -15847,8 +12714,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -15856,8 +12721,6 @@ }, "node_modules/raw-body": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -15871,8 +12734,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -15886,14 +12747,10 @@ }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15901,8 +12758,6 @@ }, "node_modules/read-cmd-shim": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", - "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -15910,8 +12765,6 @@ }, "node_modules/read-package-json-fast": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", "dev": true, "license": "ISC", "dependencies": { @@ -15924,8 +12777,6 @@ }, "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, "license": "MIT", "engines": { @@ -15934,8 +12785,6 @@ }, "node_modules/read-package-json-fast/node_modules/npm-normalize-package-bin": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", "dev": true, "license": "ISC", "engines": { @@ -15944,8 +12793,6 @@ }, "node_modules/read-package-up": { "version": "12.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", - "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", "license": "MIT", "dependencies": { "find-up-simple": "^1.0.1", @@ -15961,8 +12808,6 @@ }, "node_modules/read-package-up/node_modules/type-fest": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", - "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -15976,8 +12821,6 @@ }, "node_modules/read-pkg": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", - "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.4", @@ -15995,8 +12838,6 @@ }, "node_modules/read-pkg/node_modules/parse-json": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -16012,8 +12853,6 @@ }, "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -16024,8 +12863,6 @@ }, "node_modules/read-pkg/node_modules/type-fest": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", - "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -16039,8 +12876,6 @@ }, "node_modules/read-pkg/node_modules/unicorn-magic": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", "license": "MIT", "engines": { "node": ">=20" @@ -16051,8 +12886,6 @@ }, "node_modules/readable-stream": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "dev": true, "license": "MIT", "dependencies": { @@ -16068,8 +12901,6 @@ }, "node_modules/readable-web-to-node-stream": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", - "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", "dev": true, "license": "MIT", "dependencies": { @@ -16083,10 +12914,28 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -16108,8 +12957,6 @@ }, "node_modules/regex": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -16117,8 +12964,6 @@ }, "node_modules/regex-recursion": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -16126,14 +12971,10 @@ }, "node_modules/regex-utilities": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -16153,8 +12994,6 @@ }, "node_modules/registry-auth-token": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", - "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^3.0.2" @@ -16165,8 +13004,6 @@ }, "node_modules/registry-url": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", "license": "MIT", "dependencies": { "rc": "1.2.8" @@ -16180,8 +13017,6 @@ }, "node_modules/release-zalgo": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", "dev": true, "license": "ISC", "dependencies": { @@ -16191,60 +13026,8 @@ "node": ">=4" } }, - "node_modules/replacestream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", - "license": "BSD-3-Clause", - "dependencies": { - "escape-string-regexp": "^1.0.3", - "object-assign": "^4.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/replacestream/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/replacestream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/replacestream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/replacestream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -16253,8 +13036,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16262,15 +13043,11 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true, "license": "ISC" }, "node_modules/requizzle": { "version": "0.2.4", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", - "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", "license": "MIT", "dependencies": { "lodash": "^4.17.21" @@ -16278,8 +13055,6 @@ }, "node_modules/reserved-identifiers": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", - "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", "dev": true, "license": "MIT", "engines": { @@ -16290,11 +13065,10 @@ } }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -16311,8 +13085,6 @@ }, "node_modules/resolve-cwd": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -16323,8 +13095,6 @@ }, "node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "license": "MIT", "engines": { "node": ">=8" @@ -16332,8 +13102,6 @@ }, "node_modules/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { @@ -16342,8 +13110,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -16352,14 +13118,10 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, "node_modules/rimraf": { "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -16376,55 +13138,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -16467,8 +13182,6 @@ }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -16483,8 +13196,6 @@ }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -16493,8 +13204,6 @@ }, "node_modules/run-applescript": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "license": "MIT", "engines": { "node": ">=18" @@ -16505,8 +13214,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -16528,8 +13235,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -16548,15 +13253,11 @@ }, "node_modules/safe-array-concat/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -16575,8 +13276,6 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -16592,15 +13291,11 @@ }, "node_modules/safe-push-apply/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -16617,14 +13312,10 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -16632,21 +13323,15 @@ }, "node_modules/search-insights": { "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", "license": "MIT", "peer": true }, "node_modules/select-hose": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "license": "MIT" }, "node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -16657,8 +13342,6 @@ }, "node_modules/send": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -16681,8 +13364,6 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -16690,14 +13371,10 @@ }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, "license": "MIT", "dependencies": { @@ -16712,8 +13389,6 @@ }, "node_modules/serialize-error/node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -16725,8 +13400,6 @@ }, "node_modules/serve-static": { "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -16740,15 +13413,11 @@ }, "node_modules/set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true, "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -16765,8 +13434,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16781,8 +13448,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -16796,14 +13461,10 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -16814,8 +13475,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -16823,8 +13482,6 @@ }, "node_modules/shiki": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", - "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", "license": "MIT", "dependencies": { "@shikijs/core": "2.5.0", @@ -16839,8 +13496,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -16857,13 +13512,11 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -16874,8 +13527,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -16892,8 +13543,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -16911,8 +13560,6 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -16923,8 +13570,6 @@ }, "node_modules/sigstore": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", - "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", @@ -16939,17 +13584,14 @@ } }, "node_modules/sinon": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", - "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", + "version": "21.1.2", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.1", - "@sinonjs/samsam": "^9.0.3", - "diff": "^8.0.3", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.3.2", + "@sinonjs/samsam": "^10.0.2", + "diff": "^8.0.4" }, "funding": { "type": "opencollective", @@ -16958,8 +13600,6 @@ }, "node_modules/slash": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "license": "MIT", "engines": { "node": ">=14.16" @@ -16970,8 +13610,6 @@ }, "node_modules/slice-ansi": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16987,8 +13625,6 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -16997,8 +13633,6 @@ }, "node_modules/smol-toml": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -17010,8 +13644,6 @@ }, "node_modules/socks": { "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", "dependencies": { "ip-address": "^10.0.1", @@ -17024,8 +13656,6 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -17038,8 +13668,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -17047,8 +13675,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -17056,8 +13682,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -17066,8 +13690,6 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", "funding": { "type": "github", @@ -17076,8 +13698,6 @@ }, "node_modules/spawn-wrap": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, "license": "ISC", "dependencies": { @@ -17085,35 +13705,15 @@ "is-windows": "^1.0.2", "make-dir": "^3.0.0", "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/spawn-wrap/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/spawn-wrap/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/spawn-wrap/node_modules/foreground-child": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "license": "ISC", "dependencies": { @@ -17126,9 +13726,6 @@ }, "node_modules/spawn-wrap/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -17148,15 +13745,11 @@ }, "node_modules/spawn-wrap/node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/spawn-wrap/node_modules/make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "license": "MIT", "dependencies": { @@ -17169,24 +13762,8 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/spawn-wrap/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/spawn-wrap/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -17201,8 +13778,6 @@ }, "node_modules/spawn-wrap/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -17211,15 +13786,11 @@ }, "node_modules/spawn-wrap/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/spawn-wrap/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -17234,8 +13805,6 @@ }, "node_modules/spdx-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", - "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", "dev": true, "license": "MIT", "dependencies": { @@ -17246,8 +13815,6 @@ }, "node_modules/spdx-compare/node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -17257,8 +13824,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -17267,8 +13832,6 @@ }, "node_modules/spdx-correct/node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -17277,14 +13840,10 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -17293,8 +13852,6 @@ }, "node_modules/spdx-expression-validate": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz", - "integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==", "dev": true, "license": "(MIT AND CC-BY-3.0)", "dependencies": { @@ -17303,8 +13860,6 @@ }, "node_modules/spdx-expression-validate/node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -17314,28 +13869,20 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "license": "CC0-1.0" }, "node_modules/spdx-osi": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-osi/-/spdx-osi-3.0.0.tgz", - "integrity": "sha512-7DZMaD/rNHWGf82qWOazBsLXQsaLsoJb9RRjhEUQr5o86kw3A1ErGzSdvaXl+KalZyKkkU5T2a5NjCCutAKQSw==", "dev": true, "license": "CC0-1.0" }, "node_modules/spdx-ranges": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", - "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", "dev": true, "license": "(MIT AND CC-BY-3.0)" }, "node_modules/spdx-whitelisted": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-whitelisted/-/spdx-whitelisted-1.0.0.tgz", - "integrity": "sha512-X4FOpUCvZuo42MdB1zAZ/wdX4N0lLcWDozf2KYFVDgtLv8Lx+f31LOYLP2/FcwTzsPi64bS/VwKqklI4RBletg==", "dev": true, "license": "MIT", "dependencies": { @@ -17345,8 +13892,6 @@ }, "node_modules/spdy": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -17361,8 +13906,6 @@ }, "node_modules/spdy-transport": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -17375,8 +13918,6 @@ }, "node_modules/spdy-transport/node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -17389,8 +13930,6 @@ }, "node_modules/speakingurl": { "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -17398,15 +13937,11 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/ssri": { "version": "13.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", - "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" @@ -17417,8 +13952,6 @@ }, "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17430,8 +13963,6 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, "license": "MIT", "engines": { @@ -17440,8 +13971,6 @@ }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -17449,8 +13978,6 @@ }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17463,35 +13990,26 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "4.2.3", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17504,23 +14022,13 @@ }, "node_modules/string-width-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -17528,8 +14036,30 @@ }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17540,8 +14070,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -17562,8 +14090,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17581,8 +14107,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -17599,8 +14123,6 @@ }, "node_modules/stringify-entities": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -17613,8 +14135,6 @@ }, "node_modules/stringify-object-es5": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/stringify-object-es5/-/stringify-object-es5-2.5.0.tgz", - "integrity": "sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -17627,8 +14147,6 @@ }, "node_modules/stringify-object-es5/node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "license": "MIT", "engines": { @@ -17637,8 +14155,6 @@ }, "node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -17653,8 +14169,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17665,8 +14179,6 @@ }, "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -17674,8 +14186,6 @@ }, "node_modules/strip-bom": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { @@ -17684,8 +14194,6 @@ }, "node_modules/strip-final-newline": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, "license": "MIT", "engines": { @@ -17697,8 +14205,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "license": "MIT", "engines": { "node": ">=8" @@ -17709,8 +14215,6 @@ }, "node_modules/strtok3": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.1.1.tgz", - "integrity": "sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==", "dev": true, "license": "MIT", "dependencies": { @@ -17727,8 +14231,6 @@ }, "node_modules/stubborn-fs": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", - "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", "license": "MIT", "dependencies": { "stubborn-utils": "^1.0.1" @@ -17736,17 +14238,13 @@ }, "node_modules/stubborn-utils": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", - "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", "license": "MIT" }, "node_modules/stylehacks": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.8.tgz", - "integrity": "sha512-I3f053GBLIiS5Fg6OMFhq/c+yW+5Hc2+1fgq7gElDMMSqwlRb3tBf2ef6ucLStYRpId4q//bQO1FjcyNyy4yDQ==", + "version": "7.0.9", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "postcss-selector-parser": "^7.1.1" }, "engines": { @@ -17758,8 +14256,6 @@ }, "node_modules/superagent": { "version": "10.3.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", - "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17779,8 +14275,6 @@ }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -17792,8 +14286,6 @@ }, "node_modules/superjson": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", - "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", "license": "MIT", "dependencies": { "copy-anything": "^4" @@ -17804,8 +14296,6 @@ }, "node_modules/supertap": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", - "integrity": "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==", "dev": true, "license": "MIT", "dependencies": { @@ -17820,8 +14310,6 @@ }, "node_modules/supertap/node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { @@ -17830,8 +14318,6 @@ }, "node_modules/supertap/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -17844,8 +14330,6 @@ }, "node_modules/supertest": { "version": "7.2.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", - "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", "dev": true, "license": "MIT", "dependencies": { @@ -17859,8 +14343,6 @@ }, "node_modules/supertest/node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", "engines": { @@ -17869,8 +14351,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -17882,8 +14362,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -17894,8 +14372,6 @@ }, "node_modules/svgo": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", - "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", "license": "MIT", "dependencies": { "commander": "^11.1.0", @@ -17919,8 +14395,6 @@ }, "node_modules/svgo/node_modules/commander": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "license": "MIT", "engines": { "node": ">=16" @@ -17928,14 +14402,10 @@ }, "node_modules/tabbable": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/tagged-tag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "license": "MIT", "engines": { "node": ">=20" @@ -17946,14 +14416,10 @@ }, "node_modules/tailwindcss": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "license": "MIT" }, "node_modules/tapable": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "license": "MIT", "engines": { "node": ">=6" @@ -17965,8 +14431,6 @@ }, "node_modules/tar": { "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -17981,8 +14445,6 @@ }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -17990,8 +14452,6 @@ }, "node_modules/temp-dir": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", "dev": true, "license": "MIT", "engines": { @@ -18000,8 +14460,6 @@ }, "node_modules/tempy": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", - "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18019,8 +14477,6 @@ }, "node_modules/tempy/node_modules/is-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, "license": "MIT", "engines": { @@ -18032,8 +14488,6 @@ }, "node_modules/tempy/node_modules/type-fest": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -18045,8 +14499,6 @@ }, "node_modules/terser": { "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -18063,14 +14515,10 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/test-exclude": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", "dependencies": { @@ -18082,29 +14530,8 @@ "node": ">=8" } }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -18122,23 +14549,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/testdouble": { "version": "3.20.2", - "resolved": "https://registry.npmjs.org/testdouble/-/testdouble-3.20.2.tgz", - "integrity": "sha512-790e9vJKdfddWNOaxW1/V9FcMk48cPEl3eJSj2i8Hh1fX89qArEJ6cp3DBnaECpGXc3xKJVWbc1jeNlWYWgiMg==", "dev": true, "license": "MIT", "dependencies": { @@ -18153,15 +14565,11 @@ }, "node_modules/theredoc": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/theredoc/-/theredoc-1.0.0.tgz", - "integrity": "sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==", "dev": true, "license": "MIT" }, "node_modules/time-zone": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", - "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", "dev": true, "license": "MIT", "engines": { @@ -18169,9 +14577,7 @@ } }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.1.1", "dev": true, "license": "MIT", "engines": { @@ -18179,13 +14585,11 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -18196,8 +14600,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -18208,8 +14610,6 @@ }, "node_modules/to-valid-identifier": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", - "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", "dev": true, "license": "MIT", "dependencies": { @@ -18225,8 +14625,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -18234,8 +14632,6 @@ }, "node_modules/token-types": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", "dev": true, "license": "MIT", "dependencies": { @@ -18252,15 +14648,11 @@ }, "node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT" }, "node_modules/traverse": { "version": "0.6.11", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", - "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", "dev": true, "license": "MIT", "dependencies": { @@ -18277,8 +14669,6 @@ }, "node_modules/treeverse": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", - "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -18286,8 +14676,6 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", "funding": { "type": "github", @@ -18296,15 +14684,10 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", - "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", "license": "MIT", "dependencies": { "@tufjs/models": "4.1.0", @@ -18317,8 +14700,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -18330,8 +14711,6 @@ }, "node_modules/type-detect": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, "license": "MIT", "engines": { @@ -18340,8 +14719,6 @@ }, "node_modules/type-fest": { "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -18350,8 +14727,6 @@ }, "node_modules/type-is": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -18364,8 +14739,6 @@ }, "node_modules/type-is/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -18380,8 +14753,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -18395,8 +14766,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -18415,8 +14784,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18437,8 +14804,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -18458,8 +14823,6 @@ }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, "license": "MIT", "dependencies": { @@ -18468,8 +14831,6 @@ }, "node_modules/typedarray.prototype.slice": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", - "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", "dev": true, "license": "MIT", "dependencies": { @@ -18491,8 +14852,6 @@ }, "node_modules/typescript": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "devOptional": true, "license": "Apache-2.0", "peer": true, @@ -18506,14 +14865,10 @@ }, "node_modules/uc.micro": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, "node_modules/uglify-js": { "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, "license": "BSD-2-Clause", "optional": true, @@ -18526,8 +14881,6 @@ }, "node_modules/unbash": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unbash/-/unbash-2.2.0.tgz", - "integrity": "sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==", "dev": true, "license": "ISC", "engines": { @@ -18536,8 +14889,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -18555,29 +14906,21 @@ }, "node_modules/underscore": { "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.25.0", "license": "MIT", "engines": { "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "license": "MIT", "engines": { "node": ">=18" @@ -18588,8 +14931,6 @@ }, "node_modules/unique-filename": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, "license": "ISC", "dependencies": { @@ -18601,8 +14942,6 @@ }, "node_modules/unique-slug": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, "license": "ISC", "dependencies": { @@ -18614,8 +14953,6 @@ }, "node_modules/unique-string": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18630,8 +14967,6 @@ }, "node_modules/unist-util-is": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -18643,8 +14978,6 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -18656,8 +14989,6 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -18669,8 +15000,6 @@ }, "node_modules/unist-util-visit": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18684,8 +15013,6 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18698,8 +15025,6 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -18707,8 +15032,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -18737,8 +15060,6 @@ }, "node_modules/update-notifier": { "version": "7.3.1", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", - "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", "license": "BSD-2-Clause", "dependencies": { "boxen": "^8.0.1", @@ -18759,10 +15080,18 @@ "url": "https://github.com/yeoman/update-notifier?sponsor=1" } }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -18771,14 +15100,10 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -18786,8 +15111,6 @@ }, "node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", "bin": { @@ -18796,8 +15119,6 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -18806,8 +15127,6 @@ }, "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -18816,8 +15135,6 @@ }, "node_modules/validate-npm-package-name": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -18825,8 +15142,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -18834,8 +15149,6 @@ }, "node_modules/vfile": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18848,8 +15161,6 @@ }, "node_modules/vfile-message": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18862,8 +15173,6 @@ }, "node_modules/vite": { "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -18921,8 +15230,6 @@ }, "node_modules/vitepress": { "version": "1.6.4", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", - "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", "license": "MIT", "dependencies": { "@docsearch/css": "3.8.2", @@ -18962,8 +15269,6 @@ }, "node_modules/vue": { "version": "3.5.32", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", - "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.32", @@ -18983,8 +15288,6 @@ }, "node_modules/walk-up-path": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", - "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", "license": "ISC", "engines": { "node": "20 || >=22" @@ -18992,8 +15295,6 @@ }, "node_modules/wbuf": { "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" @@ -19001,15 +15302,11 @@ }, "node_modules/webidl-conversions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/well-known-symbols": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", - "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", "dev": true, "license": "ISC", "engines": { @@ -19018,9 +15315,6 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -19031,8 +15325,6 @@ }, "node_modules/whatwg-encoding/node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -19043,8 +15335,6 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "license": "MIT", "engines": { "node": ">=18" @@ -19052,8 +15342,6 @@ }, "node_modules/whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", "dependencies": { @@ -19063,14 +15351,10 @@ }, "node_modules/when-exit": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", - "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", "license": "MIT" }, "node_modules/which": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "license": "ISC", "dependencies": { "isexe": "^4.0.0" @@ -19084,8 +15368,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -19104,8 +15386,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19132,15 +15412,11 @@ }, "node_modules/which-builtin-type/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -19158,15 +15434,11 @@ }, "node_modules/which-module": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true, "license": "ISC" }, "node_modules/which-typed-array": { "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -19187,8 +15459,6 @@ }, "node_modules/widest-line": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", - "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "license": "MIT", "dependencies": { "string-width": "^7.0.0" @@ -19200,10 +15470,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -19212,29 +15499,23 @@ }, "node_modules/wordwrap": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true, "license": "MIT" }, "node_modules/workerpool": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.1.tgz", - "integrity": "sha512-NAnKwZJxWlj/U1cp6ZkEtPE+GQY1S6KtOS3AlCiPfPFLxV3m64giSp7g2LsNJxzYCocDT7TSl+7T0sgrDp3KoQ==", "license": "Apache-2.0" }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "9.0.2", "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -19243,8 +15524,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -19260,8 +15539,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -19269,8 +15546,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19282,39 +15557,8 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19324,23 +15568,19 @@ } }, "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "10.6.0", "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "7.2.0", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -19348,15 +15588,11 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", - "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", "dev": true, "license": "ISC", "dependencies": { @@ -19369,8 +15605,6 @@ }, "node_modules/wsl-utils": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "license": "MIT", "dependencies": { "is-wsl": "^3.1.0" @@ -19384,8 +15618,6 @@ }, "node_modules/xdg-basedir": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "license": "MIT", "engines": { "node": ">=12" @@ -19396,8 +15628,6 @@ }, "node_modules/xml2js": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "license": "MIT", "dependencies": { "sax": ">=0.6.0", @@ -19409,8 +15639,6 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "license": "MIT", "engines": { "node": ">=4.0" @@ -19418,14 +15646,10 @@ }, "node_modules/xmlcreate": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "license": "Apache-2.0" }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" @@ -19433,15 +15657,11 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { @@ -19456,14 +15676,10 @@ }, "node_modules/yaml-ast-parser": { "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", "license": "Apache-2.0" }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -19481,79 +15697,18 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yesno": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz", - "integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==", "license": "BSD" }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -19565,8 +15720,6 @@ }, "node_modules/yoctocolors": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", "dev": true, "license": "MIT", "engines": { @@ -19578,8 +15731,6 @@ }, "node_modules/zod": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { @@ -19588,8 +15739,6 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "license": "MIT", "funding": { "type": "github", @@ -19676,10 +15825,18 @@ "npm": ">= 8" } }, + "packages/cli/node_modules/chalk": { + "version": "5.6.2", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "packages/cli/node_modules/cliui": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "license": "ISC", "dependencies": { "string-width": "^7.2.0", @@ -19690,27 +15847,27 @@ "node": ">=20" } }, - "packages/cli/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "packages/cli/node_modules/emoji-regex": { + "version": "10.6.0", + "license": "MIT" + }, + "packages/cli/node_modules/string-width": { + "version": "7.2.0", "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" }, "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "packages/cli/node_modules/yargs": { "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "license": "MIT", "dependencies": { "cliui": "^9.0.1", @@ -19726,8 +15883,6 @@ }, "packages/cli/node_modules/yargs-parser": { "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" @@ -19739,6 +15894,7 @@ "license": "Apache-2.0", "dependencies": { "@ui5/logger": "^5.0.0-alpha.4", + "async-mutex": "^0.5.0", "clone": "^2.1.2", "escape-string-regexp": "^5.0.0", "globby": "^15.0.0", @@ -19746,7 +15902,8 @@ "micromatch": "^4.0.8", "minimatch": "^10.2.2", "pretty-hrtime": "^1.0.3", - "random-int": "^3.1.0" + "random-int": "^3.1.0", + "ssri": "^13.0.0" }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.3.0", @@ -19763,10 +15920,18 @@ "npm": ">= 8" } }, + "packages/fs/node_modules/escape-string-regexp": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/fs/node_modules/globby": { "version": "15.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz", - "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==", "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -19785,13 +15950,24 @@ }, "packages/fs/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { "node": ">= 4" } }, + "packages/fs/node_modules/minimatch": { + "version": "10.2.5", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/logger": { "name": "@ui5/logger", "version": "5.0.0-alpha.4", @@ -19816,6 +15992,16 @@ "npm": ">= 8" } }, + "packages/logger/node_modules/chalk": { + "version": "5.6.2", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "packages/project": { "name": "@ui5/project", "version": "5.0.0-alpha.4", @@ -19827,12 +16013,14 @@ "ajv": "^8.18.0", "ajv-errors": "^3.0.0", "chalk": "^5.6.2", + "chokidar": "^3.6.0", "escape-string-regexp": "^5.0.0", "globby": "^14.1.0", "graceful-fs": "^4.2.11", "js-yaml": "^4.1.1", "lockfile": "^1.0.4", "make-fetch-happen": "^15.0.5", + "micromatch": "^4.0.8", "node-stream-zip": "^1.15.0", "pacote": "^21.0.4", "pretty-hrtime": "^1.0.3", @@ -19840,6 +16028,7 @@ "read-pkg": "^10.0.0", "resolve": "^1.22.10", "semver": "^7.7.2", + "ssri": "^13.0.1", "xml2js": "^0.6.2", "yesno": "^0.4.0" }, @@ -19871,10 +16060,28 @@ } } }, + "packages/project/node_modules/chalk": { + "version": "5.6.2", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "packages/project/node_modules/escape-string-regexp": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/project/node_modules/istanbul-lib-instrument": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -19908,7 +16115,6 @@ "mime-types": "^2.1.35", "parseurl": "^1.3.3", "portscanner": "^2.2.0", - "replacestream": "^4.0.3", "router": "^2.2.0", "spdy": "^4.0.2", "yesno": "^0.4.0" diff --git a/packages/builder/lib/processors/nonAsciiEscaper.js b/packages/builder/lib/processors/nonAsciiEscaper.js index ff9d58e97d6..493c680453b 100644 --- a/packages/builder/lib/processors/nonAsciiEscaper.js +++ b/packages/builder/lib/processors/nonAsciiEscaper.js @@ -83,8 +83,8 @@ async function nonAsciiEscaper({resources, options: {encoding}}) { // only modify the resource's string if it was changed if (escaped.modified) { resource.setString(escaped.string); + return resource; } - return resource; } return Promise.all(resources.map(processResource)); diff --git a/packages/builder/lib/processors/stringReplacer.js b/packages/builder/lib/processors/stringReplacer.js index 2485032cc76..8c4bf6560dc 100644 --- a/packages/builder/lib/processors/stringReplacer.js +++ b/packages/builder/lib/processors/stringReplacer.js @@ -21,9 +21,10 @@ export default function({resources, options: {pattern, replacement}}) { return Promise.all(resources.map(async (resource) => { const content = await resource.getString(); const newContent = content.replaceAll(pattern, replacement); + // only modify the resource's string if it was changed if (content !== newContent) { resource.setString(newContent); + return resource; } - return resource; })); } diff --git a/packages/builder/lib/tasks/buildThemes.js b/packages/builder/lib/tasks/buildThemes.js index d407bb97ff7..de9f2e4d6fe 100644 --- a/packages/builder/lib/tasks/buildThemes.js +++ b/packages/builder/lib/tasks/buildThemes.js @@ -192,7 +192,7 @@ export default async function({ } let processedResources; - const useWorkers = !!taskUtil; + const useWorkers = !process.env.UI5_CLI_NO_WORKERS && !!taskUtil; if (useWorkers) { const threadMessageHandler = new FsMainThreadInterface(fsInterface(combo)); diff --git a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js index 53cb3e8d9f3..697b2425080 100644 --- a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js +++ b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js @@ -14,17 +14,24 @@ import nonAsciiEscaper from "../processors/nonAsciiEscaper.js"; * * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project. + * This is only set if a cache is used and changes have been detected. * @param {object} parameters.options Options * @param {string} parameters.options.pattern Glob pattern to locate the files to be processed * @param {string} parameters.options.encoding source file encoding either "UTF-8" or "ISO-8859-1" * @returns {Promise} Promise resolving with undefined once data has been written */ -export default async function({workspace, options: {pattern, encoding}}) { +export default async function({workspace, changedProjectResourcePaths, options: {pattern, encoding}}) { if (!encoding) { throw new Error("[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided"); } - const allResources = await workspace.byGlob(pattern); + let allResources; + if (changedProjectResourcePaths) { + allResources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource))); + } else { + allResources = await workspace.byGlob(pattern); + } const processedResources = await nonAsciiEscaper({ resources: allResources, @@ -33,5 +40,5 @@ export default async function({workspace, options: {pattern, encoding}}) { } }); - await Promise.all(processedResources.map((resource) => workspace.write(resource))); + await Promise.all(processedResources.map((resource) => resource && workspace.write(resource))); } diff --git a/packages/builder/lib/tasks/minify.js b/packages/builder/lib/tasks/minify.js index 2969ca688dc..439f53fc9a0 100644 --- a/packages/builder/lib/tasks/minify.js +++ b/packages/builder/lib/tasks/minify.js @@ -16,6 +16,8 @@ import fsInterface from "@ui5/fs/fsInterface"; * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil + * @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project. + * This is only set if a cache is used and changes have been detected. * @param {object} parameters.options Options * @param {string} parameters.options.pattern Pattern to locate the files to be processed * @param {boolean} [parameters.options.omitSourceMapResources=false] Whether source map resources shall @@ -26,9 +28,24 @@ import fsInterface from "@ui5/fs/fsInterface"; * @returns {Promise} Promise resolving with undefined once data has been written */ export default async function({ - workspace, taskUtil, options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true - }}) { - const resources = await workspace.byGlob(pattern); + workspace, taskUtil, changedProjectResourcePaths, + options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true} +}) { + let resources; + if (changedProjectResourcePaths) { + resources = await Promise.all( + changedProjectResourcePaths + // Filtering out non-JS resources such as .map files + // FIXME: The changed resources should rather be matched against the provided pattern + .filter((resourcePath) => resourcePath.endsWith(".js")) + .map((resource) => workspace.byPath(resource)) + ); + } else { + resources = await workspace.byGlob(pattern); + } + if (resources.length === 0) { + return; + } const processedResources = await minifier({ resources, fs: fsInterface(workspace), @@ -36,7 +53,7 @@ export default async function({ options: { addSourceMappingUrl: !omitSourceMapResources, readSourceMappingUrl: !!useInputSourceMaps, - useWorkers: !!taskUtil, + useWorkers: !process.env.UI5_CLI_NO_WORKERS && !!taskUtil, } }); diff --git a/packages/builder/lib/tasks/replaceBuildtime.js b/packages/builder/lib/tasks/replaceBuildtime.js index f4093c0b732..44498a09186 100644 --- a/packages/builder/lib/tasks/replaceBuildtime.js +++ b/packages/builder/lib/tasks/replaceBuildtime.js @@ -28,26 +28,30 @@ function getTimestamp() { * * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project. + * This is only set if a cache is used and changes have been detected. * @param {object} parameters.options Options * @param {string} parameters.options.pattern Pattern to locate the files to be processed * @returns {Promise} Promise resolving with undefined once data has been written */ -export default function({workspace, options: {pattern}}) { +export default async function({workspace, changedProjectResourcePaths, options: {pattern}}) { + let resources; + if (changedProjectResourcePaths) { + resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource))); + } else { + resources = await workspace.byGlob(pattern); + } const timestamp = getTimestamp(); - - return workspace.byGlob(pattern) - .then((processedResources) => { - return stringReplacer({ - resources: processedResources, - options: { - pattern: "${buildtime}", - replacement: timestamp - } - }); - }) - .then((processedResources) => { - return Promise.all(processedResources.map((resource) => { - return workspace.write(resource); - })); - }); + const processedResources = await stringReplacer({ + resources, + options: { + pattern: "${buildtime}", + replacement: timestamp + } + }); + return Promise.all(processedResources.map((resource) => { + if (resource) { + return workspace.write(resource); + } + })); } diff --git a/packages/builder/lib/tasks/replaceCopyright.js b/packages/builder/lib/tasks/replaceCopyright.js index 2ccb6a596df..90daed02fd5 100644 --- a/packages/builder/lib/tasks/replaceCopyright.js +++ b/packages/builder/lib/tasks/replaceCopyright.js @@ -24,32 +24,38 @@ import stringReplacer from "../processors/stringReplacer.js"; * * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project. + * This is only set if a cache is used and changes have been detected. * @param {object} parameters.options Options * @param {string} parameters.options.copyright Replacement copyright * @param {string} parameters.options.pattern Pattern to locate the files to be processed * @returns {Promise} Promise resolving with undefined once data has been written */ -export default function({workspace, options: {copyright, pattern}}) { +export default async function({workspace, changedProjectResourcePaths, options: {copyright, pattern}}) { if (!copyright) { - return Promise.resolve(); + return; } // Replace optional placeholder ${currentYear} with the current year copyright = copyright.replace(/(?:\$\{currentYear\})/, new Date().getFullYear()); - return workspace.byGlob(pattern) - .then((processedResources) => { - return stringReplacer({ - resources: processedResources, - options: { - pattern: /(?:\$\{copyright\}|@copyright@)/g, - replacement: copyright - } - }); - }) - .then((processedResources) => { - return Promise.all(processedResources.map((resource) => { - return workspace.write(resource); - })); - }); + let resources; + if (changedProjectResourcePaths) { + resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource))); + } else { + resources = await workspace.byGlob(pattern); + } + + const processedResources = await stringReplacer({ + resources, + options: { + pattern: /(?:\$\{copyright\}|@copyright@)/g, + replacement: copyright + } + }); + return Promise.all(processedResources.map((resource) => { + if (resource) { + return workspace.write(resource); + } + })); } diff --git a/packages/builder/lib/tasks/replaceVersion.js b/packages/builder/lib/tasks/replaceVersion.js index 699a6221a95..d30b0839dc6 100644 --- a/packages/builder/lib/tasks/replaceVersion.js +++ b/packages/builder/lib/tasks/replaceVersion.js @@ -14,25 +14,30 @@ import stringReplacer from "../processors/stringReplacer.js"; * * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project. + * This is only set if a cache is used and changes have been detected. * @param {object} parameters.options Options * @param {string} parameters.options.pattern Pattern to locate the files to be processed * @param {string} parameters.options.version Replacement version * @returns {Promise} Promise resolving with undefined once data has been written */ -export default function({workspace, options: {pattern, version}}) { - return workspace.byGlob(pattern) - .then((allResources) => { - return stringReplacer({ - resources: allResources, - options: { - pattern: /\$\{(?:project\.)?version\}/g, - replacement: version - } - }); - }) - .then((processedResources) => { - return Promise.all(processedResources.map((resource) => { - return workspace.write(resource); - })); - }); +export default async function({workspace, changedProjectResourcePaths, options: {pattern, version}}) { + let resources; + if (changedProjectResourcePaths) { + resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource))); + } else { + resources = await workspace.byGlob(pattern); + } + const processedResources = await stringReplacer({ + resources, + options: { + pattern: /\$\{(?:project\.)?version\}/g, + replacement: version + } + }); + await Promise.all(processedResources.map((resource) => { + if (resource) { + return workspace.write(resource); + } + })); } diff --git a/packages/builder/test/utils/fshelper.js b/packages/builder/test/utils/fshelper.js index 25e74974b79..9d069654a92 100644 --- a/packages/builder/test/utils/fshelper.js +++ b/packages/builder/test/utils/fshelper.js @@ -11,8 +11,8 @@ export async function readFileContent(filePath) { } export async function directoryDeepEqual(t, destPath, expectedPath) { - const dest = await readdir(destPath, {recursive: true}); - const expected = await readdir(expectedPath, {recursive: true}); + const dest = (await readdir(destPath, {recursive: true})).sort(); + const expected = (await readdir(expectedPath, {recursive: true})).sort(); t.deepEqual(dest, expected); } diff --git a/packages/cli/lib/cli/commands/build.js b/packages/cli/lib/cli/commands/build.js index df93ac5a12e..788b5069581 100644 --- a/packages/cli/lib/cli/commands/build.js +++ b/packages/cli/lib/cli/commands/build.js @@ -1,4 +1,6 @@ import baseMiddleware from "../middlewares/base.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("cli:commands:build"); const build = { command: "build", @@ -106,13 +108,31 @@ build.builder = function(cli) { type: "string" }) .option("cache-mode", { + describe: + "As of UI5 CLI version 5, renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior.", + type: "string", + hidden: true, // Hides it from the help output + }) + .option("snapshot-cache", { describe: "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + "does not create any requests. 'Off' invalidates any existing cache and updates from the repository", type: "string", default: "Default", - choices: ["Default", "Force", "Off"] + choices: ["Default", "Force", "Off"], + }) + .option("cache", { + describe: + "Cache mode to use for building UI5 projects. " + + "The 'Default' behavior is to always use the cache if available. 'Force' uses the cache only " + + "(if it's unavailable or invalid, the build fails). 'ReadOnly' does not create or update any " + + "cache but makes use of a cache if available. 'Off' does not use any cache and always triggers " + + "a rebuild of the project", + type: "string", + default: "Default", + choices: ["Default", "Force", "ReadOnly", "Off"], }) .option("experimental-css-variables", { describe: @@ -149,6 +169,12 @@ build.builder = function(cli) { }; async function handleBuild(argv) { + // Log warning for hidden CLI options + if (Object.prototype.hasOwnProperty.call(argv, "cacheMode")) { + log.warn("As of UI5 CLI version 5, '--cache-mode' was renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior. "+ + "Setting '--snapshot-cache' to 'Default'..."); + } const {graphFromStaticFile, graphFromPackageDependencies} = await import("@ui5/project/graph"); const command = argv._[argv._.length - 1]; @@ -159,13 +185,13 @@ async function handleBuild(argv) { filePath: argv.dependencyDefinition, rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, }); } else { graph = await graphFromPackageDependencies({ rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, workspaceConfigPath: argv.workspaceConfig, workspaceName: argv.workspace === false ? null : argv.workspace, }); @@ -194,6 +220,7 @@ async function handleBuild(argv) { excludedTasks: argv["exclude-task"], cssVariables: argv["experimental-css-variables"], outputStyle: argv["output-style"], + cache: argv["cache"], }); } diff --git a/packages/cli/lib/cli/commands/serve.js b/packages/cli/lib/cli/commands/serve.js index 4719e82bf34..c877f0431ac 100644 --- a/packages/cli/lib/cli/commands/serve.js +++ b/packages/cli/lib/cli/commands/serve.js @@ -2,6 +2,8 @@ import path from "node:path"; import os from "node:os"; import chalk from "chalk"; import baseMiddleware from "../middlewares/base.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("cli:commands:serve"); // Serve const serve = { @@ -68,12 +70,30 @@ serve.builder = function(cli) { }) .option("cache-mode", { describe: - "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + - "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + - "does not create any requests. 'Off' invalidates any existing cache and updates from the repository", + "As of UI5 CLI version 5, renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior.", + type: "string", + hidden: true, + }) + .option("snapshot-cache", { + describe: + "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + + "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + + "does not create any requests. 'Off' invalidates any existing cache and updates from the repository", + type: "string", + default: "Default", + choices: ["Default", "Force", "Off"], + }) + .option("cache", { + describe: + "Cache mode to use for building UI5 projects. " + + "The 'Default' behavior is to always use the cache if available. 'Force' uses the cache only " + + "(if it's unavailable or invalid, the build fails). 'Read-only' does not create or update any " + + "cache but makes use of a cache if available. 'Off' does not use any cache and always triggers " + + "a rebuild of the project", type: "string", default: "Default", - choices: ["Default", "Force", "Off"] + choices: ["Default", "Force", "ReadOnly", "Off"], }) .example("ui5 serve", "Start a web server for the current project") .example("ui5 serve --h2", "Enable the HTTP/2 protocol for the web server (requires SSL certificate)") @@ -85,6 +105,11 @@ serve.builder = function(cli) { }; serve.handler = async function(argv) { + if (Object.prototype.hasOwnProperty.call(argv, "cacheMode")) { + log.warn("As of UI5 CLI version 5, '--cache-mode' was renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior. "+ + "Setting '--snapshot-cache' to 'Default'..."); + } const {graphFromStaticFile, graphFromPackageDependencies} = await import("@ui5/project/graph"); const {serve: serverServe} = await import("@ui5/server"); const {getSslCertificate} = await import("@ui5/server/internal/sslUtil"); @@ -95,13 +120,13 @@ serve.handler = async function(argv) { filePath: argv.dependencyDefinition, rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, }); } else { graph = await graphFromPackageDependencies({ rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, workspaceConfigPath: argv.workspaceConfig, workspaceName: argv.workspace === false ? null : argv.workspace, }); @@ -137,7 +162,8 @@ serve.handler = async function(argv) { cert: argv.h2 ? argv.cert : undefined, key: argv.h2 ? argv.key : undefined, sendSAPTargetCSP: !!argv.sapCspPolicies, - serveCSPReports: !!argv.serveCspReports + serveCSPReports: !!argv.serveCspReports, + cache: argv.cache, }; if (serverConfig.h2) { @@ -146,7 +172,10 @@ serve.handler = async function(argv) { serverConfig.cert = cert; } - const {h2, port: actualPort} = await serverServe(graph, serverConfig); + const {promise: pOnError, reject} = Promise.withResolvers(); + const {h2, port: actualPort} = await serverServe(graph, serverConfig, function(err) { + reject(err); + }); const protocol = h2 ? "https" : "http"; let browserUrl = protocol + "://localhost:" + actualPort; @@ -183,6 +212,7 @@ serve.handler = async function(argv) { const {default: open} = await import("open"); open(browserUrl); } + await pOnError; // Await errors that should bubble into the yargs handler }; export default serve; diff --git a/packages/cli/lib/cli/commands/tree.js b/packages/cli/lib/cli/commands/tree.js index e683a72b676..fa2b2db1c01 100644 --- a/packages/cli/lib/cli/commands/tree.js +++ b/packages/cli/lib/cli/commands/tree.js @@ -28,7 +28,13 @@ tree.builder = function(cli) { "Takes the same value as the version part of \"ui5 use\"", type: "string" }) - .option("cache-mode", { + .hide("cache-mode", { + describe: + "As of UI5 CLI version 5, renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior.", + type: "string", + }) + .option("snapshot-cache", { describe: "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + @@ -51,13 +57,13 @@ tree.handler = async function(argv) { graph = await graphFromStaticFile({ filePath: argv.dependencyDefinition, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, }); } else { graph = await graphFromPackageDependencies({ rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, workspaceConfigPath: argv.workspaceConfig, workspaceName: argv.workspace === false ? null : argv.workspace, }); diff --git a/packages/cli/test/lib/cli/commands/build.js b/packages/cli/test/lib/cli/commands/build.js index ac36226cbf8..191bf19d239 100644 --- a/packages/cli/test/lib/cli/commands/build.js +++ b/packages/cli/test/lib/cli/commands/build.js @@ -25,6 +25,8 @@ function getDefaultArgv() { "experimentalCssVariables": false, "cache-mode": "Default", "cacheMode": "Default", + "snapshot-cache": "Default", + "snapshotCache": "Default", "output-style": "Default", "$0": "ui5" }; @@ -131,15 +133,15 @@ test.serial("ui5 build --framework-version", async (t) => { versionOverride: "1.99.0", workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); -test.serial("ui5 build --cache-mode", async (t) => { +test.serial("ui5 build --snapshot-cache", async (t) => { const {build, argv, graphFromPackageDependenciesStub} = t.context; - argv.cacheMode = "Off"; + argv.snapshotCache = "Off"; await build.handler(argv); @@ -150,7 +152,7 @@ test.serial("ui5 build --cache-mode", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Off", + snapshotCache: "Off", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -169,7 +171,7 @@ test.serial("ui5 build --config", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -188,7 +190,7 @@ test.serial("ui5 build --workspace", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: "dolphin", - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -207,7 +209,7 @@ test.serial("ui5 build --no-workspace", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: null, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -227,7 +229,7 @@ test.serial("ui5 build --workspace-config", async (t) => { versionOverride: undefined, workspaceConfigPath: fakePath, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -245,7 +247,7 @@ test.serial("ui5 build --dependency-definition", async (t) => { filePath: "dependencies.yaml", rootConfigPath: undefined, versionOverride: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromStaticFile got called with expected arguments" ); }); @@ -264,7 +266,7 @@ test.serial("ui5 build --dependency-definition --config", async (t) => { filePath: "dependencies.yaml", rootConfigPath: "ui5-test.yaml", versionOverride: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromStaticFile got called with expected arguments" ); }); @@ -284,7 +286,7 @@ test.serial("ui5 build --dependency-definition --config --framework-version", as filePath: "dependencies.yaml", rootConfigPath: "ui5-test.yaml", versionOverride: "1.99.0", - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromStaticFile got called with expected arguments" ); }); diff --git a/packages/cli/test/lib/cli/commands/serve.js b/packages/cli/test/lib/cli/commands/serve.js index 1f20ba09c48..0b23bd0a8d8 100644 --- a/packages/cli/test/lib/cli/commands/serve.js +++ b/packages/cli/test/lib/cli/commands/serve.js @@ -26,6 +26,8 @@ function getDefaultArgv() { "serveCspReports": false, "cache-mode": "Default", "cacheMode": "Default", + "snapshot-cache": "Default", + "snapshotCache": "Default", "$0": "ui5" }; } @@ -93,7 +95,7 @@ test.serial("ui5 serve: default", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -139,7 +141,7 @@ test.serial("ui5 serve --h2", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -181,7 +183,7 @@ test.serial("ui5 serve --accept-remote-connections", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, ` @@ -225,7 +227,7 @@ test.serial("ui5 serve --open", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -266,7 +268,7 @@ test.serial("ui5 serve --open (opens default url)", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -308,7 +310,7 @@ test.serial("ui5 serve --config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: fakePath, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -344,7 +346,7 @@ test.serial("ui5 serve --dependency-definition", async (t) => { t.is(graph.graphFromStaticFile.callCount, 1); t.deepEqual(graph.graphFromStaticFile.getCall(0).args, [{ filePath: fakePath, versionOverride: undefined, - cacheMode: "Default", rootConfigPath: undefined + snapshotCache: "Default", rootConfigPath: undefined }]); t.is(t.context.consoleOutput, `Server started @@ -383,7 +385,7 @@ test.serial("ui5 serve --dependency-definition / --config", async (t) => { t.is(graph.graphFromStaticFile.callCount, 1); t.deepEqual(graph.graphFromStaticFile.getCall(0).args, [{ filePath: fakeDependenciesPath, versionOverride: undefined, - cacheMode: "Default", rootConfigPath: fakeConfigPath + snapshotCache: "Default", rootConfigPath: fakeConfigPath }]); t.is(t.context.consoleOutput, `Server started @@ -419,7 +421,7 @@ test.serial("ui5 serve --framework-version", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: "1.234.5", workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -443,10 +445,10 @@ URL: http://localhost:8080 ]); }); -test.serial("ui5 serve --cache-mode", async (t) => { +test.serial("ui5 serve --snapshotCache", async (t) => { const {argv, serve, graph, server, fakeGraph} = t.context; - argv.cacheMode = "Force"; + argv.snapshotCache = "Force"; await serve.handler(argv); @@ -455,7 +457,7 @@ test.serial("ui5 serve --cache-mode", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Force", + snapshotCache: "Force", }]); t.is(t.context.consoleOutput, `Server started @@ -491,7 +493,7 @@ test.serial("ui5 serve --workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: "dolphin", - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -527,7 +529,7 @@ test.serial("ui5 serve --no-workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: null, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -564,7 +566,7 @@ test.serial("ui5 serve --workspace-config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: fakePath, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -600,7 +602,7 @@ test.serial("ui5 serve --sap-csp-policies", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -636,7 +638,7 @@ test.serial("ui5 serve --serve-csp-reports", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -672,7 +674,7 @@ test.serial("ui5 serve --simple-index", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -715,7 +717,7 @@ test.serial("ui5 serve with ui5.yaml port setting", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -765,7 +767,7 @@ test.serial("ui5 serve --h2 with ui5.yaml port setting", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -822,7 +824,7 @@ test.serial("ui5 serve --h2 with ui5.yaml port setting and port CLI argument", a t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started diff --git a/packages/cli/test/lib/cli/commands/tree.js b/packages/cli/test/lib/cli/commands/tree.js index f8e8fcad689..9d50acffb01 100644 --- a/packages/cli/test/lib/cli/commands/tree.js +++ b/packages/cli/test/lib/cli/commands/tree.js @@ -15,6 +15,8 @@ function getDefaultArgv() { "silent": false, "cache-mode": "Default", "cacheMode": "Default", + "snapshot-cache": "Default", + "snapshotCache": "Default", "flat": false, "level": undefined, "$0": "ui5" @@ -78,7 +80,7 @@ test.serial("ui5 tree (Without dependencies)", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -148,7 +150,7 @@ test.serial("ui5 tree", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -228,7 +230,7 @@ test.serial("ui5 tree --flat", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -305,7 +307,7 @@ test.serial("ui5 tree --level 1", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -395,7 +397,7 @@ test.serial("ui5 tree (With extensions)", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -441,7 +443,7 @@ test.serial("ui5 tree --perf", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -482,7 +484,7 @@ test.serial("ui5 tree --framework-version", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: "1.234.5", workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -495,10 +497,10 @@ ${chalk.italic("None")} `); }); -test.serial("ui5 tree --cache-mode", async (t) => { +test.serial("ui5 tree --snapshot-cache", async (t) => { const {argv, tree, traverseBreadthFirst, graph} = t.context; - argv.cacheMode = "Force"; + argv.snapshotCache = "Force"; traverseBreadthFirst.callsFake(async (fn) => { await fn({ @@ -521,7 +523,7 @@ test.serial("ui5 tree --cache-mode", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Force", + snapshotCache: "Force", }]); t.is(t.context.consoleOutput, @@ -561,7 +563,7 @@ test.serial("ui5 tree --config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: fakePath, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -600,7 +602,7 @@ test.serial("ui5 tree --workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: "dolphin", - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -639,7 +641,7 @@ test.serial("ui5 tree --no-workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: null, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -679,7 +681,7 @@ test.serial("ui5 tree --workspace-config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: fakePath, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -717,7 +719,7 @@ test.serial("ui5 tree --dependency-definition", async (t) => { t.is(graph.graphFromPackageDependencies.callCount, 0); t.is(graph.graphFromStaticFile.callCount, 1); t.deepEqual(graph.graphFromStaticFile.getCall(0).args, [{ - filePath: fakePath, versionOverride: undefined, cacheMode: "Default" + filePath: fakePath, versionOverride: undefined, snapshotCache: "Default" }]); t.is(t.context.consoleOutput, diff --git a/packages/fs/lib/MonitoredReader.js b/packages/fs/lib/MonitoredReader.js new file mode 100644 index 00000000000..820b75169fe --- /dev/null +++ b/packages/fs/lib/MonitoredReader.js @@ -0,0 +1,49 @@ +import AbstractReader from "./AbstractReader.js"; + +export default class MonitoredReader extends AbstractReader { + #reader; + #sealed = false; + #paths = []; + #patterns = []; + + constructor(reader) { + super(reader.getName()); + this.#reader = reader; + } + + getResourceRequests() { + this.#sealed = true; + return { + paths: this.#paths, + patterns: this.#patterns, + }; + } + + async _byGlob(virPattern, options, trace) { + if (this.#sealed) { + throw new Error(`Unexpected read operation after reader has been sealed`); + } + if (this.#reader.resolvePattern) { + const resolvedPattern = this.#reader.resolvePattern(virPattern); + this.#patterns.push(resolvedPattern); + } else { + this.#patterns.push(virPattern); + } + return await this.#reader._byGlob(virPattern, options, trace); + } + + async _byPath(virPath, options, trace) { + if (this.#sealed) { + throw new Error(`Unexpected read operation after reader has been sealed`); + } + if (this.#reader.resolvePath) { + const resolvedPath = this.#reader.resolvePath(virPath); + if (resolvedPath) { + this.#paths.push(resolvedPath); + } + } else { + this.#paths.push(virPath); + } + return await this.#reader._byPath(virPath, options, trace); + } +} diff --git a/packages/fs/lib/MonitoredReaderWriter.js b/packages/fs/lib/MonitoredReaderWriter.js new file mode 100644 index 00000000000..0472fb9b610 --- /dev/null +++ b/packages/fs/lib/MonitoredReaderWriter.js @@ -0,0 +1,53 @@ +import AbstractReaderWriter from "./AbstractReaderWriter.js"; + +export default class MonitoredReaderWriter extends AbstractReaderWriter { + #readerWriter; + #sealed = false; + #paths = new Set(); + #patterns = new Set(); + + constructor(readerWriter) { + super(readerWriter.getName()); + this.#readerWriter = readerWriter; + } + + getResourceRequests() { + this.#sealed = true; + return { + paths: this.#paths, + patterns: this.#patterns, + }; + } + + async _byGlob(virPattern, options, trace) { + if (this.#sealed) { + throw new Error(`Unexpected read operation after reader has been sealed`); + } + if (this.#readerWriter.resolvePattern) { + const resolvedPattern = this.#readerWriter.resolvePattern(virPattern); + this.#patterns.add(resolvedPattern); + } else { + this.#patterns.add(virPattern); + } + return await this.#readerWriter._byGlob(virPattern, options, trace); + } + + async _byPath(virPath, options, trace) { + if (this.#sealed) { + throw new Error(`Unexpected read operation after reader has been sealed`); + } + if (this.#readerWriter.resolvePath) { + const resolvedPath = this.#readerWriter.resolvePath(virPath); + if (resolvedPath) { + this.#paths.add(resolvedPath); + } + } else { + this.#paths.add(virPath); + } + return await this.#readerWriter._byPath(virPath, options, trace); + } + + async _write(resource, options) { + return this.#readerWriter.write(resource, options); + } +} diff --git a/packages/fs/lib/MonitoredResourceTagCollection.js b/packages/fs/lib/MonitoredResourceTagCollection.js new file mode 100644 index 00000000000..f23388e53d6 --- /dev/null +++ b/packages/fs/lib/MonitoredResourceTagCollection.js @@ -0,0 +1,83 @@ +/** + * Proxy of ResourceTagCollection + * + * @class + * @alias @ui5/fs/internal/MonitoredTagCollection + */ +class MonitoredTagCollection { + #tagCollection; + #previousTagCollecction; + #tagOperations = new Map(); // resourcePath -> Map + + /** + * Constructor + * + * @param {object} tagCollection The ResourceTagCollection instance to wrap + */ + constructor(tagCollection) { + this.#tagCollection = tagCollection; + this.#previousTagCollecction = tagCollection.clone(); + } + + /** + * Returns tags created or cleared via this MonitoredTagCollection during the execution of a task + * + * @returns {Map>} + * Map of resource paths to their tags that were set or cleared during this task's execution + */ + getTagOperations() { + return this.#tagOperations; + } + + /** + * Set a tag on a resource and track the operation + * + * @param {string|object} resourcePathOrResource Path of the resource or a resource instance + * @param {string} tag Tag in the format "namespace:Name" + * @param {string|number|boolean} [value=true] Tag value + */ + setTag(resourcePathOrResource, tag, value = true) { + this.#tagCollection.setTag(resourcePathOrResource, tag, value); + const resourcePath = this.#tagCollection._getPath(resourcePathOrResource); + + // Track tags set during this task's execution + if (!this.#tagOperations.has(resourcePath)) { + this.#tagOperations.set(resourcePath, new Map()); + } + this.#tagOperations.get(resourcePath).set(tag, value); + } + + /** + * Get a tag value from a resource + * + * @param {string|object} resourcePathOrResource Path of the resource or a resource instance + * @param {string} tag Tag in the format "namespace:Name" + * @returns {string|number|boolean|undefined} Tag value or undefined if not set + */ + getTag(resourcePathOrResource, tag) { + return this.#tagCollection.getTag(resourcePathOrResource, tag); + } + + getAllTagsForResource(resourcePath) { + return this.#previousTagCollecction.getAllTagsForResource(resourcePath); + } + + /** + * Clear a tag from a resource and track the operation + * + * @param {string|object} resourcePathOrResource Path of the resource or a resource instance + * @param {string} tag Tag in the format "namespace:Name" + */ + clearTag(resourcePathOrResource, tag) { + this.#tagCollection.clearTag(resourcePathOrResource, tag); + const resourcePath = this.#tagCollection._getPath(resourcePathOrResource); + + // Track cleared tags during this task's execution + const resourceTags = this.#tagOperations.get(resourcePath); + if (resourceTags) { + resourceTags.set(tag, undefined); + } + } +} + +export default MonitoredTagCollection; diff --git a/packages/fs/lib/ReaderCollectionPrioritized.js b/packages/fs/lib/ReaderCollectionPrioritized.js index 680b71357ca..8f235f22148 100644 --- a/packages/fs/lib/ReaderCollectionPrioritized.js +++ b/packages/fs/lib/ReaderCollectionPrioritized.js @@ -68,7 +68,7 @@ class ReaderCollectionPrioritized extends AbstractReader { * @returns {Promise<@ui5/fs/Resource|null>} * Promise resolving to a single resource or null if no resource is found */ - _byPath(virPath, options, trace) { + async _byPath(virPath, options, trace) { const that = this; const byPath = (i) => { if (i > this._readers.length - 1) { diff --git a/packages/fs/lib/Resource.js b/packages/fs/lib/Resource.js index c43edc2716f..07d1f7b67f1 100644 --- a/packages/fs/lib/Resource.js +++ b/packages/fs/lib/Resource.js @@ -1,11 +1,28 @@ -import stream from "node:stream"; +import {Readable} from "node:stream"; +import {buffer as streamToBuffer} from "node:stream/consumers"; +import ssri from "ssri"; import clone from "clone"; import posixPath from "node:path/posix"; +import {setTimeout} from "node:timers/promises"; +import {Mutex} from "async-mutex"; +import {getLogger} from "@ui5/logger"; + +const log = getLogger("fs:Resource"); +let deprecatedGetStreamCalled = false; +let deprecatedGetStatInfoCalled = false; -const fnTrue = () => true; -const fnFalse = () => false; const ALLOWED_SOURCE_METADATA_KEYS = ["adapter", "fsPath", "contentModified"]; +const CONTENT_TYPES = { + BUFFER: "buffer", + STREAM: "stream", + FACTORY: "factory", + DRAINED_STREAM: "drainedStream", + IN_TRANSFORMATION: "inTransformation", +}; + +const SSRI_OPTIONS = {algorithms: ["sha256"]}; + /** * Resource. UI5 CLI specific representation of a file's content and metadata * @@ -14,26 +31,39 @@ const ALLOWED_SOURCE_METADATA_KEYS = ["adapter", "fsPath", "contentModified"]; * @alias @ui5/fs/Resource */ class Resource { - #project; - #buffer; - #buffering; - #collections; - #contentDrained; - #createStream; #name; #path; + #project; #sourceMetadata; + + /* Resource Content */ + #content; + #createBufferFactory; + #createStreamFactory; + #contentType; + + /* Content Metadata */ + #byteSize; + #lastModified; #statInfo; - #stream; - #streamDrained; - #isModified; + #isDirectory; + #integrity; + #inode; + + /* States */ + #isModified = false; + // Mutex to prevent access/modification while content is being transformed + #contentMutex = new Mutex(); + + // Tracing + #collections = []; /** - * Function for dynamic creation of content streams + * Factory function to dynamic (and potentially repeated) creation of readable streams of the resource's content * * @public * @callback @ui5/fs/Resource~createStream - * @returns {stream.Readable} A readable stream of a resources content + * @returns {stream.Readable} A readable stream of the resource's content */ /** @@ -49,6 +79,9 @@ class Resource { * (cannot be used in conjunction with parameters buffer, stream or createStream) * @param {Stream} [parameters.stream] Readable stream of the content of this resource * (cannot be used in conjunction with parameters buffer, string or createStream) + * @param {@ui5/fs/Resource~createBuffer} [parameters.createBuffer] Function callback that returns a promise + * resolving with a Buffer of the content of this resource (cannot be used in conjunction with + * parameters buffer, string or stream). Must be used in conjunction with parameters createStream. * @param {@ui5/fs/Resource~createStream} [parameters.createStream] Function callback that returns a readable * stream of the content of this resource (cannot be used in conjunction with parameters buffer, * string or stream). @@ -57,20 +90,34 @@ class Resource { * @param {object} [parameters.sourceMetadata] Source metadata for UI5 CLI internal use. * Some information may be set by an adapter to store information for later retrieval. Also keeps track of whether * a resource content has been modified since it has been read from a source + * @param {boolean} [parameters.isDirectory] Flag whether the resource represents a directory + * @param {number} [parameters.byteSize] Size of the resource content in bytes + * @param {number} [parameters.lastModified] Last modified timestamp (in milliseconds since UNIX epoch) + * @param {string} [parameters.integrity] Integrity hash of the resource content + * @param {number} [parameters.inode] Inode number of the resource */ - constructor({path, statInfo, buffer, string, createStream, stream, project, sourceMetadata}) { + constructor({ + path, statInfo, buffer, createBuffer, string, createStream, stream, project, sourceMetadata, + isDirectory, byteSize, lastModified, integrity, inode, + }) { if (!path) { throw new Error("Unable to create Resource: Missing parameter 'path'"); } + if (createBuffer && !createStream) { + // If createBuffer is provided, createStream must be provided as well + throw new Error("Unable to create Resource: Parameter 'createStream' must be provided when " + + "parameter 'createBuffer' is used"); + } if (buffer && createStream || buffer && string || string && createStream || buffer && stream || string && stream || createStream && stream) { - throw new Error("Unable to create Resource: Please set only one content parameter. " + + throw new Error("Unable to create Resource: Multiple content parameters provided. " + + "Please provide only one of the following parameters: " + "'buffer', 'string', 'stream' or 'createStream'"); } if (sourceMetadata) { if (typeof sourceMetadata !== "object") { - throw new Error(`Parameter 'sourceMetadata' must be of type "object"`); + throw new Error(`Unable to create Resource: Parameter 'sourceMetadata' must be of type "object"`); } /* eslint-disable-next-line guard-for-in */ @@ -96,42 +143,91 @@ class Resource { // Since the sourceMetadata object is inherited to clones, it is the only correct indicator this.#sourceMetadata.contentModified ??= false; - this.#isModified = false; - this.#project = project; - - this.#statInfo = statInfo || { // TODO - isFile: fnTrue, - isDirectory: fnFalse, - isBlockDevice: fnFalse, - isCharacterDevice: fnFalse, - isSymbolicLink: fnFalse, - isFIFO: fnFalse, - isSocket: fnFalse, - atimeMs: new Date().getTime(), - mtimeMs: new Date().getTime(), - ctimeMs: new Date().getTime(), - birthtimeMs: new Date().getTime(), - atime: new Date(), - mtime: new Date(), - ctime: new Date(), - birthtime: new Date() - }; + this.#integrity = integrity; if (createStream) { - this.#createStream = createStream; - } else if (stream) { - this.#stream = stream; + // We store both factories individually + // This allows to create either a stream or a buffer on demand + // Note that it's possible and acceptable if only one factory is provided + if (createBuffer) { + if (typeof createBuffer !== "function") { + throw new Error("Unable to create Resource: Parameter 'createBuffer' must be a function"); + } + this.#createBufferFactory = createBuffer; + } + // createStream is always provided if a factory is used + if (typeof createStream !== "function") { + throw new Error("Unable to create Resource: Parameter 'createStream' must be a function"); + } + this.#createStreamFactory = createStream; + this.#contentType = CONTENT_TYPES.FACTORY; + } if (stream) { + if (typeof stream !== "object" || typeof stream.pipe !== "function") { + throw new Error("Unable to create Resource: Parameter 'stream' must be a readable stream"); + } + this.#content = stream; + this.#contentType = CONTENT_TYPES.STREAM; } else if (buffer) { - // Use private setter, not to accidentally set any modified flags - this.#setBuffer(buffer); - } else if (typeof string === "string" || string instanceof String) { - // Use private setter, not to accidentally set any modified flags - this.#setBuffer(Buffer.from(string, "utf8")); + if (!Buffer.isBuffer(buffer)) { + throw new Error("Unable to create Resource: Parameter 'buffer' must be of type Buffer"); + } + this.#content = buffer; + this.#contentType = CONTENT_TYPES.BUFFER; + } else if (string !== undefined) { + if (typeof string !== "string" && !(string instanceof String)) { + throw new Error("Unable to create Resource: Parameter 'string' must be of type string"); + } + this.#content = Buffer.from(string, "utf8"); // Assuming utf8 encoding + this.#contentType = CONTENT_TYPES.BUFFER; + } + + if (isDirectory !== undefined) { + this.#isDirectory = !!isDirectory; + } + if (byteSize !== undefined) { + if (typeof byteSize !== "number" || byteSize < 0) { + throw new Error("Unable to create Resource: Parameter 'byteSize' must be a positive number"); + } + this.#byteSize = byteSize; + } + if (lastModified !== undefined) { + if (typeof lastModified !== "number" || lastModified < 0) { + throw new Error("Unable to create Resource: Parameter 'lastModified' must be a positive number"); + } + this.#lastModified = lastModified; } - // Tracing: - this.#collections = []; + if (inode !== undefined) { + if (typeof inode !== "number" || inode < 0) { + throw new Error("Unable to create Resource: Parameter 'inode' must be a positive number"); + } + this.#inode = inode; + } + + if (statInfo) { + this.#isDirectory ??= statInfo.isDirectory(); + if (!this.#isDirectory && statInfo.isFile && !statInfo.isFile()) { + throw new Error("Unable to create Resource: statInfo must represent either a file or a directory"); + } + this.#byteSize ??= statInfo.size; + this.#lastModified ??= statInfo.mtimeMs; + this.#inode ??= statInfo.ino; + + // Create legacy statInfo object + this.#statInfo = parseStat(statInfo); + } else { + // if (this.#byteSize === undefined && this.#contentType) { + // if (this.#contentType !== CONTENT_TYPES.BUFFER) { + // throw new Error("Unable to create Resource: byteSize or statInfo must be provided when resource " + + // "content is stream- or factory-based"); + // } + // this.#byteSize ??= this.#content.byteLength; + // } + + // Create legacy statInfo object + this.#statInfo = createStat(this.#byteSize, this.#isDirectory, this.#lastModified); + } } /** @@ -141,20 +237,56 @@ class Resource { * @returns {Promise} Promise resolving with a buffer of the resource content. */ async getBuffer() { - if (this.#contentDrained) { - throw new Error(`Content of Resource ${this.#path} has been drained. ` + - "This might be caused by requesting resource content after a content stream has been " + - "requested and no new content (e.g. a new stream) has been set."); - } - if (this.#buffer) { - return this.#buffer; - } else if (this.#createStream || this.#stream) { - return this.#getBufferFromStream(); - } else { + // First wait for new content if the current content is flagged as drained + if (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + await this.#waitForNewContent(); + } + + // Then make sure no other operation is currently modifying the content (such as a concurrent getBuffer call + // that might be transforming the content right now) + if (this.#contentMutex.isLocked()) { + await this.#contentMutex.waitForUnlock(); + } + + switch (this.#contentType) { + case CONTENT_TYPES.FACTORY: + if (this.#createBufferFactory) { + // Prefer buffer factory if available + return await this.#getBufferFromFactory(this.#createBufferFactory); + } + // Fallback to stream factory + return this.#getBufferFromStream(this.#createStreamFactory()); + case CONTENT_TYPES.STREAM: + return this.#getBufferFromStream(this.#content); + case CONTENT_TYPES.BUFFER: + return this.#content; + case CONTENT_TYPES.DRAINED_STREAM: + // waitForNewContent call above should prevent this from ever happening + throw new Error(`Unexpected error: Content of Resource ${this.#path} is flagged as drained.`); + case CONTENT_TYPES.IN_TRANSFORMATION: + // contentMutex.waitForUnlock call above should prevent this from ever happening + throw new Error(`Unexpected error: Content of Resource ${this.#path} is currently being transformed`); + default: throw new Error(`Resource ${this.#path} has no content`); } } + async #getBufferFromFactory(factoryFn) { + const release = await this.#contentMutex.acquire(); + try { + this.#contentType = CONTENT_TYPES.IN_TRANSFORMATION; + const buffer = await factoryFn(); + if (!Buffer.isBuffer(buffer)) { + throw new Error(`Buffer factory of Resource ${this.#path} did not return a Buffer instance`); + } + this.#content = buffer; + this.#contentType = CONTENT_TYPES.BUFFER; + return buffer; + } finally { + release(); + } + } + /** * Sets a Buffer as content. * @@ -162,20 +294,12 @@ class Resource { * @param {Buffer} buffer Buffer instance */ setBuffer(buffer) { - this.#sourceMetadata.contentModified = true; - this.#isModified = true; - this.#setBuffer(buffer); - } - - #setBuffer(buffer) { - this.#createStream = null; - // if (this.#stream) { // TODO this may cause strange issues - // this.#stream.destroy(); - // } - this.#stream = null; - this.#buffer = buffer; - this.#contentDrained = false; - this.#streamDrained = false; + if (this.#contentMutex.isLocked()) { + throw new Error(`Unable to set buffer: Content of Resource ${this.#path} is currently being transformed`); + } + this.#content = buffer; + this.#contentType = CONTENT_TYPES.BUFFER; + this.#contendModified(); } /** @@ -184,13 +308,9 @@ class Resource { * @public * @returns {Promise} Promise resolving with the resource content. */ - getString() { - if (this.#contentDrained) { - return Promise.reject(new Error(`Content of Resource ${this.#path} has been drained. ` + - "This might be caused by requesting resource content after a content stream has been " + - "requested and no new content (e.g. a new stream) has been set.")); - } - return this.getBuffer().then((buffer) => buffer.toString()); + async getString() { + const buff = await this.getBuffer(); + return buff.toString("utf8"); } /** @@ -208,29 +328,130 @@ class Resource { * * Repetitive calls of this function are only possible if new content has been set in the meantime (through * [setStream]{@link @ui5/fs/Resource#setStream}, [setBuffer]{@link @ui5/fs/Resource#setBuffer} - * or [setString]{@link @ui5/fs/Resource#setString}). This - * is to prevent consumers from accessing drained streams. + * or [setString]{@link @ui5/fs/Resource#setString}). + * This is to prevent subsequent consumers from accessing drained streams. + * + * This method is deprecated. Please use the asynchronous version + * [getStreamAsync]{@link @ui5/fs/Resource#getStreamAsync} instead. + * + * For atomic operations, consider using [modifyStream]{@link @ui5/fs/Resource#modifyStream}. * * @public + * @deprecated Use asynchronous Resource.getStreamAsync() instead * @returns {stream.Readable} Readable stream for the resource content. */ getStream() { - if (this.#contentDrained) { - throw new Error(`Content of Resource ${this.#path} has been drained. ` + - "This might be caused by requesting resource content after a content stream has been " + - "requested and no new content (e.g. a new stream) has been set."); - } - let contentStream; - if (this.#buffer) { - const bufferStream = new stream.PassThrough(); - bufferStream.end(this.#buffer); - contentStream = bufferStream; - } else if (this.#createStream || this.#stream) { - contentStream = this.#getStream(); - } - if (!contentStream) { + if (!deprecatedGetStreamCalled) { + log.verbose(`[DEPRECATION] Synchronous Resource.getStream() is deprecated and will be removed ` + + `in future versions. Please use asynchronous Resource.getStreamAsync() instead.`); + deprecatedGetStreamCalled = true; + } + + // First check for drained content + if (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + throw new Error(`Content of Resource ${this.#path} is currently flagged as drained. ` + + `Consider using Resource.getStreamAsync() to wait for new content.`); + } + + // Make sure no other operation is currently modifying the content + if (this.#contentMutex.isLocked()) { + throw new Error(`Content of Resource ${this.#path} is currently being transformed. ` + + `Consider using Resource.getStreamAsync() to wait for the transformation to finish.`); + } + + return this.#getStream(); + } + + /** + * Gets a readable stream for the resource content. + * + * Repetitive calls of this function are only possible if new content has been set in the meantime (through + * [setStream]{@link @ui5/fs/Resource#setStream}, [setBuffer]{@link @ui5/fs/Resource#setBuffer} + * or [setString]{@link @ui5/fs/Resource#setString}). + * This is to prevent subsequent consumers from accessing drained streams. + * + * For atomic operations, consider using [modifyStream]{@link @ui5/fs/Resource#modifyStream}. + * + * @public + * @returns {Promise} Promise resolving with a readable stream for the resource content. + */ + async getStreamAsync() { + // First wait for new content if the current content is flagged as drained + if (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + await this.#waitForNewContent(); + } + + // Then make sure no other operation is currently modifying the content + if (this.#contentMutex.isLocked()) { + await this.#contentMutex.waitForUnlock(); + } + + return this.#getStream(); + } + + /** + * Modifies the resource content by applying the given callback function. + * The callback function receives a readable stream of the current content + * and must return either a Buffer or a readable stream with the new content. + * The resource content is locked during the modification to prevent concurrent access. + * + * @param {function(stream.Readable): (Buffer|stream.Readable|Promise)} callback + */ + async modifyStream(callback) { + // First wait for new content if the current content is flagged as drained + if (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + await this.#waitForNewContent(); + } + // Then make sure no other operation is currently modifying the content and then lock it + const release = await this.#contentMutex.acquire(); + try { + const newContent = await callback(this.#getStream()); + + // New content is either buffer or stream + if (Buffer.isBuffer(newContent)) { + this.#content = newContent; + this.#contentType = CONTENT_TYPES.BUFFER; + } else if (typeof newContent === "object" && typeof newContent.pipe === "function") { + this.#content = newContent; + this.#contentType = CONTENT_TYPES.STREAM; + } else { + throw new Error("Unable to set new content: Content must be either a Buffer or a Readable Stream"); + } + this.#contendModified(); + } finally { + release(); + } + } + + /** + * Returns the content as stream. + * + * @private + * @returns {stream.Readable} Readable stream + */ + #getStream() { + let stream; + switch (this.#contentType) { + case CONTENT_TYPES.BUFFER: + stream = Readable.from(this.#content); + break; + case CONTENT_TYPES.FACTORY: + // Prefer stream factory (which must always be set if content type is FACTORY) + stream = this.#createStreamFactory(); + break; + case CONTENT_TYPES.STREAM: + stream = this.#content; + break; + case CONTENT_TYPES.DRAINED_STREAM: + // This case is unexpected as callers should already handle this content type (by waiting for it to change) + throw new Error(`Unexpected error: Content of Resource ${this.#path} is flagged as drained.`); + case CONTENT_TYPES.IN_TRANSFORMATION: + // This case is unexpected as callers should already handle this content type (by waiting for it to change) + throw new Error(`Unexpected error: Content of Resource ${this.#path} is currently being transformed`); + default: throw new Error(`Resource ${this.#path} has no content`); } + // If a stream instance is being returned, it will typically get drained be the consumer. // In that case, further content access will result in a "Content stream has been drained" error. // However, depending on the execution environment, a resources content stream might have been @@ -239,8 +460,56 @@ class Resource { // To prevent unexpected "Content stream has been drained" errors caused by changing environments, we flag // the resource content as "drained" every time a stream is requested. Even if actually a buffer or // createStream callback is being used. - this.#contentDrained = true; - return contentStream; + this.#contentType = CONTENT_TYPES.DRAINED_STREAM; + return stream; + } + + /** + * Converts the buffer into a stream. + * + * @private + * @param {stream.Readable} stream Readable stream + * @returns {Promise} Promise resolving with buffer. + */ + async #getBufferFromStream(stream) { + const release = await this.#contentMutex.acquire(); + try { + this.#contentType = CONTENT_TYPES.IN_TRANSFORMATION; + if (this.hasSize()) { + // If size is known. preallocate buffer for improved performance + try { + const size = await this.getSize(); + const buffer = Buffer.allocUnsafe(size); + let offset = 0; + for await (const chunk of stream) { + const len = chunk.length; + if (offset + len > size) { + throw new Error(`Stream exceeded expected size: ${size}, got at least ${offset + len}`); + } + chunk.copy(buffer, offset); + offset += len; + } + if (offset !== size) { + throw new Error(`Stream ended early: expected ${size} bytes, got ${offset}`); + } + this.#content = buffer; + } catch (err) { + // Ensure the stream is cleaned up on error + if (!stream.destroyed) { + stream.destroy(err); + } + throw err; + } + } else { + // Is size is unknown, simply use utility consumer from Node.js webstreams + // See https://nodejs.org/api/webstreams.html#utility-consumers + this.#content = await streamToBuffer(stream); + } + this.#contentType = CONTENT_TYPES.BUFFER; + } finally { + release(); + } + return this.#content; } /** @@ -251,22 +520,110 @@ class Resource { callback for dynamic creation of a readable stream */ setStream(stream) { - this.#isModified = true; + if (this.#contentMutex.isLocked()) { + throw new Error(`Unable to set stream: Content of Resource ${this.#path} is currently being transformed`); + } + if (typeof stream === "function") { + this.#content = undefined; + this.#createStreamFactory = stream; + this.#contentType = CONTENT_TYPES.FACTORY; + } else { + this.#content = stream; + this.#contentType = CONTENT_TYPES.STREAM; + } + this.#contendModified(); + } + + async getIntegrity() { + if (this.#integrity) { + return this.#integrity; + } + if (this.isDirectory()) { + throw new Error(`Unable to calculate integrity for directory resource: ${this.#path}`); + } + + // First wait for new content if the current content is flagged as drained + if (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + await this.#waitForNewContent(); + } + + // Then make sure no other operation is currently modifying the content + if (this.#contentMutex.isLocked()) { + await this.#contentMutex.waitForUnlock(); + } + + switch (this.#contentType) { + case CONTENT_TYPES.BUFFER: + this.#integrity = ssri.fromData(this.#content, SSRI_OPTIONS).toString(); + break; + case CONTENT_TYPES.FACTORY: + // TODO: Investigate performance impact of buffer factory vs. stream factory for integrity calculation + // if (this.#createBufferFactory) { + // this.#integrity = ssri.fromData( + // await this.#getBufferFromFactory(this.#createBufferFactory, SSRI_OPTIONS).toString()); + // } else { + this.#integrity = (await ssri.fromStream(this.#createStreamFactory(), SSRI_OPTIONS)).toString(); + // } + break; + case CONTENT_TYPES.STREAM: + // To be discussed: Should we read the stream into a buffer here (using #getBufferFromStream) to avoid + // draining it? + this.#integrity = ssri.fromData(await this.#getBufferFromStream(this.#content), SSRI_OPTIONS).toString(); + break; + case CONTENT_TYPES.DRAINED_STREAM: + throw new Error(`Unexpected error: Content of Resource ${this.#path} is flagged as drained.`); + case CONTENT_TYPES.IN_TRANSFORMATION: + throw new Error(`Unexpected error: Content of Resource ${this.#path} is currently being transformed`); + default: + throw new Error(`Resource ${this.#path} has no content`); + } + return this.#integrity; + } + + #contendModified() { this.#sourceMetadata.contentModified = true; + this.#isModified = true; - this.#buffer = null; - // if (this.#stream) { // TODO this may cause strange issues - // this.#stream.destroy(); - // } - if (typeof stream === "function") { - this.#createStream = stream; - this.#stream = null; + this.#byteSize = undefined; + this.#integrity = undefined; + this.#lastModified = new Date().getTime(); // TODO: Always update or keep initial value (= fs stat)? + + if (this.#contentType === CONTENT_TYPES.BUFFER) { + this.#byteSize = this.#content.byteLength; + this.#updateStatInfo(this.#byteSize); } else { - this.#stream = stream; - this.#createStream = null; + this.#byteSize = undefined; + // Stat-info can't be updated based on streams or factory functions } - this.#contentDrained = false; - this.#streamDrained = false; + } + + /** + * In case the resource content is flagged as drained stream, wait for new content to be set. + * Either resolves once the content type is no longer DRAINED_STREAM, or rejects with a timeout error. + */ + async #waitForNewContent() { + if (this.#contentType !== CONTENT_TYPES.DRAINED_STREAM) { + return; + } + // Stream might currently be processed by another consumer. Try again after a short wait, hoping the + // other consumer has processing it and has set new content + let timeoutCounter = 0; + log.verbose(`Content of Resource ${this.#path} is flagged as drained, waiting for new content...`); + while (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + timeoutCounter++; + await setTimeout(1); + if (timeoutCounter > 100) { // 100 ms timeout + throw new Error(`Timeout waiting for content of Resource ${this.#path} to become available.`); + } + } + // New content is now available + } + + #updateStatInfo(byteSize) { + const now = new Date(); + this.#statInfo.mtimeMs = now.getTime(); + this.#statInfo.mtime = now; + this.#statInfo.size = byteSize; } /** @@ -279,6 +636,16 @@ class Resource { return this.#path; } + /** + * Gets the virtual resources path + * + * @public + * @returns {string} (Virtual) path of the resource + */ + getOriginalPath() { + return this.#path; + } + /** * Sets the virtual resources path * @@ -311,26 +678,81 @@ class Resource { * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance. * * @public + * @deprecated Use dedicated APIs like Resource.getSize(), .isDirectory(), .getLastModified() instead * @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} * or similar object */ getStatInfo() { + if (!deprecatedGetStatInfoCalled) { + log.verbose(`[DEPRECATION] Resource.getStatInfo() is deprecated and will be removed in future versions. ` + + `Please switch to dedicated APIs like Resource.getSize() instead.`); + deprecatedGetStatInfoCalled = true; + } return this.#statInfo; } /** - * Size in bytes allocated by the underlying buffer. + * Checks whether the resource represents a directory. + * + * @public + * @returns {boolean} True if resource is a directory + */ + isDirectory() { + return this.#isDirectory; + } + + /** + * Gets the last modified timestamp of the resource. * + * @public + * @returns {number} Last modified timestamp (in milliseconds since UNIX epoch) + */ + getLastModified() { + return this.#lastModified; + } + + /** + * Gets the inode number of the resource. + * + * @public + * @returns {number} Inode number of the resource + */ + getInode() { + return this.#inode; + } + + /** + * Resource content size in bytes. + * + * @public * @see {TypedArray#byteLength} - * @returns {Promise} size in bytes, 0 if there is no content yet + * @returns {Promise} size in bytes, 0 if the resource has no content */ async getSize() { - // if resource does not have any content it should have 0 bytes - if (!this.#buffer && !this.#createStream && !this.#stream) { + if (this.#byteSize !== undefined) { + return this.#byteSize; + } + if (this.#contentType === undefined) { return 0; } const buffer = await this.getBuffer(); - return buffer.byteLength; + this.#byteSize = buffer.byteLength; + return this.#byteSize; + } + + /** + * Checks whether the resource size can be determined without reading the entire content. + * E.g. for buffer-based content or if the size has been provided when the resource was created. + * + * @public + * @returns {boolean} True if size can be determined statically + */ + hasSize() { + return ( + this.#contentType === undefined || // No content => size is 0 + this.#byteSize !== undefined || // Size has been determined already + this.#contentType === CONTENT_TYPES.BUFFER // Buffer content => size can be determined + ); } /** @@ -354,20 +776,41 @@ class Resource { } async #getCloneOptions() { + // First wait for new content if the current content is flagged as drained + if (this.#contentType === CONTENT_TYPES.DRAINED_STREAM) { + await this.#waitForNewContent(); + } + + // Then make sure no other operation is currently modifying the content + if (this.#contentMutex.isLocked()) { + await this.#contentMutex.waitForUnlock(); + } + const options = { path: this.#path, - statInfo: clone(this.#statInfo), + statInfo: this.#statInfo, // Will be cloned in constructor + isDirectory: this.#isDirectory, + byteSize: this.#isDirectory ? undefined : await this.getSize(), + lastModified: this.#lastModified, + integrity: this.#isDirectory ? undefined : (this.#contentType ? await this.getIntegrity() : undefined), sourceMetadata: clone(this.#sourceMetadata) }; - if (this.#stream) { - options.buffer = await this.#getBufferFromStream(); - } else if (this.#createStream) { - options.createStream = this.#createStream; - } else if (this.#buffer) { - options.buffer = this.#buffer; + switch (this.#contentType) { + case CONTENT_TYPES.STREAM: + // When cloning resource we have to read the stream into memory + options.buffer = await this.#getBufferFromStream(this.#content); + break; + case CONTENT_TYPES.BUFFER: + options.buffer = this.#content; + break; + case CONTENT_TYPES.FACTORY: + if (this.#createBufferFactory) { + options.createBuffer = this.#createBufferFactory; + } + options.createStream = this.#createStreamFactory; + break; } - return options; } @@ -439,6 +882,13 @@ class Resource { return tree; } + getTags() { + const project = this.getProject(); + const collection = project?.getResourceTagCollection(this); + const tags = collection?.getAllTagsForResource(this) || null; + return tags; + } + /** * Returns source metadata which may contain information specific to the adapter that created the resource * Typically set by an adapter to store information for later retrieval. @@ -448,51 +898,65 @@ class Resource { getSourceMetadata() { return this.#sourceMetadata; } +} - /** - * Returns the content as stream. - * - * @private - * @returns {stream.Readable} Readable stream - */ - #getStream() { - if (this.#streamDrained) { - throw new Error(`Content stream of Resource ${this.#path} is flagged as drained.`); - } - if (this.#createStream) { - return this.#createStream(); - } - this.#streamDrained = true; - return this.#stream; - } +const fnTrue = function() { + return true; +}; +const fnFalse = function() { + return false; +}; - /** - * Converts the buffer into a stream. - * - * @private - * @returns {Promise} Promise resolving with buffer. - */ - #getBufferFromStream() { - if (this.#buffering) { // Prevent simultaneous buffering, causing unexpected access to drained stream - return this.#buffering; - } - return this.#buffering = new Promise((resolve, reject) => { - const contentStream = this.#getStream(); - const buffers = []; - contentStream.on("data", (data) => { - buffers.push(data); - }); - contentStream.on("error", (err) => { - reject(err); - }); - contentStream.on("end", () => { - const buffer = Buffer.concat(buffers); - this.#setBuffer(buffer); - this.#buffering = null; - resolve(buffer); - }); - }); - } +/** + * Parses a Node.js stat object to a UI5 Tooling stat object + * + * @param {fs.Stats} statInfo Node.js stat + * @returns {object} UI5 Tooling stat +*/ +function parseStat(statInfo) { + return { + isFile: statInfo.isFile?.bind(statInfo), + isDirectory: statInfo.isDirectory?.bind(statInfo), + isBlockDevice: statInfo.isBlockDevice?.bind(statInfo), + isCharacterDevice: statInfo.isCharacterDevice?.bind(statInfo), + isSymbolicLink: statInfo.isSymbolicLink?.bind(statInfo), + isFIFO: statInfo.isFIFO?.bind(statInfo), + isSocket: statInfo.isSocket?.bind(statInfo), + ino: statInfo.ino, + size: statInfo.size, + atimeMs: statInfo.atimeMs, + mtimeMs: statInfo.mtimeMs, + ctimeMs: statInfo.ctimeMs, + birthtimeMs: statInfo.birthtimeMs, + atime: statInfo.atime, + mtime: statInfo.mtime, + ctime: statInfo.ctime, + birthtime: statInfo.birthtime, + }; +} + +function createStat(size, isDirectory = false, lastModified) { + const now = new Date(); + const mtime = lastModified === undefined ? now : new Date(lastModified); + return { + isFile: isDirectory ? fnFalse : fnTrue, + isDirectory: isDirectory ? fnTrue : fnFalse, + isBlockDevice: fnFalse, + isCharacterDevice: fnFalse, + isSymbolicLink: fnFalse, + isFIFO: fnFalse, + isSocket: fnFalse, + ino: 0, + size, // Might be undefined + atimeMs: now.getTime(), + mtimeMs: mtime.getTime(), + ctimeMs: now.getTime(), + birthtimeMs: now.getTime(), + atime: now, + mtime, + ctime: now, + birthtime: now, + }; } export default Resource; diff --git a/packages/fs/lib/ResourceFacade.js b/packages/fs/lib/ResourceFacade.js index 58ba37b2a4d..fb883fb9e2f 100644 --- a/packages/fs/lib/ResourceFacade.js +++ b/packages/fs/lib/ResourceFacade.js @@ -45,6 +45,16 @@ class ResourceFacade { return this.#path; } + /** + * Gets the path original resource's path + * + * @public + * @returns {string} (Virtual) path of the resource + */ + getOriginalPath() { + return this.#resource.getPath(); + } + /** * Gets the resource name * @@ -129,16 +139,46 @@ class ResourceFacade { * * Repetitive calls of this function are only possible if new content has been set in the meantime (through * [setStream]{@link @ui5/fs/Resource#setStream}, [setBuffer]{@link @ui5/fs/Resource#setBuffer} - * or [setString]{@link @ui5/fs/Resource#setString}). This - * is to prevent consumers from accessing drained streams. + * or [setString]{@link @ui5/fs/Resource#setString}). + * This is to prevent subsequent consumers from accessing drained streams. * * @public + * @deprecated Use asynchronous Resource.getStreamAsync() instead * @returns {stream.Readable} Readable stream for the resource content. */ getStream() { return this.#resource.getStream(); } + /** + * Gets a readable stream for the resource content. + * + * Repetitive calls of this function are only possible if new content has been set in the meantime (through + * [setStream]{@link @ui5/fs/Resource#setStream}, [setBuffer]{@link @ui5/fs/Resource#setBuffer} + * or [setString]{@link @ui5/fs/Resource#setString}). + * This is to prevent subsequent consumers from accessing drained streams. + * + * For atomic operations, please use [modifyStream]{@link @ui5/fs/Resource#modifyStream} + * + * @public + * @returns {Promise} Promise resolving with a readable stream for the resource content. + */ + async getStreamAsync() { + return this.#resource.getStreamAsync(); + } + + /** + * Modifies the resource content by applying the given callback function. + * The callback function receives a readable stream of the current content + * and must return either a Buffer or a readable stream with the new content. + * The resource content is locked during the modification to prevent concurrent access. + * + * @param {function(stream.Readable): (Buffer|stream.Readable|Promise)} callback + */ + async modifyStream(callback) { + return this.#resource.modifyStream(callback); + } + /** * Sets a readable stream as content. * @@ -150,6 +190,14 @@ class ResourceFacade { return this.#resource.setStream(stream); } + getIntegrity() { + return this.#resource.getIntegrity(); + } + + getInode() { + return this.#resource.getInode(); + } + /** * Gets the resources stat info. * Note that a resources stat information is not updated when the resource is being modified. @@ -157,6 +205,7 @@ class ResourceFacade { * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance. * * @public + * @deprecated Use dedicated APIs like Resource.getSize(), .isDirectory(), .getLastModified() instead * @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} * or similar object */ @@ -164,16 +213,47 @@ class ResourceFacade { return this.#resource.getStatInfo(); } + /** + * Checks whether the resource represents a directory. + * + * @public + * @returns {boolean} True if resource is a directory + */ + isDirectory() { + return this.#resource.isDirectory(); + } + + /** + * Gets the last modified timestamp of the resource. + * + * @public + * @returns {number} Last modified timestamp (in milliseconds since UNIX epoch) + */ + getLastModified() { + return this.#resource.getLastModified(); + } + /** * Size in bytes allocated by the underlying buffer. * * @see {TypedArray#byteLength} - * @returns {Promise} size in bytes, 0 if there is no content yet + * @returns {Promise} size in bytes, 0 if the resource has no content */ async getSize() { return this.#resource.getSize(); } + /** + * Checks whether the resource size can be determined without reading the entire content. + * E.g. for buffer-based content or if the size has been provided when the resource was created. + * + * @public + * @returns {boolean} True if size can be determined statically + */ + hasSize() { + return this.#resource.hasSize(); + } + /** * Adds a resource collection name that was involved in locating this resource. * @@ -192,6 +272,10 @@ class ResourceFacade { return this.#resource.getPathTree(); } + getTags() { + return this.#resource.getTags(); + } + /** * Retrieve the project assigned to the resource *
diff --git a/packages/fs/lib/ResourceTagCollection.js b/packages/fs/lib/ResourceTagCollection.js index 9214c15bd0b..712c2eb27af 100644 --- a/packages/fs/lib/ResourceTagCollection.js +++ b/packages/fs/lib/ResourceTagCollection.js @@ -5,11 +5,18 @@ import ResourceFacade from "./ResourceFacade.js"; /** * A ResourceTagCollection * - * @private * @class * @alias @ui5/fs/internal/ResourceTagCollection */ class ResourceTagCollection { + /** + * Constructor + * + * @param {object} options Options + * @param {string[]} [options.allowedTags=[]] List of allowed tags + * @param {string[]} [options.allowedNamespaces=[]] List of allowed namespaces + * @param {object} [options.tags] Initial tags object mapping resource paths to their tags + */ constructor({allowedTags = [], allowedNamespaces = [], tags}) { this._allowedTags = allowedTags; // Allowed tags are validated during use this._allowedNamespaces = allowedNamespaces; @@ -32,6 +39,13 @@ class ResourceTagCollection { this._pathTags = tags || Object.create(null); } + /** + * Set a tag on a resource + * + * @param {string|object} resourcePath Path of the resource or a resource instance + * @param {string} tag Tag in the format "namespace:Name" + * @param {string|number|boolean} [value=true] Tag value + */ setTag(resourcePath, tag, value = true) { resourcePath = this._getPath(resourcePath); this._validateTag(tag); @@ -43,6 +57,12 @@ class ResourceTagCollection { this._pathTags[resourcePath][tag] = value; } + /** + * Clear a tag from a resource + * + * @param {string|object} resourcePath Path of the resource or a resource instance + * @param {string} tag Tag in the format "namespace:Name" + */ clearTag(resourcePath, tag) { resourcePath = this._getPath(resourcePath); this._validateTag(tag); @@ -52,6 +72,13 @@ class ResourceTagCollection { } } + /** + * Get a tag value from a resource + * + * @param {string|object} resourcePath Path of the resource or a resource instance + * @param {string} tag Tag in the format "namespace:Name" + * @returns {string|number|boolean|undefined} Tag value or undefined if not set + */ getTag(resourcePath, tag) { resourcePath = this._getPath(resourcePath); this._validateTag(tag); @@ -61,10 +88,31 @@ class ResourceTagCollection { } } + /** + * Get all tags for all resources + * + * @returns {object} Object mapping resource paths to their tags + */ getAllTags() { return this._pathTags; } + /** + * Get all tags for all resources + * + * @param {string|object} resourcePath Path of the resource + * @returns {object|null} Object mapping tags to their values for the given resource path + */ + getAllTagsForResource(resourcePath) { + resourcePath = this._getPath(resourcePath); + return this._pathTags[resourcePath] || null; + } + /** + * Check if a tag is accepted by this collection + * + * @param {string} tag Tag in the format "namespace:Name" + * @returns {boolean} Whether the tag is accepted + */ acceptsTag(tag) { if (this._allowedTags.includes(tag) || this._allowedNamespacesRegExp?.test(tag)) { return true; @@ -72,6 +120,12 @@ class ResourceTagCollection { return false; } + /** + * Extract the path from a resource or validate a path string + * + * @param {string|object} resourcePath Path of the resource or a resource instance + * @returns {string} Resolved resource path + */ _getPath(resourcePath) { if (typeof resourcePath !== "string") { if (resourcePath instanceof ResourceFacade) { @@ -86,6 +140,12 @@ class ResourceTagCollection { return resourcePath; } + /** + * Validate a tag format and check if it's accepted by this collection + * + * @param {string} tag Tag in the format "namespace:Name" + * @throws {Error} If the tag format is invalid or not accepted + */ _validateTag(tag) { if (!tag.includes(":")) { throw new Error(`Invalid Tag "${tag}": Colon required after namespace`); @@ -112,6 +172,12 @@ class ResourceTagCollection { } } + /** + * Validate that a tag value has an acceptable type + * + * @param {any} value Value to validate + * @throws {Error} If the value type is not string, number, or boolean + */ _validateValue(value) { const type = typeof value; if (!["string", "number", "boolean"].includes(type)) { @@ -119,6 +185,14 @@ class ResourceTagCollection { `Invalid Tag Value: Must be of type string, number or boolean but is ${type}`); } } + + clone() { + return new ResourceTagCollection({ + allowedTags: this._allowedTags, + allowedNamespaces: this._allowedNamespaces, + tags: JSON.parse(JSON.stringify(this._pathTags)) + }); + } } export default ResourceTagCollection; diff --git a/packages/fs/lib/WriterCollection.js b/packages/fs/lib/WriterCollection.js index f601867632e..51f49f21f5d 100644 --- a/packages/fs/lib/WriterCollection.js +++ b/packages/fs/lib/WriterCollection.js @@ -62,10 +62,14 @@ class WriterCollection extends AbstractReaderWriter { this._writerMapping = writerMapping; this._readerCollection = new ReaderCollection({ name: `Reader collection of writer collection '${this._name}'`, - readers: Object.values(writerMapping) + readers: Array.from(new Set(Object.values(writerMapping))) // Ensure unique readers }); } + getMapping() { + return this._writerMapping; + } + /** * Locates resources by glob. * diff --git a/packages/fs/lib/adapters/AbstractAdapter.js b/packages/fs/lib/adapters/AbstractAdapter.js index 96cf4154250..9e0ee367cbb 100644 --- a/packages/fs/lib/adapters/AbstractAdapter.js +++ b/packages/fs/lib/adapters/AbstractAdapter.js @@ -17,20 +17,20 @@ import Resource from "../Resource.js"; */ class AbstractAdapter extends AbstractReaderWriter { /** - * The constructor * * @public * @param {object} parameters Parameters + * @param {string} parameters.name * @param {string} parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash * @param {string[]} [parameters.excludes] List of glob patterns to exclude * @param {object} [parameters.project] Experimental, internal parameter. Do not use */ - constructor({virBasePath, excludes = [], project}) { + constructor({name, virBasePath, excludes = [], project}) { if (new.target === AbstractAdapter) { throw new TypeError("Class 'AbstractAdapter' is abstract"); } - super(); + super(name); if (!virBasePath) { throw new Error(`Unable to create adapter: Missing parameter 'virBasePath'`); @@ -81,17 +81,7 @@ class AbstractAdapter extends AbstractReaderWriter { if (patterns[i] && idx !== -1 && idx < this._virBaseDir.length) { const subPath = patterns[i]; return [ - this._createResource({ - statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { - return true; - } - }, - source: { - adapter: "Abstract" - }, - path: subPath - }) + this._createDirectoryResource(subPath) ]; } } @@ -106,7 +96,7 @@ class AbstractAdapter extends AbstractReaderWriter { * @returns {boolean} True if path is excluded, otherwise false */ _isPathExcluded(virPath) { - return micromatch(virPath, this._excludes).length > 0; + return micromatch(virPath, this._excludes, {dot: true}).length > 0; } /** @@ -201,6 +191,10 @@ class AbstractAdapter extends AbstractReaderWriter { if (this._project) { parameters.project = this._project; } + if (!parameters.source) { + parameters.source = Object.create(null); + } + parameters.source.adapter = this.constructor.name; return new Resource(parameters); } @@ -289,6 +283,38 @@ class AbstractAdapter extends AbstractReaderWriter { const relPath = virPath.substr(this._virBasePath.length); return relPath; } + + _createDirectoryResource(dirPath) { + const now = new Date(); + const fnFalse = function() { + return false; + }; + const fnTrue = function() { + return true; + }; + const statInfo = { + isFile: fnFalse, + isDirectory: fnTrue, + isBlockDevice: fnFalse, + isCharacterDevice: fnFalse, + isSymbolicLink: fnFalse, + isFIFO: fnFalse, + isSocket: fnFalse, + size: 0, + atimeMs: now.getTime(), + mtimeMs: now.getTime(), + ctimeMs: now.getTime(), + birthtimeMs: now.getTime(), + atime: now, + mtime: now, + ctime: now, + birthtime: now, + }; + return this._createResource({ + statInfo: statInfo, + path: dirPath, + }); + } } export default AbstractAdapter; diff --git a/packages/fs/lib/adapters/FileSystem.js b/packages/fs/lib/adapters/FileSystem.js index d086fac40dd..33a6b798579 100644 --- a/packages/fs/lib/adapters/FileSystem.js +++ b/packages/fs/lib/adapters/FileSystem.js @@ -7,12 +7,13 @@ const copyFile = promisify(fs.copyFile); const chmod = promisify(fs.chmod); const mkdir = promisify(fs.mkdir); const stat = promisify(fs.stat); +const readFile = promisify(fs.readFile); import {globby, isGitIgnored} from "globby"; import {PassThrough} from "node:stream"; import AbstractAdapter from "./AbstractAdapter.js"; const READ_ONLY_MODE = 0o444; -const ADAPTER_NAME = "FileSystem"; + /** * File system resource adapter * @@ -23,9 +24,9 @@ const ADAPTER_NAME = "FileSystem"; */ class FileSystem extends AbstractAdapter { /** - * The Constructor. * * @param {object} parameters Parameters + * @param {string} parameters.name * @param {string} parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash * @param {string} parameters.fsBasePath @@ -35,8 +36,8 @@ class FileSystem extends AbstractAdapter { * Whether to apply any excludes defined in an optional .gitignore in the given fsBasePath directory * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) */ - constructor({virBasePath, project, fsBasePath, excludes, useGitignore=false}) { - super({virBasePath, project, excludes}); + constructor({name, virBasePath, project, fsBasePath, excludes, useGitignore=false}) { + super({name, virBasePath, project, excludes}); if (!fsBasePath) { throw new Error(`Unable to create adapter: Missing parameter 'fsBasePath'`); @@ -80,7 +81,7 @@ class FileSystem extends AbstractAdapter { statInfo: stat, path: this._virBaseDir, sourceMetadata: { - adapter: ADAPTER_NAME, + adapter: this.constructor.name, fsPath: this._fsBasePath }, createStream: () => { @@ -124,11 +125,14 @@ class FileSystem extends AbstractAdapter { statInfo: stat, path: virPath, sourceMetadata: { - adapter: ADAPTER_NAME, + adapter: this.constructor.name, fsPath: fsPath }, createStream: () => { return fs.createReadStream(fsPath); + }, + createBuffer: () => { + return readFile(fsPath); } })); } @@ -158,16 +162,8 @@ class FileSystem extends AbstractAdapter { // Neither starts with basePath, nor equals baseDirectory if (!options.nodir && this._virBasePath.startsWith(virPath)) { // Create virtual directories for the virtual base path (which has to exist) - // TODO: Maybe improve this by actually matching the base paths segments to the virPath - return this._createResource({ - project: this._project, - statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { - return true; - } - }, - path: virPath - }); + // FUTURE: Maybe improve this by actually matching the base paths segments to the virPath + return this._createDirectoryResource(virPath); } else { return null; } @@ -200,7 +196,7 @@ class FileSystem extends AbstractAdapter { statInfo, path: virPath, sourceMetadata: { - adapter: ADAPTER_NAME, + adapter: this.constructor.name, fsPath } }; @@ -210,6 +206,9 @@ class FileSystem extends AbstractAdapter { resourceOptions.createStream = function() { return fs.createReadStream(fsPath); }; + resourceOptions.createBuffer = function() { + return readFile(fsPath); + }; } return this._createResource(resourceOptions); @@ -260,7 +259,7 @@ class FileSystem extends AbstractAdapter { await mkdir(dirPath, {recursive: true}); const sourceMetadata = resource.getSourceMetadata(); - if (sourceMetadata && sourceMetadata.adapter === ADAPTER_NAME && sourceMetadata.fsPath) { + if (sourceMetadata && sourceMetadata.adapter === this.constructor.name && sourceMetadata.fsPath) { // Resource has been created by FileSystem adapter. This means it might require special handling /* The following code covers these four conditions: @@ -292,6 +291,60 @@ class FileSystem extends AbstractAdapter { } else {/* Different paths + modifications require no special handling */} } + if (sourceMetadata && sourceMetadata.adapter === "CAS" && sourceMetadata.fsPath && + !sourceMetadata.contentModified) { + // CAS-backed resource: Stream directly to disk without PassThrough intermediary. + // This eliminates one pipe hop and one stream object per resource while maintaining + // stream backpressure for I/O throttling at scale (14k+ concurrent writes). + log.silly(`Writing CAS resource to ${fsPath}`); + await new Promise((resolve, reject) => { + const contentStream = resource.getStream(); + contentStream.on("error", (err) => { + reject(err); + }); + + if (!drain) { + // Capture decompressed content for future getBuffer() calls + const buffers = []; + contentStream.on("data", (data) => { + buffers.push(data); + }); + contentStream.on("end", () => { + resource.setBuffer(Buffer.concat(buffers)); + }); + } + + const writeOptions = {}; + if (readOnly) { + writeOptions.mode = READ_ONLY_MODE; + } + const write = fs.createWriteStream(fsPath, writeOptions); + write.on("error", (err) => { + reject(err); + }); + write.on("close", () => { + resolve(); + }); + contentStream.pipe(write); + }); + return; + } + + if (sourceMetadata && sourceMetadata.adapter === "CAS_SQLITE" && + !sourceMetadata.contentModified) { + log.silly(`Writing CAS_SQLITE resource to ${fsPath}`); + const buffer = await resource.getBuffer(); + const writeOptions = {}; + if (readOnly) { + writeOptions.mode = READ_ONLY_MODE; + } + await writeFile(fsPath, buffer, writeOptions); + if (!drain) { + resource.setBuffer(buffer); + } + return; + } + log.silly(`Writing to ${fsPath}`); await new Promise((resolve, reject) => { diff --git a/packages/fs/lib/adapters/Memory.js b/packages/fs/lib/adapters/Memory.js index 35be99cf953..7215e2ab189 100644 --- a/packages/fs/lib/adapters/Memory.js +++ b/packages/fs/lib/adapters/Memory.js @@ -3,8 +3,6 @@ const log = getLogger("resources:adapters:Memory"); import micromatch from "micromatch"; import AbstractAdapter from "./AbstractAdapter.js"; -const ADAPTER_NAME = "Memory"; - /** * Virtual resource Adapter * @@ -15,17 +13,17 @@ const ADAPTER_NAME = "Memory"; */ class Memory extends AbstractAdapter { /** - * The constructor. * * @public * @param {object} parameters Parameters + * @param {string} parameters.name * @param {string} parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash * @param {string[]} [parameters.excludes] List of glob patterns to exclude * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) */ - constructor({virBasePath, project, excludes}) { - super({virBasePath, project, excludes}); + constructor({name, virBasePath, project, excludes}) { + super({name, virBasePath, project, excludes}); this._virFiles = Object.create(null); // map full of files this._virDirs = Object.create(null); // map full of directories } @@ -72,18 +70,7 @@ class Memory extends AbstractAdapter { async _runGlob(patterns, options = {nodir: true}, trace) { if (patterns[0] === "" && !options.nodir) { // Match virtual root directory return [ - this._createResource({ - project: this._project, - statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { - return true; - } - }, - sourceMetadata: { - adapter: ADAPTER_NAME - }, - path: this._virBasePath.slice(0, -1) - }) + this._createDirectoryResource(this._virBasePath.slice(0, -1)) ]; } @@ -157,18 +144,7 @@ class Memory extends AbstractAdapter { for (let i = pathSegments.length - 1; i >= 0; i--) { const segment = pathSegments[i]; if (!this._virDirs[segment]) { - this._virDirs[segment] = this._createResource({ - project: this._project, - sourceMetadata: { - adapter: ADAPTER_NAME - }, - statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { - return true; - } - }, - path: this._virBasePath + segment - }); + this._virDirs[segment] = this._createDirectoryResource(this._virBasePath + segment); } } } diff --git a/packages/fs/lib/readers/Filter.js b/packages/fs/lib/readers/Filter.js index b95654daa29..903f43cef76 100644 --- a/packages/fs/lib/readers/Filter.js +++ b/packages/fs/lib/readers/Filter.js @@ -23,12 +23,13 @@ class Filter extends AbstractReader { * * @public * @param {object} parameters Parameters + * @param {object} parameters.name Name of the reader * @param {@ui5/fs/AbstractReader} parameters.reader The resource reader or collection to wrap * @param {@ui5/fs/readers/Filter~callback} parameters.callback * Filter function. Will be called for every resource read through this reader. */ - constructor({reader, callback}) { - super(); + constructor({name, reader, callback}) { + super(name); if (!reader) { throw new Error(`Missing parameter "reader"`); } diff --git a/packages/fs/lib/readers/Link.js b/packages/fs/lib/readers/Link.js index 726a22b763b..b21c7f469ae 100644 --- a/packages/fs/lib/readers/Link.js +++ b/packages/fs/lib/readers/Link.js @@ -42,11 +42,12 @@ class Link extends AbstractReader { * * @public * @param {object} parameters Parameters + * @param {object} parameters.name Name of the reader * @param {@ui5/fs/AbstractReader} parameters.reader The resource reader or collection to wrap * @param {@ui5/fs/readers/Link/PathMapping} parameters.pathMapping */ - constructor({reader, pathMapping}) { - super(); + constructor({name, reader, pathMapping}) { + super(name); if (!reader) { throw new Error(`Missing parameter "reader"`); } @@ -58,17 +59,7 @@ class Link extends AbstractReader { Link._validatePathMapping(pathMapping); } - /** - * Locates resources by glob. - * - * @private - * @param {string|string[]} patterns glob pattern as string or an array of - * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing/Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - async _byGlob(patterns, options, trace) { + resolvePattern(patterns) { if (!(patterns instanceof Array)) { patterns = [patterns]; } @@ -80,7 +71,29 @@ class Link extends AbstractReader { }); // Flatten prefixed patterns - patterns = Array.prototype.concat.apply([], patterns); + return Array.prototype.concat.apply([], patterns); + } + + resolvePath(virPath) { + if (!virPath.startsWith(this._pathMapping.linkPath)) { + return null; + } + const targetPath = this._pathMapping.targetPath + virPath.substr(this._pathMapping.linkPath.length); + return targetPath; + } + + /** + * Locates resources by glob. + * + * @private + * @param {string|string[]} patterns glob pattern as string or an array of + * glob patterns for virtual directory structure + * @param {object} options glob options + * @param {@ui5/fs/tracing/Trace} trace Trace instance + * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + */ + async _byGlob(patterns, options, trace) { + patterns = this.resolvePattern(patterns); // Keep resource's internal path unchanged for now const resources = await this._reader._byGlob(patterns, options, trace); @@ -105,10 +118,10 @@ class Link extends AbstractReader { * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource */ async _byPath(virPath, options, trace) { - if (!virPath.startsWith(this._pathMapping.linkPath)) { + const targetPath = this.resolvePath(virPath); + if (!targetPath) { return null; } - const targetPath = this._pathMapping.targetPath + virPath.substr(this._pathMapping.linkPath.length); log.silly(`byPath: Rewriting virtual path ${virPath} to ${targetPath}`); const resource = await this._reader._byPath(targetPath, options, trace); diff --git a/packages/fs/lib/readers/Proxy.js b/packages/fs/lib/readers/Proxy.js new file mode 100644 index 00000000000..23f340f27bb --- /dev/null +++ b/packages/fs/lib/readers/Proxy.js @@ -0,0 +1,126 @@ +import micromatch from "micromatch"; +import AbstractReader from "../AbstractReader.js"; + +/** + * Callback function to retrieve a resource by its virtual path. + * + * @public + * @callback @ui5/fs/readers/Proxy~getResource + * @param {string} virPath Virtual path + * @returns {Promise} Promise resolving with a Resource instance + */ + +/** + * Callback function to list all available virtual resource paths. + * + * @public + * @callback @ui5/fs/readers/Proxy~listResourcePaths + * @returns {Promise} Promise resolving to an array of strings (the virtual resource paths) + */ + +/** + * Generic proxy adapter. Allowing to serve resources using callback functions. Read only. + * + * @public + * @class + * @alias @ui5/fs/readers/Proxy + * @extends @ui5/fs/readers/AbstractReader + */ +class Proxy extends AbstractReader { + /** + * Constructor + * + * @public + * @param {object} parameters + * @param {object} parameters.name Name of the reader + * @param {@ui5/fs/readers/Proxy~getResource} parameters.getResource + * Callback function to retrieve a resource by its virtual path. + * @param {@ui5/fs/readers/Proxy~listResourcePaths} parameters.listResourcePaths + * Callback function to list all available virtual resource paths. + * @returns {@ui5/fs/readers/Proxy} Reader instance + */ + constructor({name, getResource, listResourcePaths}) { + super(name); + if (typeof getResource !== "function") { + throw new Error(`Proxy adapter: Missing or invalid parameter 'getResource'`); + } + if (typeof listResourcePaths !== "function") { + throw new Error(`Proxy adapter: Missing or invalid parameter 'listResourcePaths'`); + } + this._getResource = getResource; + this._listResourcePaths = listResourcePaths; + } + + async _listResourcePaths() { + const virPaths = await this._listResourcePaths(); + if (!Array.isArray(virPaths) || !virPaths.every((p) => typeof p === "string")) { + throw new Error( + `Proxy adapter: 'listResourcePaths' did not return an array of strings`); + } + return virPaths; + } + + /** + * Matches and returns resources from a given map (either _virFiles or _virDirs). + * + * @private + * @param {string[]} patterns + * @param {string[]} resourcePaths + * @returns {Promise} + */ + async _matchPatterns(patterns, resourcePaths) { + const matchedPaths = micromatch(resourcePaths, patterns, { + dot: true + }); + return await Promise.all(matchedPaths.map((virPath) => { + return this._getResource(virPath); + })); + } + + /** + * Locate resources by glob. + * + * @private + * @param {string|string[]} virPattern glob pattern as string or array of glob patterns for + * virtual directory structure + * @param {object} [options={}] glob options + * @param {boolean} [options.nodir=true] Do not match directories + * @param {@ui5/fs/tracing.Trace} trace Trace instance + * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + */ + async _byGlob(virPattern, options = {nodir: true}, trace) { + if (!(virPattern instanceof Array)) { + virPattern = [virPattern]; + } + + if (virPattern[0] === "" && !options.nodir) { // Match virtual root directory + return [ + this._createDirectoryResource(this._virBasePath.slice(0, -1)) + ]; + } + + return await this._matchPatterns(virPattern, await this._listResourcePaths()); + } + + /** + * Locates resources by path. + * + * @private + * @param {string} virPath Virtual path + * @param {object} options Options + * @param {@ui5/fs/tracing.Trace} trace Trace instance + * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource + */ + async _byPath(virPath, options, trace) { + trace.pathCall(); + + const resource = await this._getResource(virPath); + if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) { + return null; + } else { + return resource; + } + } +} + +export default Proxy; diff --git a/packages/fs/lib/resourceFactory.js b/packages/fs/lib/resourceFactory.js index 6a98c9a7961..cfa27fd7bc5 100644 --- a/packages/fs/lib/resourceFactory.js +++ b/packages/fs/lib/resourceFactory.js @@ -9,6 +9,9 @@ import Resource from "./Resource.js"; import WriterCollection from "./WriterCollection.js"; import Filter from "./readers/Filter.js"; import Link from "./readers/Link.js"; +import Proxy from "./readers/Proxy.js"; +import MonitoredReader from "./MonitoredReader.js"; +import MonitoredReaderWriter from "./MonitoredReaderWriter.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("resources:resourceFactory"); @@ -26,6 +29,7 @@ const log = getLogger("resources:resourceFactory"); * * @public * @param {object} parameters Parameters + * @param {string} parameters.name * @param {string} parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash * @param {string} [parameters.fsBasePath] * File System base path. @@ -38,11 +42,11 @@ const log = getLogger("resources:resourceFactory"); * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) * @returns {@ui5/fs/adapters/FileSystem|@ui5/fs/adapters/Memory} File System- or Virtual Adapter */ -export function createAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}) { +export function createAdapter({name, fsBasePath, virBasePath, project, excludes, useGitignore}) { if (fsBasePath) { - return new FsAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}); + return new FsAdapter({name, fsBasePath, virBasePath, project, excludes, useGitignore}); } else { - return new MemAdapter({virBasePath, project, excludes}); + return new MemAdapter({name, virBasePath, project, excludes}); } } @@ -178,15 +182,17 @@ export function createResource(parameters) { export function createWorkspace({reader, writer, virBasePath = "/", name = "workspace"}) { if (!writer) { writer = new MemAdapter({ + name: `Workspace writer for ${name}`, virBasePath }); } - return new DuplexCollection({ + const d = new DuplexCollection({ reader, writer, name }); + return d; } /** @@ -234,6 +240,19 @@ export function createLinkReader(parameters) { return new Link(parameters); } +/** + * @param {object} parameters + * @param {object} parameters.name Name of the reader + * @param {@ui5/fs/readers/Proxy~getResource} parameters.getResource + * Callback function to retrieve a resource by its virtual path. + * @param {@ui5/fs/readers/Proxy~listResourcePaths} parameters.listResourcePaths + * Callback function to list all available virtual resource paths. + * @returns {@ui5/fs/readers/Proxy} Reader instance + */ +export function createProxy(parameters) { + return new Proxy(parameters); +} + /** * Create a [Link-Reader]{@link @ui5/fs/readers/Link} where all requests are prefixed with * /resources/<namespace>. @@ -243,12 +262,14 @@ export function createLinkReader(parameters) { * * @public * @param {object} parameters + * @param {string} parameters.name * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers * @param {string} parameters.namespace Project namespace * @returns {@ui5/fs/readers/Link} Reader instance */ -export function createFlatReader({reader, namespace}) { +export function createFlatReader({name, reader, namespace}) { return new Link({ + name, reader: reader, pathMapping: { linkPath: `/`, @@ -257,6 +278,13 @@ export function createFlatReader({reader, namespace}) { }); } +export function createMonitor(readerWriter) { + if (readerWriter instanceof DuplexCollection) { + return new MonitoredReaderWriter(readerWriter); + } + return new MonitoredReader(readerWriter); +} + /** * Normalizes virtual glob patterns by prefixing them with * a given virtual base directory path diff --git a/packages/fs/package.json b/packages/fs/package.json index 16a0a16e6f3..4c8d1c05928 100644 --- a/packages/fs/package.json +++ b/packages/fs/package.json @@ -29,7 +29,8 @@ "./Resource": "./lib/Resource.js", "./resourceFactory": "./lib/resourceFactory.js", "./package.json": "./package.json", - "./internal/ResourceTagCollection": "./lib/ResourceTagCollection.js" + "./internal/ResourceTagCollection": "./lib/ResourceTagCollection.js", + "./internal/MonitoredResourceTagCollection": "./lib/MonitoredResourceTagCollection.js" }, "engines": { "node": "^22.20.0 || >=24.0.0", @@ -56,6 +57,7 @@ }, "dependencies": { "@ui5/logger": "^5.0.0-alpha.4", + "async-mutex": "^0.5.0", "clone": "^2.1.2", "escape-string-regexp": "^5.0.0", "globby": "^15.0.0", @@ -63,7 +65,8 @@ "micromatch": "^4.0.8", "minimatch": "^10.2.2", "pretty-hrtime": "^1.0.3", - "random-int": "^3.1.0" + "random-int": "^3.1.0", + "ssri": "^13.0.0" }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.3.0", diff --git a/packages/fs/test/lib/Resource.js b/packages/fs/test/lib/Resource.js index aca04728746..aa0d64fa507 100644 --- a/packages/fs/test/lib/Resource.js +++ b/packages/fs/test/lib/Resource.js @@ -1,18 +1,21 @@ import test from "ava"; +import sinon from "sinon"; import {Stream, Transform} from "node:stream"; -import {promises as fs, createReadStream} from "node:fs"; +import {statSync, createReadStream} from "node:fs"; +import {stat, readFile} from "node:fs/promises"; import path from "node:path"; import Resource from "../../lib/Resource.js"; function createBasicResource() { const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); + const statInfo = statSync(fsPath); const resource = new Resource({ path: "/app/index.html", createStream: function() { return createReadStream(fsPath); }, project: {}, - statInfo: {}, + statInfo: statInfo, fsPath }); return resource; @@ -39,6 +42,10 @@ const readStream = (readableStream) => { }); }; +test.afterEach.always((t) => { + sinon.restore(); +}); + test("Resource: constructor with missing path parameter", (t) => { t.throws(() => { new Resource({}); @@ -85,12 +92,152 @@ test("Resource: constructor with duplicated content parameter", (t) => { new Resource(resourceParams); }, { instanceOf: Error, - message: "Unable to create Resource: Please set only one content parameter. " + - "'buffer', 'string', 'stream' or 'createStream'" + message: "Unable to create Resource: Multiple content parameters provided. " + + "Please provide only one of the following parameters: 'buffer', 'string', 'stream' or 'createStream'" }, "Threw with expected error message"); }); }); +test("Resource: constructor with createBuffer factory must provide createStream", (t) => { + t.throws(() => { + new Resource({ + path: "/my/path", + createBuffer: () => Buffer.from("Content"), + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'createStream' must be provided when " + + "parameter 'createBuffer' is used" + }); +}); + +test("Resource: constructor with invalid createBuffer parameter", (t) => { + t.throws(() => { + new Resource({ + path: "/my/path", + createBuffer: "not a function", + createStream: () => { + return new Stream.Readable(); + } + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'createBuffer' must be a function" + }); +}); + +test("Resource: constructor with invalid content parameters", (t) => { + t.throws(() => { + new Resource({ + path: "/my/path", + createStream: "not a function" + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'createStream' must be a function" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + stream: "not a stream" + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'stream' must be a readable stream" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + buffer: "not a buffer" + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'buffer' must be of type Buffer" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + string: 123 + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'string' must be of type string" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + buffer: Buffer.from("Content"), + byteSize: -1 + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'byteSize' must be a positive number" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + buffer: Buffer.from("Content"), + byteSize: "not a number" + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'byteSize' must be a positive number" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + buffer: Buffer.from("Content"), + lastModified: -1 + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'lastModified' must be a positive number" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + buffer: Buffer.from("Content"), + lastModified: "not a number" + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: Parameter 'lastModified' must be a positive number" + }); + + const invalidStatInfo = { + isDirectory: () => false, + isFile: () => false, + size: 100, + mtimeMs: Date.now() + }; + t.throws(() => { + new Resource({ + path: "/my/path", + statInfo: invalidStatInfo + }); + }, { + instanceOf: Error, + message: "Unable to create Resource: statInfo must represent either a file or a directory" + }); + + t.throws(() => { + new Resource({ + path: "/my/path", + sourceMetadata: "invalid value" + }); + }, { + instanceOf: Error, + message: `Unable to create Resource: Parameter 'sourceMetadata' must be of type "object"` + }); +}); + test("Resource: From buffer", async (t) => { const resource = new Resource({ path: "/my/path", @@ -126,6 +273,24 @@ test("Resource: From createStream", async (t) => { const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); const resource = new Resource({ path: "/my/path", + byteSize: 91, + createStream: () => { + return createReadStream(fsPath); + } + }); + t.is(await resource.getSize(), 91, "Content is set"); + t.false(resource.isModified(), "Content of new resource is not modified"); + t.false(resource.getSourceMetadata().contentModified, "Content of new resource is not modified"); +}); + +test("Resource: From createBuffer", async (t) => { + const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); + const resource = new Resource({ + path: "/my/path", + byteSize: 91, + createBuffer: async () => { + return Buffer.from(await readFile(fsPath)); + }, createStream: () => { return createReadStream(fsPath); } @@ -150,6 +315,7 @@ test("Resource: Source metadata", async (t) => { t.is(resource.getSourceMetadata().adapter, "My Adapter", "Correct source metadata 'adapter' value"); t.is(resource.getSourceMetadata().fsPath, "/some/path", "Correct source metadata 'fsPath' value"); }); + test("Resource: Source metadata with modified content", async (t) => { const resource = new Resource({ path: "/my/path", @@ -307,6 +473,31 @@ test("Resource: getStream throwing an error", (t) => { }); }); +test("Resource: getStream call while resource is being transformed", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + }) + }); + + const p1 = resource.getBuffer(); // Trigger async transformation of stream to buffer + t.throws(() => { + resource.getStream(); // Synchronous getStream can't wait for transformation to finish + }, { + message: /Content of Resource \/my\/path\/to\/resource is currently being transformed. Consider using Resource.getStreamAsync\(\) to wait for the transformation to finish./ + }); + await p1; // Wait for initial transformation to finish + + t.false(resource.isModified(), "Resource has not been modified"); + + const value = await resource.getString(); + t.is(value, "Stream content", "Initial content still set"); +}); + test("Resource: setString", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", @@ -315,11 +506,13 @@ test("Resource: setString", async (t) => { t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); t.false(resource.isModified(), "Resource is not modified"); + t.falsy(resource.getLastModified(), "lastModified is not set"); resource.setString("Content"); t.is(resource.getSourceMetadata().contentModified, true, "sourceMetadata modified flag updated correctly"); t.true(resource.isModified(), "Resource is modified"); + t.truthy(resource.getLastModified(), "lastModified should be updated"); const value = await resource.getString(); t.is(value, "Content", "String set"); @@ -333,20 +526,48 @@ test("Resource: setBuffer", async (t) => { t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); t.false(resource.isModified(), "Resource is not modified"); + t.falsy(resource.getLastModified(), "lastModified is not set"); resource.setBuffer(Buffer.from("Content")); t.is(resource.getSourceMetadata().contentModified, true, "sourceMetadata modified flag updated correctly"); t.true(resource.isModified(), "Resource is modified"); + t.truthy(resource.getLastModified(), "lastModified should be updated"); const value = await resource.getString(); t.is(value, "Content", "String set"); }); +test("Resource: setBuffer call while resource is being transformed", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + }) + }); + + const p1 = resource.getBuffer(); // Trigger async transformation of stream to buffer + t.throws(() => { + resource.setBuffer(Buffer.from("Content")); // Set new buffer while transformation is still ongoing + }, { + message: `Unable to set buffer: Content of Resource /my/path/to/resource is currently being transformed` + }); + await p1; // Wait for initial transformation to finish + + t.false(resource.isModified(), "Resource has not been modified"); + + const value = await resource.getString(); + t.is(value, "Stream content", "Initial content still set"); +}); + test("Resource: size modification", async (t) => { const resource = new Resource({ path: "/my/path/to/resource" }); + t.true(resource.hasSize(), "resource without content has size"); t.is(await resource.getSize(), 0, "initial size without content"); // string @@ -361,9 +582,11 @@ test("Resource: size modification", async (t) => { // buffer resource.setBuffer(Buffer.from("Super")); + t.true(resource.hasSize(), "has size"); t.is(await resource.getSize(), 5, "size after manually setting the string"); const clonedResource1 = await resource.clone(); + t.true(clonedResource1.hasSize(), "has size after cloning"); t.is(await clonedResource1.getSize(), 5, "size after cloning the resource"); // buffer with alloc @@ -378,6 +601,7 @@ test("Resource: size modification", async (t) => { }).getSize(), 1234, "buffer with alloc when passing buffer to constructor"); const clonedResource2 = await resource.clone(); + t.true(clonedResource2.hasSize(), "buffer with alloc after clone has size"); t.is(await clonedResource2.getSize(), 1234, "buffer with alloc after clone"); // stream @@ -392,9 +616,11 @@ test("Resource: size modification", async (t) => { stream.push(null); streamResource.setStream(stream); + t.false(streamResource.hasSize(), "size not yet known for streamResource"); // stream is read and stored in buffer // test parallel size retrieval + await streamResource.getBuffer(); const [size1, size2] = await Promise.all([streamResource.getSize(), streamResource.getSize()]); t.is(size1, 23, "size for streamResource, parallel 1"); t.is(size2, 23, "size for streamResource, parallel 2"); @@ -409,6 +635,7 @@ test("Resource: setStream (Stream)", async (t) => { t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); t.false(resource.isModified(), "Resource is not modified"); + t.falsy(resource.getLastModified(), "lastModified is not set"); const stream = new Stream.Readable(); stream._read = function() {}; @@ -421,6 +648,7 @@ test("Resource: setStream (Stream)", async (t) => { t.is(resource.getSourceMetadata().contentModified, true, "sourceMetadata modified flag updated correctly"); t.true(resource.isModified(), "Resource is modified"); + t.truthy(resource.getLastModified(), "lastModified should be updated"); const value = await resource.getString(); t.is(value, "I am a readable stream!", "Stream set correctly"); @@ -434,6 +662,7 @@ test("Resource: setStream (Create stream callback)", async (t) => { t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); t.false(resource.isModified(), "Resource is not modified"); + t.falsy(resource.getLastModified(), "lastModified is not set"); resource.setStream(() => { const stream = new Stream.Readable(); @@ -447,11 +676,38 @@ test("Resource: setStream (Create stream callback)", async (t) => { t.is(resource.getSourceMetadata().contentModified, true, "sourceMetadata modified flag updated correctly"); t.true(resource.isModified(), "Resource is modified"); + t.truthy(resource.getLastModified(), "lastModified should be updated"); const value = await resource.getString(); t.is(value, "I am a readable stream!", "Stream set correctly"); }); +test("Resource: setStream call while resource is being transformed", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + }) + }); + + const p1 = resource.getBuffer(); // Trigger async transformation of stream to buffer + t.throws(() => { + resource.setStream(new Stream.Readable()); // Set new stream while transformation is still ongoing + }, { + message: `Unable to set stream: Content of Resource /my/path/to/resource is currently being transformed` + }); + await p1; // Wait for initial transformation to finish + + t.false(resource.isModified(), "Resource has not been modified"); + + const value = await resource.getString(); + t.is(value, "Stream content", "Initial content still set"); +}); + + test("Resource: clone resource with buffer", async (t) => { t.plan(2); @@ -487,6 +743,89 @@ test("Resource: clone resource with stream", async (t) => { t.is(clonedResourceContent, "Content", "Cloned resource has correct content string"); }); +test("Resource: clone resource with createBuffer factory", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + createStream: () => { + const stream = new Stream.Readable(); + stream._read = function() {}; + stream.push("Stream Content"); + stream.push(null); + return stream; + }, + createBuffer: async () => { + return Buffer.from("Buffer Content"); + } + + }); + + const clonedResource = await resource.clone(); + + const clonedResourceContent = await clonedResource.getString(); + t.is(clonedResourceContent, "Buffer Content", "Cloned resource has correct content string"); +}); + +test("Resource: clone resource with createStream factory", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + createStream: () => { + const stream = new Stream.Readable(); + stream._read = function() {}; + stream.push("Stream Content"); + stream.push(null); + return stream; + }, + }); + + const clonedResource = await resource.clone(); + + const clonedResourceContent = await clonedResource.getString(); + t.is(clonedResourceContent, "Stream Content", "Cloned resource has correct content string"); +}); + +test("Resource: clone resource with stream during transformation to buffer", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource" + }); + const stream = new Stream.Readable(); + stream._read = function() {}; + stream.push("Content"); + stream.push(null); + + resource.setStream(stream); + + const p1 = resource.getBuffer(); // Trigger async transformation of stream to buffer + + const clonedResource = await resource.clone(); + t.pass("Resource cloned"); + await p1; // Wait for initial transformation to finish + + t.is(await resource.getString(), "Content", "Original resource has correct content string"); + t.is(await clonedResource.getString(), "Content", "Cloned resource has correct content string"); +}); + +test("Resource: clone resource while stream is drained/waiting for new content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource" + }); + const stream = new Stream.Readable(); + stream._read = function() {}; + stream.push("Content"); + stream.push(null); + + resource.setStream(stream); + + resource.getStream(); // Drain stream + + const p1 = resource.clone(); // Trigger async clone while stream is drained + + resource.setString("New Content"); + const clonedResource = await p1; // Wait for clone to finish + + t.is(await resource.getString(), "New Content", "Original resource has correct content string"); + t.is(await clonedResource.getString(), "New Content", "Cloned resource has correct content string"); +}); + test("Resource: clone resource with sourceMetadata", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", @@ -553,6 +892,7 @@ test("Resource: create resource with sourceMetadata.contentModified: true", (t) t.true(resource.getSourceMetadata().contentModified, "Modified flag is still true"); t.false(resource.isModified(), "Resource is not modified"); + t.falsy(resource.getLastModified(), "lastModified is not set"); }); test("getStream with createStream callback content: Subsequent content requests should throw error due " + @@ -561,9 +901,13 @@ test("getStream with createStream callback content: Subsequent content requests resource.getStream(); t.throws(() => { resource.getStream(); - }, {message: /Content of Resource \/app\/index.html has been drained/}); - await t.throwsAsync(resource.getBuffer(), {message: /Content of Resource \/app\/index.html has been drained/}); - await t.throwsAsync(resource.getString(), {message: /Content of Resource \/app\/index.html has been drained/}); + }, {message: /Content of Resource \/app\/index.html is currently flagged as drained. Consider using Resource\.getStreamAsync\(\) to wait for new content./}); + await t.throwsAsync(resource.getBuffer(), { + message: /Timeout waiting for content of Resource \/app\/index.html to become available/ + }); + await t.throwsAsync(resource.getString(), { + message: /Timeout waiting for content of Resource \/app\/index.html to become available/ + }); }); test("getStream with Buffer content: Subsequent content requests should throw error due to drained " + @@ -573,9 +917,9 @@ test("getStream with Buffer content: Subsequent content requests should throw er resource.getStream(); t.throws(() => { resource.getStream(); - }, {message: /Content of Resource \/app\/index.html has been drained/}); - await t.throwsAsync(resource.getBuffer(), {message: /Content of Resource \/app\/index.html has been drained/}); - await t.throwsAsync(resource.getString(), {message: /Content of Resource \/app\/index.html has been drained/}); + }, {message: /Content of Resource \/app\/index.html is currently flagged as drained. Consider using Resource\.getStreamAsync\(\) to wait for new content./}); + await t.throwsAsync(resource.getBuffer(), {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); + await t.throwsAsync(resource.getString(), {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); }); test("getStream with Stream content: Subsequent content requests should throw error due to drained " + @@ -594,51 +938,552 @@ test("getStream with Stream content: Subsequent content requests should throw er resource.getStream(); t.throws(() => { resource.getStream(); - }, {message: /Content of Resource \/app\/index.html has been drained/}); - await t.throwsAsync(resource.getBuffer(), {message: /Content of Resource \/app\/index.html has been drained/}); - await t.throwsAsync(resource.getString(), {message: /Content of Resource \/app\/index.html has been drained/}); + }, {message: /Content of Resource \/app\/index.html is currently flagged as drained. Consider using Resource\.getStreamAsync\(\) to wait for new content./}); + await t.throwsAsync(resource.getBuffer(), {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); + await t.throwsAsync(resource.getString(), {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); }); -test("getBuffer from Stream content: Subsequent content requests should not throw error due to drained " + - "content", async (t) => { - const resource = createBasicResource(); - const tStream = new Transform({ - transform(chunk, encoding, callback) { - this.push(chunk.toString()); - callback(); - } +test("getStream from factory content: Prefers createStream factory over createBuffer", async (t) => { + const createBufferStub = sinon.stub().resolves(Buffer.from("Buffer content")); + const createStreamStub = sinon.stub().returns( + new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + })); + const resource = new Resource({ + path: "/my/path/to/resource", + createBuffer: createBufferStub, + createStream: createStreamStub }); - const stream = resource.getStream(); - stream.pipe(tStream); - resource.setStream(tStream); - - const p1 = resource.getBuffer(); - const p2 = resource.getBuffer(); - - await t.notThrowsAsync(p1); - - // Race condition in _getBufferFromStream used to cause p2 - // to throw "Content stream of Resource /app/index.html is flagged as drained." - await t.notThrowsAsync(p2); + const stream = await resource.getStream(); + const streamedResult = await readStream(stream); + t.is(streamedResult, "Stream content", "getStream used createStream factory"); + t.true(createStreamStub.calledOnce, "createStream factory called once"); + t.false(createBufferStub.called, "createBuffer factory not called"); }); -test("Resource: getProject", (t) => { - t.plan(1); +test("getStreamAsync with Buffer content", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - project: {getName: () => "Mock Project"} + buffer: Buffer.from("Content") }); - const project = resource.getProject(); - t.is(project.getName(), "Mock Project"); + + const stream = await resource.getStreamAsync(); + const result = await readStream(stream); + t.is(result, "Content", "Stream has been read correctly"); }); -test("Resource: setProject", (t) => { - t.plan(1); +test("getStreamAsync with createStream callback", async (t) => { + const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); const resource = new Resource({ - path: "/my/path/to/resource" + path: "/my/path/to/resource", + createStream: () => { + return createReadStream(fsPath); + } }); - const project = {getName: () => "Mock Project"}; - resource.setProject(project); + + const stream = await resource.getStreamAsync(); + const result = await readStream(stream); + t.is(result.length, 91, "Stream content has correct length"); +}); + +test("getStreamAsync with Stream content", async (t) => { + const stream = new Stream.Readable(); + stream._read = function() {}; + stream.push("Stream "); + stream.push("content!"); + stream.push(null); + + const resource = new Resource({ + path: "/my/path/to/resource", + stream + }); + + const resultStream = await resource.getStreamAsync(); + const result = await readStream(resultStream); + t.is(result, "Stream content!", "Stream has been read correctly"); +}); + +test("getStreamAsync: Factory content can be used to create new streams after setting new content", async (t) => { + const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); + const createStreamStub = sinon.stub().returns( + new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + })); + const resource = new Resource({ + path: "/my/path/to/resource", + createStream: createStreamStub, + }); + + // First call creates a stream + const stream1 = await resource.getStreamAsync(); + const result1 = await readStream(stream1); + t.is(result1.length, 14, "First stream read successfully"); + t.is(createStreamStub.callCount, 1, "Factory called once"); + + // Content is now drained. To call getStreamAsync again, we need to set new content + // by calling setStream with the factory again + resource.setStream(() => createReadStream(fsPath)); + + const stream2 = await resource.getStreamAsync(); + const result2 = await readStream(stream2); + t.is(result2.length, 91, "Second stream read successfully after resetting content"); +}); + +test("getStreamAsync: Waits for new content after stream is drained", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "Initial content" + }); + + const stream1 = await resource.getStreamAsync(); + const result1 = await readStream(stream1); + t.is(result1, "Initial content", "First stream read successfully"); + + // Content is now drained, set new content + setTimeout(() => { + resource.setString("New content"); + }, 10); + + const stream2 = await resource.getStreamAsync(); + const result2 = await readStream(stream2); + t.is(result2, "New content", "Second stream read successfully after setting new content"); +}); + +test("getStreamAsync: Waits for content transformation to complete", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Initial content"); + this.push(null); + } + }) + }); + + // Start getBuffer which will transform content + const bufferPromise = resource.getBuffer(); + + // Immediately call getStreamAsync while transformation is in progress + const streamPromise = resource.getStreamAsync(); + + // Both should complete successfully + await bufferPromise; + const stream = await streamPromise; + const result = await readStream(stream); + t.is(result, "Initial content", "Stream read successfully after waiting for transformation"); +}); + +test("getStreamAsync with no content throws error", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource" + }); + + await t.throwsAsync(resource.getStreamAsync(), { + message: "Resource /my/path/to/resource has no content" + }); +}); + +test("getStreamAsync from factory content: Prefers createStream factory", async (t) => { + const createBufferStub = sinon.stub().resolves(Buffer.from("Buffer content")); + const createStreamStub = sinon.stub().returns( + new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + })); + const resource = new Resource({ + path: "/my/path/to/resource", + createBuffer: createBufferStub, + createStream: createStreamStub + }); + + const stream = await resource.getStreamAsync(); + const streamedResult = await readStream(stream); + t.is(streamedResult, "Stream content", "getStreamAsync used createStream factory"); + t.true(createStreamStub.calledOnce, "createStream factory called once"); + t.false(createBufferStub.called, "createBuffer factory not called"); +}); + +test("modifyStream: Modify buffer content with transform stream", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "hello world" + }); + + t.false(resource.isModified(), "Resource is not modified initially"); + + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + return Buffer.from(content.toUpperCase()); + }); + + const result = await resource.getString(); + t.is(result, "HELLO WORLD", "Content was modified correctly"); + t.true(resource.isModified(), "Resource is marked as modified"); +}); + +test("modifyStream: Return new stream from callback", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "test content" + }); + + await resource.modifyStream((stream) => { + const transformStream = new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk.toString().toUpperCase()); + callback(); + } + }); + stream.pipe(transformStream); + return transformStream; + }); + + const result = await resource.getString(); + t.is(result, "TEST CONTENT", "Content was modified with transform stream"); +}); + +test("modifyStream: Can modify multiple times", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "test" + }); + + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + return Buffer.from(content + " modified"); + }); + + t.is(await resource.getString(), "test modified", "First modification applied"); + + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + return Buffer.from(content + " again"); + }); + + t.is(await resource.getString(), "test modified again", "Second modification applied"); +}); + +test("modifyStream: Works with factory content", async (t) => { + const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); + const resource = new Resource({ + path: "/my/path/to/resource", + createStream: () => createReadStream(fsPath) + }); + + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + return Buffer.from(content.toUpperCase()); + }); + + const result = await resource.getString(); + t.true(result.includes(""), "Content was read and modified from factory"); +}); + +test("modifyStream: Waits for drained content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "initial" + }); + + // Drain the content + const stream1 = await resource.getStreamAsync(); + await readStream(stream1); + + // Set new content after a delay + setTimeout(() => { + resource.setString("new content"); + }, 10); + + // modifyStream should wait for new content + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + return Buffer.from(content.toUpperCase()); + }); + + const result = await resource.getString(); + t.is(result, "NEW CONTENT", "modifyStream waited for new content and modified it"); +}); + +test("modifyStream: Locks content during modification", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "test" + }); + + const modifyPromise = resource.modifyStream(async (stream) => { + // Simulate slow transformation + await new Promise((resolve) => setTimeout(resolve, 20)); + const content = await readStream(stream); + return Buffer.from(content.toUpperCase()); + }); + + // Try to access content while modification is in progress + // This should wait for the lock to be released + const bufferPromise = resource.getBuffer(); + + await modifyPromise; + const buffer = await bufferPromise; + + t.is(buffer.toString(), "TEST", "Content access waited for modification to complete"); +}); + +test("modifyStream: Throws error if callback returns invalid content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "test" + }); + + await t.throwsAsync( + resource.modifyStream(async (stream) => { + return "not a buffer or stream"; + }), + { + message: "Unable to set new content: Content must be either a Buffer or a Readable Stream" + } + ); +}); + +test("modifyStream: Async callback returning Promise", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "async test" + }); + + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + // Simulate async operation + await new Promise((resolve) => setTimeout(resolve, 5)); + return Buffer.from(content.replace("async", "ASYNC")); + }); + + const result = await resource.getString(); + t.is(result, "ASYNC test", "Async callback worked correctly"); +}); + +test("modifyStream: Sync callback returning Buffer", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "sync test" + }); + + await resource.modifyStream((stream) => { + // Return buffer synchronously + return Buffer.from("SYNC TEST"); + }); + + const result = await resource.getString(); + t.is(result, "SYNC TEST", "Sync callback returning Buffer worked correctly"); +}); + +test("modifyStream: Updates modified flag", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "test", + sourceMetadata: {} + }); + + t.false(resource.isModified(), "Resource is not marked as modified"); + t.false(resource.getSourceMetadata().contentModified, "contentModified is false initially"); + + await resource.modifyStream(async (stream) => { + const content = await readStream(stream); + return Buffer.from(content.toUpperCase()); + }); + + t.true(resource.isModified(), "Resource is marked as modified"); + t.true(resource.getSourceMetadata().contentModified, "contentModified is true after modification"); +}); + +test("getBuffer from Stream content with known size", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + byteSize: 14, + stream: new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + }) + }); + + const p1 = resource.getBuffer(); + const p2 = resource.getBuffer(); + + t.is((await p1).toString(), "Stream content"); + t.is((await p2).toString(), "Stream content"); +}); + +test("getBuffer from Stream content with incorrect size", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + byteSize: 80, + stream: new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + }) + }); + + await await t.throwsAsync(resource.getBuffer(), { + message: `Stream ended early: expected 80 bytes, got 14` + }, `Threw with expected error message`); + + const resource2 = new Resource({ + path: "/my/path/to/resource", + byteSize: 1, + stream: new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + }) + }); + + await await t.throwsAsync(resource2.getBuffer(), { + message: `Stream exceeded expected size: 1, got at least 14` + }, `Threw with expected error message`); +}); + +test("getBuffer from Stream content with stream error", async (t) => { + let destroyCalled = false; + const resource = new Resource({ + path: "/my/path/to/resource", + byteSize: 14, + stream: new Stream.Readable({ + read() { + this.emit("error", new Error("Stream failure")); + }, + destroy(err, callback) { + destroyCalled = true; + // The error will be present when stream.destroy is called due to the error + t.truthy(err, "destroy called with error"); + callback(err); + } + }) + }); + + await t.throwsAsync(resource.getBuffer()); + t.true(destroyCalled, "Stream destroy was called due to error"); +}); + +test("getBuffer from Stream content: Subsequent content requests should not throw error due to drained " + + "content", async (t) => { + const resource = createBasicResource(); + const tStream = new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk.toString()); + callback(); + } + }); + const stream = resource.getStream(); + stream.pipe(tStream); + resource.setStream(tStream); + + const p1 = resource.getBuffer(); + const p2 = resource.getBuffer(); + + await t.notThrowsAsync(p1); + + // Race condition in _getBufferFromStream used to cause p2 + // to throw "Content stream of Resource /app/index.html is flagged as drained." + await t.notThrowsAsync(p2); +}); + +test("getBuffer from Stream content: getBuffer call while stream is consumed and new content is not yet set", + async (t) => { + const resource = createBasicResource(); + const tStream = new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk.toString()); + callback(); + } + }); + const stream = resource.getStream(); + const p1 = resource.getBuffer(); + stream.pipe(tStream); + resource.setStream(tStream); + + const p2 = resource.getBuffer(); + + await t.notThrowsAsync(p1); + + // Race condition in _getBufferFromStream used to cause p2 + // to throw "Content stream of Resource /app/index.html is flagged as drained." + await t.notThrowsAsync(p2); + }); + +test("getBuffer from factory content: Prefers createBuffer factory over createStream", async (t) => { + const createBufferStub = sinon.stub().resolves(Buffer.from("Buffer content")); + const createStreamStub = sinon.stub().returns( + new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + })); + const resource = new Resource({ + path: "/my/path/to/resource", + createBuffer: createBufferStub, + createStream: createStreamStub + }); + const buffer = await resource.getBuffer(); + t.is(buffer.toString(), "Buffer content", "getBuffer used createBuffer factory"); + t.true(createBufferStub.calledOnce, "createBuffer factory called once"); + t.false(createStreamStub.called, "createStream factory not called"); + + // Calling getBuffer again should not call factories again + const buffer2 = await resource.getBuffer(); + t.is(buffer2, buffer, "getBuffer returned same buffer instance"); + t.true(createBufferStub.calledOnce, "createBuffer factory still called only once"); +}); + +test("getBuffer from factory content: Factory does not return buffer instance", async (t) => { + const createBufferStub = sinon.stub().resolves("Buffer content"); + const createStreamStub = sinon.stub().returns( + new Stream.Readable({ + read() { + this.push("Stream content"); + this.push(null); + } + })); + const resource = new Resource({ + path: "/my/path/to/resource", + createBuffer: createBufferStub, + createStream: createStreamStub + }); + await t.throwsAsync(resource.getBuffer(), { + message: `Buffer factory of Resource /my/path/to/resource did not return a Buffer instance` + }, `Threw with expected error message`); + t.true(createBufferStub.calledOnce, "createBuffer factory called once"); + t.false(createStreamStub.called, "createStream factory not called"); +}); + +test("Resource: getProject", (t) => { + t.plan(1); + const resource = new Resource({ + path: "/my/path/to/resource", + project: {getName: () => "Mock Project"} + }); + const project = resource.getProject(); + t.is(project.getName(), "Mock Project"); +}); + +test("Resource: setProject", (t) => { + t.plan(1); + const resource = new Resource({ + path: "/my/path/to/resource" + }); + const project = {getName: () => "Mock Project"}; + resource.setProject(project); t.is(resource.getProject().getName(), "Mock Project"); }); @@ -665,6 +1510,7 @@ test("Resource: constructor with stream", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", stream, + byteSize: 23, sourceMetadata: {} // Needs to be passed in order to get the "modified" state }); @@ -677,9 +1523,9 @@ test("Resource: constructor with stream", async (t) => { t.is(resource.getSourceMetadata().contentModified, false); }); -test("integration stat - resource size", async (t) => { +test("integration stat", async (t) => { const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); - const statInfo = await fs.stat(fsPath); + const statInfo = await stat(fsPath); const resource = new Resource({ path: "/some/path", @@ -689,11 +1535,302 @@ test("integration stat - resource size", async (t) => { } }); t.is(await resource.getSize(), 91); + t.false(resource.isDirectory()); + t.is(resource.getLastModified(), statInfo.mtimeMs); // Setting the same content again should end up with the same size resource.setString(await resource.getString()); t.is(await resource.getSize(), 91); + t.true(resource.getLastModified() > statInfo.mtimeMs, "lastModified should be updated"); resource.setString("myvalue"); t.is(await resource.getSize(), 7); }); + +test("getSize", async (t) => { + const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); + const statInfo = await stat(fsPath); + + const resource = new Resource({ + path: "/some/path", + byteSize: statInfo.size, + createStream: () => { + return createReadStream(fsPath); + } + }); + t.true(resource.hasSize()); + t.is(await resource.getSize(), 91); + + const resourceNoSize = new Resource({ + path: "/some/path", + createStream: () => { + return createReadStream(fsPath); + } + }); + t.false(resourceNoSize.hasSize(), "Resource with createStream and no byteSize has no size"); + t.is(await resourceNoSize.getSize(), 91); +}); + +/* Integrity Glossary + + "Content" = "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=" + "New content" = "sha256-EvQbHDId8MgpzlgZllZv3lKvbK/h0qDHRmzeU+bxPMo=" +*/ +test("getIntegrity: Throws error for directory resource", async (t) => { + const resource = new Resource({ + path: "/my/directory", + isDirectory: true + }); + + await t.throwsAsync(resource.getIntegrity(), { + message: "Unable to calculate integrity for directory resource: /my/directory" + }); +}); + +test("getIntegrity: Returns integrity for buffer content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + buffer: Buffer.from("Content") + }); + + const integrity = await resource.getIntegrity(); + t.is(integrity, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity for content"); +}); + +test("getIntegrity: Returns integrity for stream content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Content"); + this.push(null); + } + }), + }); + + const integrity = await resource.getIntegrity(); + t.is(integrity, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity for content"); +}); + +test("getIntegrity: Returns integrity for factory content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + createStream: () => { + return new Stream.Readable({ + read() { + this.push("Content"); + this.push(null); + } + }); + } + }); + + const integrity = await resource.getIntegrity(); + t.is(integrity, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity for content"); +}); + +test("getIntegrity: Throws error for resource with no content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource" + }); + + await t.throwsAsync(resource.getIntegrity(), { + message: "Resource /my/path/to/resource has no content" + }); +}); + +test("getIntegrity: Different content produces different integrities", async (t) => { + const resource1 = new Resource({ + path: "/my/path/to/resource1", + string: "Content 1" + }); + + const resource2 = new Resource({ + path: "/my/path/to/resource2", + string: "Content 2" + }); + + const integrity1 = await resource1.getIntegrity(); + const integrity2 = await resource2.getIntegrity(); + + t.not(integrity1, integrity2, "Different content produces different integrities"); +}); + +test("getIntegrity: Same content produces same integrity", async (t) => { + const resource1 = new Resource({ + path: "/my/path/to/resource1", + string: "Content" + }); + + const resource2 = new Resource({ + path: "/my/path/to/resource2", + buffer: Buffer.from("Content") + }); + + const resource3 = new Resource({ + path: "/my/path/to/resource2", + stream: new Stream.Readable({ + read() { + this.push("Content"); + this.push(null); + } + }), + }); + + const integrity1 = await resource1.getIntegrity(); + const integrity2 = await resource2.getIntegrity(); + const integrity3 = await resource3.getIntegrity(); + + t.is(integrity1, integrity2, "Same content produces same integrity for string and buffer content"); + t.is(integrity1, integrity3, "Same content produces same integrity for string and stream"); +}); + +test("getIntegrity: Waits for drained content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "Initial content" + }); + + // Drain the stream + await resource.getStreamAsync(); + const p1 = resource.getIntegrity(); // Start getIntegrity which should wait for new content + + resource.setString("New content"); + + const integrity = await p1; + t.is(integrity, "sha256-EvQbHDId8MgpzlgZllZv3lKvbK/h0qDHRmzeU+bxPMo=", + "Correct integrity for new content"); +}); + +test("getIntegrity: Waits for content transformation to complete", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Content"); + this.push(null); + } + }) + }); + + // Start getBuffer which will transform content + const bufferPromise = resource.getBuffer(); + + // Immediately call getIntegrity while transformation is in progress + const integrityPromise = resource.getIntegrity(); + + // Both should complete successfully + await bufferPromise; + const integrity = await integrityPromise; + t.is(integrity, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity after waiting for transformation"); +}); + +test("getIntegrity: Can be called multiple times on buffer content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + buffer: Buffer.from("Content") + }); + + const integrity1 = await resource.getIntegrity(); + const integrity2 = await resource.getIntegrity(); + const integrity3 = await resource.getIntegrity(); + + t.is(integrity1, integrity2, "First and second integrity are identical"); + t.is(integrity2, integrity3, "Second and third integrity are identical"); + + t.is(integrity1, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity for content"); +}); + +test("getIntegrity: Can be called multiple times on factory content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + createStream: () => { + return new Stream.Readable({ + read() { + this.push("Content"); + this.push(null); + } + }); + } + }); + + const integrity1 = await resource.getIntegrity(); + const integrity2 = await resource.getIntegrity(); + const integrity3 = await resource.getIntegrity(); + + t.is(integrity1, integrity2, "First and second integrity are identical"); + t.is(integrity2, integrity3, "Second and third integrity are identical"); + + t.is(integrity1, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity for content"); +}); + +test("getIntegrity: Can be called multiple times on stream content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + stream: new Stream.Readable({ + read() { + this.push("Content"); + this.push(null); + } + }) + }); + + const integrity1 = await resource.getIntegrity(); + const integrity2 = await resource.getIntegrity(); + const integrity3 = await resource.getIntegrity(); + + t.is(integrity1, integrity2, "First and second integrity are identical"); + t.is(integrity2, integrity3, "Second and third integrity are identical"); + + t.is(integrity1, "sha256-R70pB1+LgBnwvuxthr7afJv2eq8FBT3L4LO8tjloUX8=", + "Correct integrity for content"); +}); + +test("getIntegrity: Integrity changes after content modification", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "Original content" + }); + + const integrity1 = await resource.getIntegrity(); + t.is(integrity1, "sha256-OUni2q0Lopc2NkTnXeaaYPNQJNUATQtbAqMWJvtCVNo=", + "Correct integrity for original content"); + + resource.setString("Modified content"); + + const integrity2 = await resource.getIntegrity(); + t.is(integrity2, "sha256-8fba0TDG5CusKMUf/7GVTTxaYjVbRXacQv2lt3RdtT8=", + "Integrity changes after content modification"); + t.not(integrity1, integrity2, "New integrity is different from original"); +}); + +test("getIntegrity: Works with empty content", async (t) => { + const resource = new Resource({ + path: "/my/path/to/resource", + string: "" + }); + + const integrity = await resource.getIntegrity(); + + t.is(integrity, "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "Correct integrity for empty content"); +}); + +test("getIntegrity: Works with large content", async (t) => { + const largeContent = "x".repeat(1024 * 1024); // 1MB of 'x' + const resource = new Resource({ + path: "/my/path/to/resource", + string: largeContent + }); + + const integrity = await resource.getIntegrity(); + + t.is(integrity, "sha256-j5kLoLV3tRzwCeoEk2jBa72hsh4bk74HqCR1i7JTw5s=", + "Correct integrity for large content"); +}); diff --git a/packages/fs/test/lib/ResourceFacade.js b/packages/fs/test/lib/ResourceFacade.js index 5dee2fc8f1c..cabaa1b6748 100644 --- a/packages/fs/test/lib/ResourceFacade.js +++ b/packages/fs/test/lib/ResourceFacade.js @@ -17,6 +17,7 @@ test("Create instance", (t) => { resource }); t.is(resourceFacade.getPath(), "/my/path", "Returns correct path"); + t.is(resourceFacade.getOriginalPath(), "/my/path/to/resource", "Returns correct original path"); t.is(resourceFacade.getName(), "path", "Returns correct name"); t.is(resourceFacade.getConcealedResource(), resource, "Returns correct concealed resource"); }); @@ -86,7 +87,7 @@ test("ResourceFacade provides same public functions as Resource", (t) => { methods.forEach((method) => { t.truthy(resourceFacade[method], `resourceFacade provides function #${method}`); - if (["constructor", "getPath", "getName", "setPath", "clone"].includes(method)) { + if (["constructor", "getPath", "getOriginalPath", "getName", "setPath", "clone"].includes(method)) { // special functions with separate tests return; } diff --git a/packages/fs/test/lib/adapters/FileSystem_write.js b/packages/fs/test/lib/adapters/FileSystem_write.js index 8386c2a5487..0d7a2fb0053 100644 --- a/packages/fs/test/lib/adapters/FileSystem_write.js +++ b/packages/fs/test/lib/adapters/FileSystem_write.js @@ -1,7 +1,11 @@ import path from "node:path"; -import {readFile} from "node:fs/promises"; +import {readFile, writeFile as fsWriteFile} from "node:fs/promises"; import {access as fsAccess, constants as fsConstants, mkdir} from "node:fs/promises"; import {fileURLToPath} from "node:url"; +import {gzipSync} from "node:zlib"; +import {gunzip, createGunzip} from "node:zlib"; +import {promisify} from "node:util"; +import fs from "graceful-fs"; import test from "ava"; import {rimraf} from "rimraf"; import sinon from "sinon"; @@ -116,7 +120,7 @@ test("Write modified resource in drain mode", async (t) => { await t.notThrowsAsync(fileEqual(t, destFsPath, "./test/fixtures/application.a/webapp/index.html")); await t.throwsAsync(resource.getBuffer(), - {message: /Content of Resource \/app\/index.html has been drained/}); + {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); }); test("Write with readOnly and drain options set should fail", async (t) => { @@ -216,7 +220,7 @@ test("Write modified resource into same file in drain mode", async (t) => { await t.notThrowsAsync(fileEqual(t, destFsPath, "./test/fixtures/application.a/webapp/index.html")); await t.throwsAsync(resource.getBuffer(), - {message: /Content of Resource \/app\/index.html has been drained/}); + {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); }); test("Write modified resource into same file in read-only mode", async (t) => { @@ -268,7 +272,7 @@ test("Write new resource in drain mode", async (t) => { await readerWriters.dest.write(resource, {drain: true}); await t.notThrowsAsync(fileContent(t, destFsPath, "Resource content")); await t.throwsAsync(resource.getBuffer(), - {message: /Content of Resource \/app\/index.html has been drained/}); + {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); }); test("Write new resource in read-only mode", async (t) => { @@ -305,3 +309,82 @@ test("Migration of resource is executed", async (t) => { await t.notThrowsAsync(fileEqual(t, destFsPath, "./test/fixtures/application.a/webapp/index.html")); await t.notThrowsAsync(resource.getBuffer(), "Resource content can still be accessed"); }); + +async function createCasResource(t, content, virPath) { + const compressedContent = gzipSync(Buffer.from(content, "utf8")); + const casDir = path.join(t.context.tmpDirPath, ".cas"); + await mkdir(casDir, {recursive: true}); + const casFilePath = path.join(casDir, path.basename(virPath) + ".gz"); + await fsWriteFile(casFilePath, compressedContent); + + return createResource({ + path: virPath, + sourceMetadata: { + adapter: "CAS", + fsPath: casFilePath, + contentModified: false, + }, + createStream: () => { + return fs.createReadStream(casFilePath).pipe(createGunzip()); + }, + createBuffer: async () => { + const compressedBuffer = await promisify(fs.readFile)(casFilePath); + return await promisify(gunzip)(compressedBuffer); + }, + }); +} + +test("Write CAS resource", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + const content = "CAS resource content"; + + const resource = await createCasResource(t, content, "/app/index.html"); + await readerWriters.dest.write(resource); + + await t.notThrowsAsync(fileContent(t, destFsPath, content)); + await t.notThrowsAsync(resource.getBuffer(), "Resource content can still be accessed"); +}); + +test("Write CAS resource in readOnly mode", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + const content = "CAS resource content readOnly"; + + const resource = await createCasResource(t, content, "/app/index.html"); + await readerWriters.dest.write(resource, {readOnly: true}); + + await t.notThrowsAsync(fileContent(t, destFsPath, content)); + await t.notThrowsAsync(fsAccess(destFsPath, fsConstants.R_OK), "File can be read"); + await t.throwsAsync(fsAccess(destFsPath, fsConstants.W_OK), + {message: /EACCES: permission denied|EPERM: operation not permitted/}, + "File can not be written"); + await t.notThrowsAsync(resource.getBuffer(), "Resource content can still be accessed"); +}); + +test("Write CAS resource in drain mode", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + const content = "CAS resource content drain"; + + const resource = await createCasResource(t, content, "/app/index.html"); + await readerWriters.dest.write(resource, {drain: true}); + + await t.notThrowsAsync(fileContent(t, destFsPath, content)); + await t.throwsAsync(resource.getBuffer(), + {message: /Timeout waiting for content of Resource \/app\/index.html to become available./}); +}); + +test("Write modified CAS resource", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + const modifiedContent = "Modified CAS content"; + + const resource = await createCasResource(t, "Original CAS content", "/app/index.html"); + resource.setString(modifiedContent); + + await readerWriters.dest.write(resource); + + await t.notThrowsAsync(fileContent(t, destFsPath, modifiedContent)); + await t.notThrowsAsync(resource.getBuffer(), "Resource content can still be accessed"); +}); diff --git a/packages/fs/test/lib/package-exports.js b/packages/fs/test/lib/package-exports.js index 13201c3d901..9000ba782cb 100644 --- a/packages/fs/test/lib/package-exports.js +++ b/packages/fs/test/lib/package-exports.js @@ -12,7 +12,7 @@ test("export of package.json", (t) => { // Check number of definied exports test("check number of exports", (t) => { const packageJson = require("@ui5/fs/package.json"); - t.is(Object.keys(packageJson.exports).length, 12); + t.is(Object.keys(packageJson.exports).length, 13); }); // Public API contract (exported modules) @@ -74,6 +74,10 @@ test("check number of exports", (t) => { exportedSpecifier: "@ui5/fs/internal/ResourceTagCollection", mappedModule: "../../lib/ResourceTagCollection.js" }, + { + exportedSpecifier: "@ui5/fs/internal/MonitoredResourceTagCollection", + mappedModule: "../../lib/MonitoredResourceTagCollection.js" + }, ].forEach(({exportedSpecifier, mappedModule}) => { test(`${exportedSpecifier}`, async (t) => { const actual = await import(exportedSpecifier); diff --git a/packages/logger/lib/loggers/ProjectBuild.js b/packages/logger/lib/loggers/ProjectBuild.js index 61c81115963..57856e211fe 100644 --- a/packages/logger/lib/loggers/ProjectBuild.js +++ b/packages/logger/lib/loggers/ProjectBuild.js @@ -48,7 +48,7 @@ class ProjectBuild extends Logger { }); } - startTask(taskName) { + startTask(taskName, isDifferentialBuild) { if (!this.#tasksToRun || !this.#tasksToRun.includes(taskName)) { throw new Error(`loggers/ProjectBuild#startTask: Unknown task ${taskName}`); } @@ -59,6 +59,7 @@ class ProjectBuild extends Logger { projectType: this.#projectType, taskName, status: "task-start", + isDifferentialBuild, }); if (!hasListeners) { @@ -66,7 +67,7 @@ class ProjectBuild extends Logger { } } - endTask(taskName) { + endTask(taskName, isDifferentialBuild) { if (!this.#tasksToRun || !this.#tasksToRun.includes(taskName)) { throw new Error(`loggers/ProjectBuild#endTask: Unknown task ${taskName}`); } @@ -77,6 +78,7 @@ class ProjectBuild extends Logger { projectType: this.#projectType, taskName, status: "task-end", + isDifferentialBuild, }); if (!hasListeners) { diff --git a/packages/logger/lib/writers/Console.js b/packages/logger/lib/writers/Console.js index 8243df7720c..846701cf807 100644 --- a/packages/logger/lib/writers/Console.js +++ b/packages/logger/lib/writers/Console.js @@ -334,7 +334,7 @@ class Console { } projectMetadata.buildSkipped = true; message = `${chalk.yellow(figures.tick)} ` + - `Skipping build of ${projectType} project ${chalk.bold(projectName)}`; + chalk.grey(`Skipping build of ${projectType} project ${chalk.bold(projectName)}`); // Update progress bar (if used) // All tasks of this projects are completed @@ -349,7 +349,7 @@ class Console { this.#writeMessage(level, `${chalk.grey(buildIndex)}: ${message}`); } - #handleProjectBuildStatusEvent({level, projectName, projectType, taskName, status}) { + #handleProjectBuildStatusEvent({level, projectName, projectType, taskName, status, isDifferentialBuild}) { const {projectTasks} = this.#getProjectMetadata(projectName); const taskMetadata = projectTasks.get(taskName); if (!taskMetadata) { @@ -382,7 +382,8 @@ class Console { `for project ${projectName}, task ${taskName}`); } taskMetadata.executionStarted = true; - message = `${chalk.blue(figures.pointerSmall)} Running task ${chalk.bold(taskName)}...`; + message = (isDifferentialBuild ? chalk.grey(figures.lozengeOutline) : chalk.blue(figures.pointerSmall)) + + ` Running task ${chalk.bold(taskName)}...`; break; case "task-end": if (taskMetadata.executionEnded) { @@ -412,7 +413,7 @@ class Console { `Task execution already started`); } taskMetadata.executionEnded = true; - message = `${chalk.green(figures.tick)} Skipping task ${chalk.bold(taskName)}`; + message = chalk.yellow(figures.tick) + chalk.grey(` Skipping task ${chalk.bold(taskName)}`); // Update progress bar (if used) this._getProgressBar()?.increment(1); diff --git a/packages/logger/test/lib/loggers/ProjectBuild.js b/packages/logger/test/lib/loggers/ProjectBuild.js index cc8ae00d45a..446732d41fe 100644 --- a/packages/logger/test/lib/loggers/ProjectBuild.js +++ b/packages/logger/test/lib/loggers/ProjectBuild.js @@ -115,6 +115,7 @@ test.serial("Start task", (t) => { projectType: "projectType", status: "task-start", taskName: "task.a", + isDifferentialBuild: undefined, }, "Metadata event has expected payload"); t.is(logHandler.callCount, 0, "No log event emitted"); @@ -135,6 +136,7 @@ test.serial("End task", (t) => { projectType: "projectType", status: "task-end", taskName: "task.a", + isDifferentialBuild: undefined, }, "Metadata event has expected payload"); t.is(logHandler.callCount, 0, "No log event emitted"); diff --git a/packages/project/lib/build/BuildReader.js b/packages/project/lib/build/BuildReader.js new file mode 100644 index 00000000000..51abd136ee3 --- /dev/null +++ b/packages/project/lib/build/BuildReader.js @@ -0,0 +1,165 @@ +import AbstractReader from "@ui5/fs/AbstractReader"; + +/** + * Reader for accessing build results of multiple projects + * + * Provides efficient resource access by delegating to appropriate project readers + * based on resource paths and namespaces. Supports namespace-based routing to + * minimize unnecessary project searches. + * + * @class + * @extends @ui5/fs/AbstractReader + */ +class BuildReader extends AbstractReader { + #projects; + #projectNames; + #applicationProjectName; + #namespaces = new Map(); + #buildServerInterface; + + /** + * Creates a new BuildReader instance + * + * @public + * @param {string} name Name of the reader + * @param {Array<@ui5/project/specifications/Project>} projects Array of projects to read from + * @param {object} buildServerInterface Function that returns a reader for a single project by name + * @throws {Error} If multiple projects share the same namespace + */ + constructor(name, projects, buildServerInterface) { + super(name); + this.#projects = projects; + this.#projectNames = projects.map((p) => p.getName()); + this.#buildServerInterface = buildServerInterface; + + for (const project of projects) { + const ns = project.getNamespace(); + // Not all projects have a namespace, e.g. modules or theme-libraries + if (ns) { + if (this.#namespaces.has(ns)) { + throw new Error(`Multiple projects with namespace '${ns}' found: ` + + `${this.#namespaces.get(ns)} and ${project.getName()}`); + } + this.#namespaces.set(ns, project.getName()); + } + + if (project.getType() === "application") { + this.#applicationProjectName = project.getName(); + } + } + } + + /** + * Locates resources by glob pattern + * + * Retrieves a combined reader for all projects and delegates the glob search to it. + * + * @public + * @param {...*} args Arguments to pass to the underlying reader's byGlob method + * @returns {Promise>} Promise resolving to list of resources + */ + async byGlob(...args) { + const reader = await this.#buildServerInterface.getReaderForProjects(this.#projectNames); + return reader.byGlob(...args); + } + + /** + * Locates a resource by path + * + * Attempts to determine the appropriate project reader based on the resource path + * and namespace. Falls back to searching all projects if the resource cannot be found. + * + * @public + * @param {string} virPath Virtual path of the resource + * @param {...*} args Additional arguments to pass to the underlying reader's byPath method + * @returns {Promise<@ui5/fs/Resource|null>} Promise resolving to resource or null if not found + */ + async byPath(virPath, ...args) { + const reader = await this._getReaderForResource(virPath); + let res = await reader.byPath(virPath, ...args); + if (!res) { + // Fallback to unspecified projects + const allReader = await this.#buildServerInterface.getReaderForProjects(this.#projectNames); + res = await allReader.byPath(virPath, ...args); + } + return res; + } + + /** + * Gets the appropriate reader for a resource at the given path + * + * Determines which project(s) might contain the resource based on namespace matching + * and returns a reader for those projects. For single-project readers, returns that + * project's reader directly. + * + * @param {string} virPath Virtual path of the resource + * @returns {Promise<@ui5/fs/AbstractReader>} Promise resolving to appropriate reader + */ + async _getReaderForResource(virPath) { + if (this.#projects.length === 1) { + // Filtering on a single project (typically the root project) + return await this.#buildServerInterface.getReaderForProject(this.#projectNames[0]); + } + // Determine project for resource path + const projects = this._getProjectsForResourcePath(virPath); + if (projects.length) { + return await this.#buildServerInterface.getReaderForProjects(projects); + } + + // Unable to determine project for resource using path + // Fallback 1: Try to find resource in cached readers (if available) to identify the relevant project + const cachedReader = this.#buildServerInterface.getCachedReadersForProjects(this.#projectNames); + if (cachedReader) { + const res = await cachedReader.byPath(virPath); + if (res) { + // Found resource in one of the cached readers. Assume it still belongs to the associated project + return this.#buildServerInterface.getReaderForProject(res.getProject().getName()); + } + } + + // Fallback 2: If the root project is of type application, and the request does not start with + // /resources/ or /test-resources/, test whether the resource can be found in the root project + if (this.#applicationProjectName && !virPath.startsWith("/resources/") && + !virPath.startsWith("/test-resources/")) { + const appReader = await this.#buildServerInterface.getReaderForProject(this.#applicationProjectName); + const res = await appReader.byPath(virPath); + if (res) { + return appReader; + } + } + + // Fallback to request a reader for all projects + return await this.#buildServerInterface.getReaderForProjects(this.#projectNames); + } + + /** + * Determines which projects might contain the resource for the given path + * + * Analyzes the resource path to identify matching project namespaces. Only processes + * paths starting with /resources/ or /test-resources/. Returns project names in order + * from most specific to least specific namespace match. + * + * @param {string} virPath Virtual resource path + * @returns {string[]} Array of project names that might contain the resource + */ + _getProjectsForResourcePath(virPath) { + if (!virPath.startsWith("/resources/") && !virPath.startsWith("/test-resources/")) { + return []; + } + // Remove first two entries (e.g. "/resources/") + const parts = virPath.split("/").slice(2); + + const projectNames = []; + while (parts.length > 1) { + // Search for namespace, starting with the longest path + parts.pop(); + const ns = parts.join("/"); + if (this.#namespaces.has(ns)) { + projectNames.push(this.#namespaces.get(ns)); + } + } + return projectNames; + } +} + +export default BuildReader; diff --git a/packages/project/lib/build/BuildServer.js b/packages/project/lib/build/BuildServer.js new file mode 100644 index 00000000000..fbf500823bd --- /dev/null +++ b/packages/project/lib/build/BuildServer.js @@ -0,0 +1,512 @@ +import EventEmitter from "node:events"; +import {createReaderCollectionPrioritized} from "@ui5/fs/resourceFactory"; +import BuildReader from "./BuildReader.js"; +import WatchHandler from "./helpers/WatchHandler.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:BuildServer"); +import Cache from "./cache/Cache.js"; + +class AbortBuildError extends Error { + constructor(message) { + super(message); + this.name = "AbortBuildError"; + } +}; + +/** + * Development server that provides access to built project resources with automatic rebuilding + * + * BuildServer watches project sources for changes and automatically rebuilds affected projects + * on-demand. It provides readers for accessing built resources and emits events for build + * completion and source changes. + * + * The server maintains separate readers for: + * - All projects (root + dependencies) + * - Root project only + * - Dependencies only + * + * Projects are built lazily when their resources are first requested, and rebuilt automatically + * when source files change. + * + * @class + * @extends EventEmitter + * @fires BuildServer#buildFinished + * @fires BuildServer#sourcesChanged + * @fires BuildServer#error + */ +class BuildServer extends EventEmitter { + #graph; + #projectBuilder; + #watchHandler; + #rootProjectName; + #resourceChangeQueue = new Map(); + #projectBuildStatus = new Map(); + #pendingBuildRequest = new Set(); + #activeBuild = null; + #processBuildRequestsTimeout; + #allReader; + #rootReader; + #dependenciesReader; + + /** + * Creates a new BuildServer instance + * + * Initializes readers for different project combinations, sets up file watching, + * and optionally performs an initial build of specified dependencies. + * + * @public + * @param {@ui5/project/graph/ProjectGraph} graph Project graph containing all projects + * @param {@ui5/project/build/ProjectBuilder} projectBuilder Builder instance for executing builds + * @param {boolean} initialBuildRootProject Whether to build the root project in the initial build + * @param {string[]} initialBuildIncludedDependencies Project names to include in initial build + * @param {string[]} initialBuildExcludedDependencies Project names to exclude from initial build + */ + constructor( + graph, projectBuilder, + initialBuildRootProject, initialBuildIncludedDependencies, initialBuildExcludedDependencies + ) { + super(); + this.#graph = graph; + this.#rootProjectName = graph.getRoot().getName(); + this.#projectBuilder = projectBuilder; + + const buildServerInterface = { + getReaderForProject: this.#getReaderForProject.bind(this), + getReaderForProjects: this.#getReaderForProjects.bind(this), + getCachedReadersForProjects: this.#getCachedReadersForProjects.bind(this), + }; + + this.#allReader = new BuildReader( + "Build Server: All Projects Reader", Array.from(this.#graph.getProjects()), buildServerInterface); + + const rootProject = this.#graph.getRoot(); + this.#rootReader = new BuildReader("Build Server: Root Project Reader", [rootProject], buildServerInterface); + + const dependencies = graph.getTransitiveDependencies(rootProject.getName()).map((dep) => graph.getProject(dep)); + this.#dependenciesReader = new BuildReader( + "Build Server: Dependencies Reader", dependencies, buildServerInterface); + + // Initialize cache states + this.#projectBuildStatus.set(this.#rootProjectName, new ProjectBuildStatus()); + + for (const dep of dependencies) { + this.#projectBuildStatus.set(dep.getName(), new ProjectBuildStatus()); + } + + if (initialBuildRootProject) { + log.verbose("Enqueueing root project for initial build"); + this.#enqueueBuild(this.#rootProjectName); + } + if (initialBuildIncludedDependencies.length > 0) { + // Enqueue initial build dependencies + for (const projectName of initialBuildIncludedDependencies) { + if (!initialBuildExcludedDependencies.includes(projectName)) { + log.verbose(`Enqueueing project '${projectName}' for initial build`); + this.#enqueueBuild(projectName); + } + } + } + + const watchHandler = new WatchHandler(); + this.#watchHandler = watchHandler; + const allProjects = graph.getProjects(); + watchHandler.watch(allProjects).catch((err) => { + // Error during watch setup + this.emit("error", err); + }); + watchHandler.on("error", (err) => { + this.emit("error", err); + }); + watchHandler.on("change", (eventType, resourcePath, project) => { + log.verbose(`Source change detected: ${eventType} ${resourcePath} in project '${project.getName()}'`); + this._projectResourceChanged(project, resourcePath, ["add", "unlink", "unlinkDir"].includes(eventType)); + }); + } + + async destroy() { + await this.#watchHandler.destroy(); + if (this.#activeBuild) { + // Await active build to finish + await this.#activeBuild; + } + } + + /** + * Gets a reader for all projects (root and dependencies) + * + * Returns a reader that provides access to built resources from all projects in the graph. + * Projects are built on-demand when their resources are requested. + * + * @public + * @returns {BuildReader} Reader for all projects + */ + getReader() { + return this.#allReader; + } + + /** + * Gets a reader for the root project only + * + * Returns a reader that provides access to built resources from only the root project, + * excluding all dependencies. The root project is built on-demand when its resources + * are requested. + * + * @public + * @returns {BuildReader} Reader for root project + */ + getRootReader() { + return this.#rootReader; + } + + /** + * Gets a reader for dependencies only (excluding root project) + * + * Returns a reader that provides access to built resources from all transitive + * dependencies of the root project. Dependencies are built on-demand when their + * resources are requested. + * + * @public + * @returns {BuildReader} Reader for all dependencies + */ + getDependenciesReader() { + return this.#dependenciesReader; + } + + /** + * Gets a reader for a single project, building it if necessary + * + * Checks if the project has already been built and returns its reader from cache. + * If not built, enqueues the project for building and returns a promise that + * resolves when the reader is available. + * + * @param {string} projectName Name of the project to get reader for + * @returns {Promise<@ui5/fs/AbstractReader>} Reader for the built project + */ + async #getReaderForProject(projectName) { + if (!this.#projectBuildStatus.has(projectName)) { + throw new Error(`Project '${projectName}' not found in project graph`); + } + const projectBuildStatus = this.#projectBuildStatus.get(projectName); + const cacheMode = this.#projectBuilder._buildContext.getBuildConfig().cache; + + // When cache=Off, always rebuild - don't use in-memory cached readers + if (cacheMode !== Cache.Off && projectBuildStatus.isFresh()) { + return projectBuildStatus.getReader(); + } + const {promise, resolve, reject} = Promise.withResolvers(); + projectBuildStatus.addReaderRequest({resolve, reject}); + + log.verbose(`Reader for project '${projectName}' is not fresh. Enqueuing build request.`); + this.#enqueueBuild(projectName); + return promise; + } + + /** + * Gets a combined reader for multiple projects, building them if necessary + * + * Enqueues all projects that need to be built and waits for all of them to complete. + * Returns a prioritized collection reader combining all requested projects. + * + * @param {string[]} projectNames Array of project names to get readers for + * @returns {Promise<@ui5/fs/ReaderCollection>} Combined reader for all requested projects + */ + async #getReaderForProjects(projectNames) { + if (projectNames.length === 1) { + return await this.#getReaderForProject(projectNames[0]); + } + const readers = await Promise.all(projectNames.map((projectName) => this.#getReaderForProject(projectName))); + return createReaderCollectionPrioritized({ + name: `Build Server: Reader for projects: ${projectNames.join(", ")}`, + readers + }); + } + + #getCachedReadersForProjects(projectNames) { + const readers = []; + for (const projectName of projectNames) { + const projectBuildStatus = this.#projectBuildStatus.get(projectName); + const reader = projectBuildStatus.getReader(); + if (reader) { + readers.push(reader); + } + } + if (!readers.length) { + return; + } + + return createReaderCollectionPrioritized({ + name: `Build Server: Cached readers for projects: ${projectNames.join(", ")}`, + readers + }); + } + + /** + * Several projects might be affected by the source file change. + * However, at this time we can't tell for sure which ones: + * Only the project builder can determine the affected projects for a given (set of) source file changes. + * This check is only possible while no build is running, and is therefore only done in the batched change handler. + * + * Assuming that the change in source files might corrupt a currently running (or about to be started) build, + * we abort all active builds affecting the changed project or any of its dependents. + * + * @param {@ui5/project/specifications/Project} project Project where the resource change occurred + * @param {string} filePath Path of the affected file + * @param {boolean} fileAddedOrRemoved Whether a file was added or removed + */ + _projectResourceChanged(project, filePath, fileAddedOrRemoved) { + // First, invalidate all potentially affected projects (which also aborts any running builds) + for (const {project: affectedProject} of this.#graph.traverseDependents(project.getName(), true)) { + const projectBuildStatus = this.#projectBuildStatus.get(affectedProject.getName()); + projectBuildStatus.invalidate(`Source change in project '${project.getName()}'`); + if (fileAddedOrRemoved) { + // Reset any cached readers in case files were added or removed + projectBuildStatus.resetReaderCache(); + } + } + + // Enqueue resource change for processing before next build + const queuedChanges = this.#resourceChangeQueue.get(project.getName()); + if (queuedChanges) { + queuedChanges.add(filePath); + } else { + this.#resourceChangeQueue.set(project.getName(), new Set([filePath])); + } + + // : Emit event debounced + // Emit change event immediately so that consumers can react to it (like browser reloading) + // const changedResourcePaths = [...changes.values()].flat(); + // this.emit("sourcesChanged", changedResourcePaths); + } + + #flushResourceChanges() { + if (this.#resourceChangeQueue.size === 0) { + return; + } + const changes = this.#resourceChangeQueue; + this.#resourceChangeQueue = new Map(); + + // Inform project builder + // This is essential so that the project builder can determine changed resources as it does not + // use file watchers or check for all changed files by itself + this.#projectBuilder.resourcesChanged(changes); + } + + /** + * Enqueues a project for building and returns a promise that resolves with its reader + * + * If the project is already queued, returns the existing promise. Otherwise, creates + * a new promise, adds the project to the pending build queue, and triggers queue processing. + * + * @param {string} projectName Name of the project to enqueue + */ + #enqueueBuild(projectName) { + if (this.#pendingBuildRequest.has(projectName)) { + // Already queued + return; + } + + log.verbose(`Enqueuing project '${projectName}' for build`); + + // Add to pending build requests + this.#pendingBuildRequest.add(projectName); + + this.#triggerRequestQueue(); + } + + #triggerRequestQueue() { + if (this.#activeBuild) { + return; + } + // If no build is active, trigger queue processing debounced + if (this.#processBuildRequestsTimeout) { + clearTimeout(this.#processBuildRequestsTimeout); + } + this.#processBuildRequestsTimeout = setTimeout(() => { + this.#processBuildRequests().catch((err) => { + this.emit("error", err); + }); + }, 10); + } + + /** + * Processes the build queue by batching pending projects and building them + * + * Runs while there are pending build requests. Collects all pending projects, + * builds them in a single batch, resolves/rejects promises for built projects, + * and handles errors with proper isolation. + * + * @returns {Promise} Promise that resolves when queue processing is complete + */ + async #processBuildRequests() { + // Process queue while there are pending requests + while (this.#pendingBuildRequest.size > 0) { + // Collect all pending projects for this batch + const projectsToBuild = Array.from(this.#pendingBuildRequest); + let buildRootProject = false; + let dependenciesToBuild; + const rootProjectIdx = projectsToBuild.indexOf(this.#rootProjectName); + if (rootProjectIdx !== -1) { + buildRootProject = true; + dependenciesToBuild = projectsToBuild.toSpliced(rootProjectIdx, 1); + } else { + dependenciesToBuild = projectsToBuild; + } + this.#pendingBuildRequest.clear(); + + log.verbose(`Building projects: ${projectsToBuild.join(", ")}`); + const signal = AbortSignal.any(projectsToBuild.map((projectName) => { + return this.#projectBuildStatus.get(projectName).getAbortSignal(); + })); + + // Process any queued resource changes (must be done before starting the build) + this.#flushResourceChanges(); + + // Set active build to prevent concurrent builds + const buildPromise = this.#activeBuild = this.#projectBuilder.build({ + includeRootProject: buildRootProject, + includedDependencies: dependenciesToBuild, + signal, + }, (projectName, project) => { + // Project has been built and result can be used + const projectBuildStatus = this.#projectBuildStatus.get(projectName); + projectBuildStatus.setReader(project.getReader({style: "runtime"})); + }).catch((err) => { + if (err instanceof AbortBuildError) { + log.info("Build aborted"); + log.verbose(`Projects affected by abort: ${projectsToBuild.join(", ")}`); + // Build was aborted - do not log as error + // Re-queue any outstanding projects + for (const projectName of projectsToBuild) { + const projectBuildStatus = this.#projectBuildStatus.get(projectName); + if (!projectBuildStatus.isFresh()) { + log.verbose(`Re-enqueueing project '${projectName}' after aborted build`); + this.#pendingBuildRequest.add(projectName); + } + } + } else { + log.error(`Build failed: ${err.message}`); + // Build failed - reject promises for projects that weren't built + for (const projectName of projectsToBuild) { + const projectBuildStatus = this.#projectBuildStatus.get(projectName); + projectBuildStatus.rejectReaderRequests(err); + } + // Re-throw to be handled by caller + // TODO: rather emit 'error' event for the BuildServer and continue processing the queue? + // Currently, this.#activeBuild will not be cleared. + throw err; + } + }); + + const builtProjects = await buildPromise; + this.emit("buildFinished", builtProjects); + // Clear active build + this.#activeBuild = null; + if (signal.aborted) { + log.verbose(`Build aborted for projects: ${projectsToBuild.join(", ")}`); + // Do not continue processing the queue if the build was aborted, but re-trigger processing debounced + // to ensure that any source changes are properly queued before the next build. + // This is also essential to re-trigger the build in case all resources changes have already been + // processed while the build was still aborting. Otherwise the build would not be re-triggered. + this.#triggerRequestQueue(); + return; + } + } + } +} + +const PROJECT_STATES = Object.freeze({ + INITIAL: "initial", + INVALIDATED: "invalidated", + // TODO: New state BUILDING + FRESH: "fresh", +}); + +class ProjectBuildStatus { + #state = PROJECT_STATES.INITIAL; + #readerQueue = []; + #reader; + #abortController = new AbortController(); + + invalidate(reason = "Project invalidated") { + if (this.#state === PROJECT_STATES.INVALIDATED) { + // Already invalidated + return; + } + this.#state = PROJECT_STATES.INVALIDATED; + // Ensure any running build is aborted. Then reset the abort controller + this.#abortController.abort(new AbortBuildError(reason)); + this.#abortController = new AbortController(); + } + + abortBuild(reason) { + this.#abortController.abort(reason); + } + + getAbortSignal() { + return this.#abortController.signal; + } + + isFresh() { + return this.#state === PROJECT_STATES.FRESH; + } + + getReader() { + return this.#reader; + } + + setReader(reader) { + this.#reader = reader; + this.#state = PROJECT_STATES.FRESH; + // Resolve any queued getReader promises + for (const {resolve} of this.#readerQueue) { + resolve(reader); + } + this.#readerQueue = []; + } + + resetReaderCache() { + this.#reader = null; + } + + addReaderRequest(promiseResolvers) { + this.#readerQueue.push(promiseResolvers); + } + + rejectReaderRequests(error) { + this.#state = PROJECT_STATES.INVALIDATED; + for (const {reject} of this.#readerQueue) { + reject(error); + } + this.#readerQueue = []; + } +} + +/** + * Build finished event + * + * Emitted when one or more projects have finished building. + * + * @event BuildServer#buildFinished + * @param {string[]} projectNames Array of project names that were built + */ + +/** + * Sources changed event + * + * Emitted when source files have changed and affected projects have been invalidated. + * + * @event BuildServer#sourcesChanged + * @param {string[]} changedResourcePaths Array of changed resource paths + */ + +/** + * Error event + * + * Emitted when an error occurs during watching or building. + * + * @event BuildServer#error + * @param {Error} error The error that occurred + */ + + +export default BuildServer; diff --git a/packages/project/lib/build/ProjectBuilder.js b/packages/project/lib/build/ProjectBuilder.js index 4a805d8a385..85c0caf8f53 100644 --- a/packages/project/lib/build/ProjectBuilder.js +++ b/packages/project/lib/build/ProjectBuilder.js @@ -13,6 +13,8 @@ import OutputStyleEnum from "./helpers/ProjectBuilderOutputStyle.js"; */ class ProjectBuilder { #log; + #buildIsRunning = false; + /** * Build Configuration * @@ -30,6 +32,8 @@ class ProjectBuilder { * @property {Array.} [includedTasks=[]] List of tasks to be included * @property {Array.} [excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. + * @property {module:@ui5/project/build/cache/Cache} [cache=Cache.Default] + * Cache mode to use for building UI5 projects */ /** @@ -118,6 +122,43 @@ class ProjectBuilder { this.#log = new BuildLogger("ProjectBuilder"); } + /** + * Propagate resource changes through the build context + * + * @public + * @param {Array} changes Array of resource changes to propagate + * @returns {Set} Names of projects potentially affected by the resource changes + * @throws {Error} If a build is currently running + */ + resourcesChanged(changes) { + if (this.#buildIsRunning) { + throw new Error(`Unable to safely propagate resource changes. Build is currently running.`); + } + return this._buildContext.propagateResourceChanges(changes); + } + + /** + * Build projects without writing to a target directory + * + * @public + * @param {object} parameters Parameters + * @param {boolean} [parameters.includeRootProject=true] Whether to include the root project + * @param {Array.} [parameters.includedDependencies=[]] List of dependencies to include + * @param {Array.} [parameters.excludedDependencies=[]] List of dependencies to exclude + * @param {AbortSignal} [parameters.signal] Signal to abort the build + * @param {Function} [projectBuiltCallback] Callback invoked after each project is built + * @returns {Promise} Promise resolving with array of processed project names + */ + async build({ + includeRootProject = true, + includedDependencies = [], excludedDependencies = [], + signal, + }, projectBuiltCallback) { + const requestedProjects = this._determineRequestedProjects( + includeRootProject, includedDependencies, excludedDependencies); + return await this.#build(requestedProjects, projectBuiltCallback, signal); + } + /** * Executes a project build, including all necessary or requested dependencies * @@ -136,14 +177,15 @@ class ProjectBuilder { * part of the build result. If this is provided, the other mentioned parameters are ignored. * @returns {Promise} Promise resolving once the build has finished */ - async build({ + async buildToTarget({ destPath, cleanDest = false, includedDependencies = [], excludedDependencies = [], - dependencyIncludes + dependencyIncludes, }) { if (!destPath) { throw new Error(`Missing parameter 'destPath'`); } + if (dependencyIncludes) { if (includedDependencies.length || excludedDependencies.length) { throw new Error( @@ -151,13 +193,52 @@ class ProjectBuilder { "with parameters 'includedDependencies' or 'excludedDependencies"); } } - const rootProjectName = this._graph.getRoot().getName(); - this.#log.info(`Preparing build for project ${rootProjectName}`); - this.#log.info(` Target directory: ${destPath}`); + this.#log.info(`Target directory: ${destPath}`); + const requestedProjects = this._determineRequestedProjects( + true, includedDependencies, excludedDependencies, dependencyIncludes); + + if (cleanDest) { + this.#log.info(`Cleaning target directory...`); + await rmrf(destPath); + } + let fsTarget; + if (!process.env.UI5_BUILD_NO_WRITE_DEST) { + fsTarget = resourceFactory.createAdapter({ + fsBasePath: destPath, + virBasePath: "/" + }); + } + const pWrites = []; + await this.#build(requestedProjects, async (projectName, project, projectBuildContext) => { + if (!fsTarget) { + // Nothing to write to + return; + } + // Only write requested projects to target + // (excluding dependencies that were required to be built, but not requested) + this.#log.verbose(`Writing out files for project ${projectName}...`); + await this._writeResults(projectBuildContext, fsTarget, pWrites); + }); + await Promise.all(pWrites); + } + + /** + * Determine which projects should be built based on filter criteria + * + * @param {boolean} includeRootProject Whether to include the root project + * @param {Array.} includedDependencies Dependencies to include + * @param {Array.} excludedDependencies Dependencies to exclude + * @param {@ui5/project/build/ProjectBuilder~DependencyIncludes} [dependencyIncludes] + * Alternative dependency configuration + * @returns {string[]} Array of project names to build + * @throws {Error} If creating a build manifest with multiple projects + */ + _determineRequestedProjects(includeRootProject, includedDependencies, excludedDependencies, dependencyIncludes) { // Get project filter function based on include/exclude params // (also logs some info to console) - const filterProject = await this._getProjectFilter({ + const filterProject = this._createProjectFilter({ + includeRootProject, explicitIncludes: includedDependencies, explicitExcludes: excludedDependencies, dependencyIncludes @@ -177,18 +258,35 @@ class ProjectBuilder { } } - const projectBuildContexts = await this._createRequiredBuildContexts(requestedProjects); - const cleanupSigHooks = this._registerCleanupSigHooks(); - const fsTarget = resourceFactory.createAdapter({ - fsBasePath: destPath, - virBasePath: "/" - }); + return requestedProjects; + } - const queue = []; - const alreadyBuilt = []; + /** + * Internal build implementation that orchestrates the actual build process + * + * @param {string[]} requestedProjects Array of project names to build + * @param {Function} [projectBuiltCallback] Callback invoked after each project is built + * @param {AbortSignal} [signal] Signal to abort the build + * @returns {Promise} Promise resolving with array of processed project names + * @throws {Error} If a build is already running + */ + async #build(requestedProjects, projectBuiltCallback, signal) { + if (this.#buildIsRunning) { + throw new Error("A build is already running"); + } + this.#buildIsRunning = true; + this.#log.info(`Preparing build for projects: ${requestedProjects.join(", ")}`); + const reqStart = performance.now(); + const projectBuildContexts = await this._buildContext.getRequiredProjectContexts(requestedProjects); + if (this.#log.isLevelEnabled("perf")) { + this.#log.perf( + `getRequiredProjectContexts completed in ${(performance.now() - reqStart).toFixed(2)} ms`); + } // Create build queue based on graph depth-first search to ensure correct build order - await this._graph.traverseDepthFirst(async ({project}) => { + const queue = []; + const processedProjectNames = []; + for (const {project} of this._graph.traverseDependenciesDepthFirst(true)) { const projectName = project.getName(); const projectBuildContext = projectBuildContexts.get(projectName); if (projectBuildContext) { @@ -196,128 +294,115 @@ class ProjectBuilder { // => This project needs to be built or, in case it has already // been built, it's build result needs to be written out (if requested) queue.push(projectBuildContext); - if (!projectBuildContext.requiresBuild()) { - alreadyBuilt.push(projectName); - } + processedProjectNames.push(projectName); } - }); + } this.#log.setProjects(queue.map((projectBuildContext) => { return projectBuildContext.getProject().getName(); })); - if (queue.length > 1) { // Do not log if only the root project is being built - this.#log.info(`Processing ${queue.length} projects`); - if (alreadyBuilt.length) { - this.#log.info(` Reusing build results of ${alreadyBuilt.length} projects`); - this.#log.info(` Building ${queue.length - alreadyBuilt.length} projects`); - } - if (this.#log.isLevelEnabled("verbose")) { - this.#log.verbose(` Required projects:`); - this.#log.verbose(` ${queue - .map((projectBuildContext) => { - const projectName = projectBuildContext.getProject().getName(); - let msg; - if (alreadyBuilt.includes(projectName)) { - const buildMetadata = projectBuildContext.getBuildMetadata(); - const ts = new Date(buildMetadata.timestamp).toUTCString(); - msg = `*> ${projectName} /// already built at ${ts}`; - } else { - msg = `=> ${projectName}`; - } - return msg; - }) - .join("\n ")}`); + const alreadyBuilt = []; + for (const projectBuildContext of queue) { + if (!projectBuildContext.possiblyRequiresBuild()) { + const projectName = projectBuildContext.getProject().getName(); + alreadyBuilt.push(projectName); } } - if (cleanDest) { - this.#log.info(`Cleaning target directory...`); - await rmrf(destPath); - } - const startTime = process.hrtime(); + const cleanupSigHooks = this._registerCleanupSigHooks(); + const pCacheWrites = []; try { - const pWrites = []; - for (const projectBuildContext of queue) { - const projectName = projectBuildContext.getProject().getName(); - const projectType = projectBuildContext.getProject().getType(); + const startTime = process.hrtime(); + while (queue.length) { + const projectBuildContext = queue.shift(); + const project = projectBuildContext.getProject(); + const projectName = project.getName(); + const projectType = project.getType(); this.#log.verbose(`Processing project ${projectName}...`); // Only build projects that are not already build (i.e. provide a matching build manifest) if (alreadyBuilt.includes(projectName)) { this.#log.skipProjectBuild(projectName, projectType); } else { - this.#log.startProjectBuild(projectName, projectType); - await projectBuildContext.getTaskRunner().runTasks(); - this.#log.endProjectBuild(projectName, projectType); + const prepStart = performance.now(); + const usesCache = await projectBuildContext.prepareProjectBuildAndValidateCache(); + if (this.#log.isLevelEnabled("perf")) { + this.#log.perf( + `prepareProjectBuildAndValidateCache for ${projectName} ` + + `completed in ${(performance.now() - prepStart).toFixed(2)} ms ` + + `(usesCache=${usesCache})`); + } + if (usesCache) { + this.#log.skipProjectBuild(projectName, projectType); + alreadyBuilt.push(projectName); + } else { + await this._buildProject(projectBuildContext, signal); + } + } + signal?.throwIfAborted(); + + if (projectBuiltCallback && requestedProjects.includes(projectName)) { + await projectBuiltCallback(projectName, project, projectBuildContext); } - if (!requestedProjects.includes(projectName) || !!process.env.UI5_BUILD_NO_WRITE_DEST) { - // Project has not been requested or writing is disabled - // => Its resources shall not be part of the build result - continue; + + if (!alreadyBuilt.includes(projectName) && !process.env.UI5_BUILD_NO_WRITE_CACHE) { + this.#log.verbose(`Triggering cache update for project ${projectName}...`); + pCacheWrites.push(projectBuildContext.writeBuildCache()); } - this.#log.verbose(`Writing out files...`); - pWrites.push(this._writeResults(projectBuildContext, fsTarget)); + projectBuildContext.buildFinished(); } - await Promise.all(pWrites); this.#log.info(`Build succeeded in ${this._getElapsedTime(startTime)}`); } catch (err) { - this.#log.error(`Build failed in ${this._getElapsedTime(startTime)}`); + this.#log.error(`Build failed`); throw err; } finally { + await Promise.all(pCacheWrites); this._deregisterCleanupSigHooks(cleanupSigHooks); await this._executeCleanupTasks(); + this.#buildIsRunning = false; } + return processedProjectNames; } - async _createRequiredBuildContexts(requestedProjects) { - const requiredProjects = new Set(this._graph.getProjectNames().filter((projectName) => { - return requestedProjects.includes(projectName); - })); - - const projectBuildContexts = new Map(); - - for (const projectName of requiredProjects) { - this.#log.verbose(`Creating build context for project ${projectName}...`); - const projectBuildContext = this._buildContext.createProjectContext({ - project: this._graph.getProject(projectName) - }); - - projectBuildContexts.set(projectName, projectBuildContext); - - if (projectBuildContext.requiresBuild()) { - const taskRunner = projectBuildContext.getTaskRunner(); - const requiredDependencies = await taskRunner.getRequiredDependencies(); + /** + * Build a single project + * + * @param {object} projectBuildContext Build context for the project + * @param {AbortSignal} [signal] Signal to abort the build + * @returns {Promise} Promise resolving with array of changed resources + */ + async _buildProject(projectBuildContext, signal) { + const project = projectBuildContext.getProject(); + const projectName = project.getName(); + const projectType = project.getType(); - if (requiredDependencies.size === 0) { - continue; - } - // This project needs to be built and required dependencies to be built as well - this._graph.getDependencies(projectName).forEach((depName) => { - if (projectBuildContexts.has(depName)) { - // Build context already exists - // => Dependency will be built - return; - } - if (!requiredDependencies.has(depName)) { - return; - } - // Add dependency to list of projects to build - requiredProjects.add(depName); - }); - } - } + this.#log.startProjectBuild(projectName, projectType); + const changedResources = await projectBuildContext.buildProject(signal); + this.#log.endProjectBuild(projectName, projectType); - return projectBuildContexts; + return changedResources; } - async _getProjectFilter({ + /** + * Create a filter function to determine which projects should be built + * + * @param {object} parameters Parameters + * @param {boolean} [parameters.includeRootProject=true] Whether to include the root project + * @param {@ui5/project/build/ProjectBuilder~DependencyIncludes} [parameters.dependencyIncludes] + * Dependency configuration + * @param {Array.} [parameters.explicitIncludes] Explicit dependencies to include + * @param {Array.} [parameters.explicitExcludes] Explicit dependencies to exclude + * @returns {Function} Filter function that takes a project name and returns boolean + */ + _createProjectFilter({ + includeRootProject = true, dependencyIncludes, explicitIncludes, explicitExcludes }) { - const {includedDependencies, excludedDependencies} = await composeProjectList( + const {includedDependencies, excludedDependencies} = composeProjectList( this._graph, dependencyIncludes || { includeDependencyTree: explicitIncludes, @@ -327,15 +412,13 @@ class ProjectBuilder { if (includedDependencies.length) { if (includedDependencies.length === this._graph.getSize() - 1) { - this.#log.info(` Including all dependencies`); + this.#log.info(` Requested all dependencies`); } else { - this.#log.info(` Requested dependencies:`); - this.#log.info(` + ${includedDependencies.join("\n + ")}`); + this.#log.info(` Requested dependencies:\n + ${includedDependencies.join("\n + ")}`); } } if (excludedDependencies.length) { - this.#log.info(` Excluded dependencies:`); - this.#log.info(` - ${excludedDependencies.join("\n + ")}`); + this.#log.info(` Excluded dependencies:\n - ${excludedDependencies.join("\n + ")}`); } const rootProjectName = this._graph.getRoot().getName(); @@ -345,8 +428,8 @@ class ProjectBuilder { dep.test(projectName) : dep === projectName); } - if (projectName === rootProjectName) { - // Always include the root project + if (includeRootProject && projectName === rootProjectName) { + // Include root project return true; } @@ -362,7 +445,15 @@ class ProjectBuilder { }; } - async _writeResults(projectBuildContext, target) { + /** + * Write build results for a project to the target destination + * + * @param {object} projectBuildContext Build context for the project + * @param {@ui5/fs/adapters/FileSystem} target Target adapter to write to + * @param {Array} deferredWork + * @returns {Promise} Promise resolving when write is complete + */ + async _writeResults(projectBuildContext, target, deferredWork) { const project = projectBuildContext.getProject(); const taskUtil = projectBuildContext.getTaskUtil(); const buildConfig = this._buildContext.getBuildConfig(); @@ -385,34 +476,47 @@ class ProjectBuilder { const resources = await reader.byGlob("/**/*"); if (createBuildManifest) { - // Create and write a build manifest metadata file + // Create and write a build manifest metadata file+ const { default: createBuildManifest } = await import("./helpers/createBuildManifest.js"); - const metadata = await createBuildManifest(project, buildConfig, this._buildContext.getTaskRepository()); + const buildManifest = await createBuildManifest( + project, buildConfig, this._buildContext.getTaskRepository(), + projectBuildContext.getBuildSignature()); await target.write(resourceFactory.createResource({ path: `/.ui5/build-manifest.json`, - string: JSON.stringify(metadata, null, "\t") + string: JSON.stringify(buildManifest, null, "\t") })); } - await Promise.all(resources.map((resource) => { + const resourcesToWrite = resources.filter((resource) => { if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) { - this.#log.silly(`Skipping write of resource tagged as "OmitFromBuildResult": ` + + this.#log.silly(`Skipping resource tagged as "OmitFromBuildResult": ` + resource.getPath()); - return; // Skip target write for this resource + return false; // Skip this resource } + return true; + }); + + deferredWork.push( + this._writeToDisk(resourcesToWrite, target, resources, project, isRootProject, outputStyle)); + } + + async _writeToDisk(resourcesToWrite, target, resources, project, isRootProject, outputStyle) { + await Promise.all(resourcesToWrite.map((resource) => { return target.write(resource); })); if (isRootProject && outputStyle === OutputStyleEnum.Flat && - project.getType() !== "application" /* application type is with a default flat build output structure */) { + /* application type is with a default flat build output structure */ + project.getType() !== "application") { const namespace = project.getNamespace(); const libraryResourcesPrefix = `/resources/${namespace}/`; const testResourcesPrefix = "/test-resources/"; const namespacedRegex = new RegExp(`/(resources|test-resources)/${namespace}`); - const processedResourcesSet = resources.reduce((acc, resource) => acc.add(resource.getPath()), new Set()); + const processedResourcesSet = resources.reduce( + (acc, resource) => acc.add(resource.getPath()), new Set()); // If outputStyle === "Flat", then the FlatReader would have filtered // some resources. We now need to get all of the available resources and @@ -431,7 +535,8 @@ class ProjectBuilder { skippedResources.forEach((resource) => { if (resource.originalPath.startsWith(testResourcesPrefix)) { this.#log.verbose( - `Omitting ${resource.originalPath} from build result. File is part of ${testResourcesPrefix}.` + `Omitting ${resource.originalPath} from build result. ` + + `File is part of ${testResourcesPrefix}.` ); } else if (!resource.originalPath.startsWith(libraryResourcesPrefix)) { this.#log.warn( @@ -443,12 +548,23 @@ class ProjectBuilder { } } + /** + * Execute cleanup tasks for all build contexts + * + * @param {boolean} [force] Whether to force cleanup execution + * @returns {Promise} Promise resolving when cleanup is complete + */ async _executeCleanupTasks(force) { this.#log.info("Executing cleanup tasks..."); await this._buildContext.executeCleanupTasks(force); } + /** + * Register signal handlers for cleanup on process termination + * + * @returns {object} Map of signal names to their handlers + */ _registerCleanupSigHooks() { const that = this; function createListener(exitCode) { @@ -490,6 +606,11 @@ class ProjectBuilder { return processSignals; } + /** + * Remove previously registered signal handlers + * + * @param {object} signals Map of signal names to their handlers + */ _deregisterCleanupSigHooks(signals) { for (const signal of Object.keys(signals)) { process.removeListener(signal, signals[signal]); @@ -499,7 +620,6 @@ class ProjectBuilder { /** * Calculates the elapsed build time and returns a prettified output * - * @private * @param {Array} startTime Array provided by process.hrtime() * @returns {string} Difference between now and the provided time array as formatted string */ diff --git a/packages/project/lib/build/TaskRunner.js b/packages/project/lib/build/TaskRunner.js index 0f5677170a3..b24aed34e25 100644 --- a/packages/project/lib/build/TaskRunner.js +++ b/packages/project/lib/build/TaskRunner.js @@ -1,28 +1,31 @@ import {getLogger} from "@ui5/logger"; import composeTaskList from "./helpers/composeTaskList.js"; -import {createReaderCollection} from "@ui5/fs/resourceFactory"; +import {createReaderCollection, createMonitor} from "@ui5/fs/resourceFactory"; /** * TaskRunner * - * @private + * Manages the execution of build tasks for a project, including task composition, + * dependency management, and custom task integration. + * * @hideconstructor */ class TaskRunner { /** * Constructor * - * @param {object} parameters - * @param {object} parameters.graph - * @param {object} parameters.project + * @param {object} parameters Parameters + * @param {@ui5/project/graph/ProjectGraph} parameters.graph Project graph instance + * @param {@ui5/project/specifications/Project} parameters.project Project instance * @param {@ui5/logger/loggers/ProjectBuild} parameters.log Logger to use + * @param {@ui5/project/build/cache/ProjectBuildCache} parameters.buildCache Build cache instance * @param {@ui5/project/build/helpers/TaskUtil} parameters.taskUtil TaskUtil instance * @param {@ui5/builder/tasks/taskRepository} parameters.taskRepository Task repository * @param {@ui5/project/build/ProjectBuilder~BuildConfiguration} parameters.buildConfig * Build configuration */ - constructor({graph, project, log, taskUtil, taskRepository, buildConfig}) { - if (!graph || !project || !log || !taskUtil || !taskRepository || !buildConfig) { + constructor({graph, project, log, buildCache, taskUtil, taskRepository, buildConfig}) { + if (!graph || !project || !log || !buildCache || !taskUtil || !taskRepository || !buildConfig) { throw new Error("TaskRunner: One or more mandatory parameters not provided"); } this._project = project; @@ -31,10 +34,21 @@ class TaskRunner { this._taskRepository = taskRepository; this._buildConfig = buildConfig; this._log = log; + this._buildCache = buildCache; this._directDependencies = new Set(this._taskUtil.getDependencies()); } + /** + * Initializes the task list based on the project type + * + * This method: + * 1. Loads the appropriate build definition for the project type + * 2. Adds all standard tasks from the definition + * 3. Adds any custom tasks configured for the project + * + * @returns {Promise} + */ async _initTasks() { if (this._tasks) { return; @@ -79,30 +93,28 @@ class TaskRunner { } await this._addCustomTasks(); - - // Create readers for *all* dependencies - const depReaders = []; - await this._graph.traverseBreadthFirst(project.getName(), async function({project: dep}) { - if (dep.getName() === project.getName()) { - // Ignore project itself - return; - } - depReaders.push(dep.getReader()); - }); - - this._allDependenciesReader = createReaderCollection({ - name: `Dependency reader collection of project ${project.getName()}`, - readers: depReaders - }); } /** - * Takes a list of tasks which should be executed from the available task list of the current builder + * Executes all configured tasks for the project * - * @returns {Promise} Returns promise resolving once all tasks have been executed + * This method: + * 1. Initializes the task list if not already done + * 2. Ensures dependency reader is ready + * 3. Composes the final list of tasks to execute based on build configuration + * 4. Executes each task in order, respecting cache and abort signals + * 5. Returns the list of changed resources after all tasks complete + * + * @public + * @param {AbortSignal} [signal] Abort signal to cancel task execution + * @returns {Promise} Array of changed resource paths since the last build */ - async runTasks() { + async runTasks(signal) { await this._initTasks(); + + // Ensure cached dependencies reader is initialized and up-to-date (TODO: improve this lifecycle) + await this.getDependenciesReader(this._directDependencies); + const tasksToRun = composeTaskList(Object.keys(this._tasks), this._buildConfig); const allTasks = this._taskExecutionOrder.filter((taskName) => { // There might be a numeric suffix in case a custom task is configured multiple times. @@ -120,22 +132,38 @@ class TaskRunner { }); this._log.setTasks(allTasks); - for (const taskName of allTasks) { + this._buildCache.setTasks(allTasks); + for (let i = 0; i < allTasks.length; i++) { + signal?.throwIfAborted(); + const taskName = allTasks[i]; const taskFunction = this._tasks[taskName].task; + if (i + 1 < allTasks.length) { + this._buildCache.prefetchStageCache(allTasks[i + 1]); + } if (typeof taskFunction === "function") { await this._executeTask(taskName, taskFunction); } } + signal?.throwIfAborted(); + return await this._buildCache.allTasksCompleted(); } /** - * First compiles a list of all tasks that will be executed, then a list of all direct project - * dependencies that those tasks require access to. + * Determines which project dependencies are required by the tasks that will be executed + * + * This method: + * 1. Initializes the task list if needed + * 2. Composes the list of tasks that will be executed + * 3. Collects all dependencies required by those tasks * - * @returns {Set} Returns a set containing the names of all required direct project dependencies + * @public + * @returns {Promise>} Set containing the names of all required direct project dependencies */ async getRequiredDependencies() { + if (this._requiredDependencies) { + return this._requiredDependencies; + } await this._initTasks(); const tasksToRun = composeTaskList(Object.keys(this._tasks), this._buildConfig); const allTasks = this._taskExecutionOrder.filter((taskName) => { @@ -150,7 +178,7 @@ class TaskRunner { const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, ""); return tasksToRun.includes(taskWithoutSuffixCounter); }); - return allTasks.reduce((requiredDependencies, taskName) => { + this._requiredDependencies = allTasks.reduce((requiredDependencies, taskName) => { if (this._tasks[taskName].requiredDependencies.size) { this._log.verbose(`Task ${taskName} for project ${this._project.getName()} requires dependencies`); } @@ -159,20 +187,29 @@ class TaskRunner { } return requiredDependencies; }, new Set()); + return this._requiredDependencies; } /** * Adds an executable task to the builder * - * The order this function is being called defines the build order. FIFO. + * The order this function is called defines the build order (FIFO). + * Tasks can be explicitly skipped by setting taskFunction to null. * - * @param {string} taskName Name of the task which should be in the list availableTasks. - * @param {object} [parameters] - * @param {boolean} [parameters.requiresDependencies] - * @param {object} [parameters.options] - * @param {Function} [parameters.taskFunction] + * @param {string} taskName Name of the task to add + * @param {object} [parameters] Task parameters + * @param {boolean} [parameters.requiresDependencies=false] + * Whether the task requires access to project dependencies + * @param {boolean} [parameters.supportsDifferentialBuilds=false] + * Whether the task supports differential updates using cache + * @param {object} [parameters.options={}] Options to pass to the task + * @param {Function|null} [parameters.taskFunction] + * Task function to execute, or null to explicitly skip the task + * @returns {void} */ - _addTask(taskName, {requiresDependencies = false, options = {}, taskFunction} = {}) { + _addTask(taskName, { + requiresDependencies = false, supportsDifferentialBuilds = false, options = {}, taskFunction + } = {}) { if (this._tasks[taskName]) { throw new Error(`Failed to add duplicate task ${taskName} for project ${this._project.getName()}`); } @@ -190,20 +227,47 @@ class TaskRunner { options.projectName = this._project.getName(); options.projectNamespace = this._project.getNamespace(); + const cacheInfo = await this._buildCache.prepareTaskExecutionAndValidateCache(taskName); + if (cacheInfo === true) { + this._log.skipTask(taskName); + return; + } + const usingCache = !!(supportsDifferentialBuilds && cacheInfo); + const workspace = createMonitor(this._project.getWorkspace()); const params = { - workspace: this._project.getWorkspace(), + workspace, taskUtil: this._taskUtil, - options + options, }; + let dependencies; if (requiresDependencies) { - params.dependencies = this._allDependenciesReader; + dependencies = createMonitor(this._cachedDependenciesReader); + params.dependencies = dependencies; + } + if (usingCache) { + params.changedProjectResourcePaths = cacheInfo.changedProjectResourcePaths; + if (requiresDependencies) { + params.changedDependencyResourcePaths = cacheInfo.changedDependencyResourcePaths; + } } - if (!taskFunction) { - taskFunction = (await this._taskRepository.getTask(taskName)).task; + const {task} = await this._taskRepository.getTask(taskName); + taskFunction = task; + } + this._log.startTask(taskName, usingCache); + this._taskStart = performance.now(); + await taskFunction(params); + if (this._log.isLevelEnabled("perf")) { + this._log.perf( + `Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`); } - return taskFunction(params); + this._log.endTask(taskName); + await this._buildCache.recordTaskResult(taskName, + workspace.getResourceRequests(), + dependencies?.getResourceRequests(), + usingCache ? cacheInfo : undefined, + supportsDifferentialBuilds); }; } this._tasks[taskName] = { @@ -214,8 +278,11 @@ class TaskRunner { } /** + * Adds all custom tasks configured for the project * - * @private + * Processes custom tasks in the order they are defined in the project configuration. + * + * @returns {Promise} */ async _addCustomTasks() { const projectCustomTasks = this._project.getCustomTasks(); @@ -228,10 +295,21 @@ class TaskRunner { } } /** - * Adds custom tasks to execute + * Adds a single custom task to the task execution order + * + * This method: + * 1. Validates the custom task definition + * 2. Loads the task extension from the project graph + * 3. Determines required dependencies via callback if provided + * 4. Creates a wrapper function for the custom task + * 5. Inserts the task at the correct position based on beforeTask/afterTask configuration * - * @private - * @param {object} taskDef + * @param {object} taskDef Custom task definition from project configuration + * @param {string} taskDef.name Name of the custom task + * @param {string} [taskDef.beforeTask] Name of task to insert before + * @param {string} [taskDef.afterTask] Name of task to insert after + * @param {object} [taskDef.configuration] Custom task configuration + * @returns {Promise} */ async _addCustomTask(taskDef) { const project = this._project; @@ -276,6 +354,9 @@ class TaskRunner { // Tasks can provide an optional callback to tell build process which dependencies they require const requiredDependenciesCallback = await task.getRequiredDependenciesCallback(); + // const buildSignatureCallback = await task.getBuildSignatureCallback(); + // const expectedOutputCallback = await task.getExpectedOutputCallback(); + const supportsDifferentialBuildsCallback = await task.getSupportsDifferentialBuildsCallback(); const specVersion = task.getSpecVersion(); let requiredDependencies; @@ -338,6 +419,10 @@ class TaskRunner { } }); } + let supportsDifferentialBuilds = false; + if (specVersion.gte("5.0") && supportsDifferentialBuildsCallback && supportsDifferentialBuildsCallback()) { + supportsDifferentialBuilds = true; + } this._tasks[newTaskName] = { task: this._createCustomTaskWrapper({ @@ -347,9 +432,10 @@ class TaskRunner { taskName: newTaskName, taskConfiguration: taskDef.configuration, provideDependenciesReader, - getDependenciesReader: () => { + supportsDifferentialBuilds, + getDependenciesReaderCb: () => { // Create the dependencies reader on-demand - return this._createDependenciesReader(requiredDependencies); + return this.getDependenciesReader(requiredDependencies); }, }), requiredDependencies @@ -381,10 +467,42 @@ class TaskRunner { } } + /** + * Creates a wrapper function for executing a custom task + * + * The wrapper: + * 1. Validates cache and determines if task can be skipped + * 2. Prepares workspace and dependencies readers + * 3. Builds the parameter object for the custom task interface + * 4. Executes the custom task function + * 5. Records the task result in the build cache + * + * @param {object} parameters Parameters + * @param {@ui5/project/specifications/Project} parameters.project Project instance + * @param {@ui5/project/build/helpers/TaskUtil} parameters.taskUtil TaskUtil instance + * @param {Function} parameters.getDependenciesReaderCb + * Callback to get dependencies reader on-demand + * @param {boolean} parameters.provideDependenciesReader + * Whether to provide dependencies reader to the task + * @param {boolean} parameters.supportsDifferentialBuilds + * Whether the task supports differential updates + * @param {@ui5/project/specifications/Extension} parameters.task Task extension instance + * @param {string} parameters.taskName Runtime name of the task (may include suffix) + * @param {object} [parameters.taskConfiguration] Task configuration from ui5.yaml + * @returns {Function} Async wrapper function for the custom task + */ _createCustomTaskWrapper({ - project, taskUtil, getDependenciesReader, provideDependenciesReader, task, taskName, taskConfiguration + project, taskUtil, getDependenciesReaderCb, provideDependenciesReader, supportsDifferentialBuilds, + task, taskName, taskConfiguration }) { - return async function() { + return async () => { + const cacheInfo = await this._buildCache.prepareTaskExecutionAndValidateCache(taskName); + if (cacheInfo === true) { + this._log.skipTask(taskName); + return; + } + const usingCache = !!(supportsDifferentialBuilds && cacheInfo); + /* Custom Task Interface Parameters: {Object} parameters Parameters @@ -407,14 +525,21 @@ class TaskRunner { Returns: {Promise} Promise resolving with undefined once data has been written */ + const workspace = createMonitor(this._project.getWorkspace()); const params = { - workspace: project.getWorkspace(), + workspace, options: { projectName: project.getName(), projectNamespace: project.getNamespace(), configuration: taskConfiguration, } }; + if (usingCache) { + params.changedProjectResourcePaths = cacheInfo.changedProjectResourcePaths; + if (provideDependenciesReader) { + params.changedDependencyResourcePaths = cacheInfo.changedDependencyResourcePaths; + } + } const specVersion = task.getSpecVersion(); const taskUtilInterface = taskUtil.getInterface(specVersion); // Interface is undefined if specVersion does not support taskUtil @@ -428,36 +553,60 @@ class TaskRunner { params.log = getLogger(`builder:custom-task:${taskName}`); } + let dependencies; if (provideDependenciesReader) { - params.dependencies = await getDependenciesReader(); + dependencies = createMonitor(await getDependenciesReaderCb()); + params.dependencies = dependencies; } - return taskFunction(params); + this._log.startTask(taskName, usingCache); + await taskFunction(params); + this._log.endTask(taskName); + await this._buildCache.recordTaskResult(taskName, + workspace.getResourceRequests(), + dependencies?.getResourceRequests(), + usingCache ? cacheInfo : undefined, + supportsDifferentialBuilds); }; } /** - * Adds progress related functionality to task function. + * Executes a task function with performance tracking + * + * Wraps task execution with performance measurements and logging. * - * @private * @param {string} taskName Name of the task - * @param {Function} taskFunction Function which executed the task + * @param {Function} taskFunction Function which executes the task * @param {object} taskParams Base parameters for all tasks - * @returns {Promise} Resolves when task has finished + * @returns {Promise} Resolves when task has finished */ async _executeTask(taskName, taskFunction, taskParams) { - this._log.startTask(taskName); this._taskStart = performance.now(); await taskFunction(taskParams, this._log); if (this._log.isLevelEnabled("perf")) { + // FIXME: Standard tasks are currently additionally measured within taskFunction (See _addTask). + // The measurement here includes the time for checking whether the task can be skipped via cache. this._log.perf(`Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`); } - this._log.endTask(taskName); } - async _createDependenciesReader(requiredDirectDependencies) { - if (requiredDirectDependencies.size === this._directDependencies.size) { + /** + * Creates a reader collection for the specified project dependencies + * + * This method: + * 1. Returns a cached reader if all direct dependencies are requested and available + * 2. Resolves transitive dependencies for the requested dependency names + * 3. Creates a reader collection containing readers for all required dependencies + * 4. Caches the reader if it covers all direct dependencies + * + * @public + * @param {Set} dependencyNames Set of dependency project names to include + * @param {boolean} [forceUpdate=false] Force creation of a new reader even if cached + * @returns {Promise<@ui5/fs/ReaderCollection>} Reader collection for the requested dependencies + */ + async getDependenciesReader(dependencyNames, forceUpdate = false) { + if (!forceUpdate && dependencyNames.size === this._directDependencies.size && this._cachedDependenciesReader) { // Shortcut: If all direct dependencies are required, just return the already created reader - return this._allDependenciesReader; + return this._cachedDependenciesReader; } const rootProject = this._project; @@ -465,8 +614,8 @@ class TaskRunner { const readers = []; // Add transitive dependencies to set of required dependencies - const requiredDependencies = new Set(requiredDirectDependencies); - for (const projectName of requiredDirectDependencies) { + const requiredDependencies = new Set(dependencyNames); + for (const projectName of dependencyNames) { this._graph.getTransitiveDependencies(projectName).forEach((depName) => { requiredDependencies.add(depName); }); @@ -480,10 +629,15 @@ class TaskRunner { }); // Create a reader collection for that - return createReaderCollection({ + const reader = createReaderCollection({ name: `Reduced dependency reader collection of project ${rootProject.getName()}`, readers }); + + if (dependencyNames.size === this._directDependencies.size) { + this._cachedDependenciesReader = reader; + } + return reader; } } diff --git a/packages/project/lib/build/cache/BuildCacheStorage.js b/packages/project/lib/build/cache/BuildCacheStorage.js new file mode 100644 index 00000000000..4e02888d54b --- /dev/null +++ b/packages/project/lib/build/cache/BuildCacheStorage.js @@ -0,0 +1,441 @@ +import {DatabaseSync} from "node:sqlite"; +import {mkdirSync, existsSync} from "node:fs"; +import path from "node:path"; +import {gzipSync, gunzipSync} from "node:zlib"; +import {getLogger} from "@ui5/logger"; + +const log = getLogger("build:cache:BuildCacheStorage"); + +/** + * Unified SQLite-backed storage for the build cache + * + * Stores both metadata (index caches, stage metadata, task metadata, result metadata) + * and content-addressable resource content (gzip-compressed BLOBs) in a single database. + * + * @class + */ +export default class BuildCacheStorage { + #db; + #stmts; + #dbPath; + #inMetadataBatch = false; + #inContentBatch = false; + + /** + * @param {string} dbDir Directory in which to create the cache.db file + */ + constructor(dbDir) { + mkdirSync(dbDir, {recursive: true}); + this.#dbPath = path.join(dbDir, "cache.db"); + log.verbose(`Opening build cache database: ${this.#dbPath}`); + + this.#db = new DatabaseSync(this.#dbPath); + this.#db.exec("PRAGMA journal_mode=WAL"); + this.#db.exec("PRAGMA synchronous=NORMAL"); + this.#db.exec("PRAGMA busy_timeout=5000"); + this.#db.exec("PRAGMA page_size=8192"); + + this.#createTables(); + this.#prepareStatements(); + } + + #createTables() { + this.#db.exec(` + CREATE TABLE IF NOT EXISTS content ( + integrity TEXT PRIMARY KEY, + data BLOB NOT NULL + ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS index_cache ( + project_id TEXT NOT NULL, + build_signature TEXT NOT NULL, + kind TEXT NOT NULL, + data TEXT NOT NULL, + PRIMARY KEY (project_id, build_signature, kind) + ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS stage_metadata ( + project_id TEXT NOT NULL, + build_signature TEXT NOT NULL, + stage_id TEXT NOT NULL, + stage_signature TEXT NOT NULL, + data TEXT NOT NULL, + PRIMARY KEY (project_id, build_signature, stage_id, stage_signature) + ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS task_metadata ( + project_id TEXT NOT NULL, + build_signature TEXT NOT NULL, + task_name TEXT NOT NULL, + type TEXT NOT NULL, + data TEXT NOT NULL, + PRIMARY KEY (project_id, build_signature, task_name, type) + ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS result_metadata ( + project_id TEXT NOT NULL, + build_signature TEXT NOT NULL, + stage_signature TEXT NOT NULL, + data TEXT NOT NULL, + PRIMARY KEY (project_id, build_signature, stage_signature) + ) WITHOUT ROWID; + `); + } + + #prepareStatements() { + this.#stmts = { + // Content (CAS) + hasContent: this.#db.prepare( + "SELECT 1 FROM content WHERE integrity = ?" + ), + readContent: this.#db.prepare( + "SELECT data FROM content WHERE integrity = ?" + ), + writeContent: this.#db.prepare( + "INSERT OR IGNORE INTO content (integrity, data) VALUES (?, ?)" + ), + + // Index cache + readIndexCache: this.#db.prepare( + "SELECT data FROM index_cache WHERE project_id = ? AND build_signature = ? AND kind = ?" + ), + writeIndexCache: this.#db.prepare( + `INSERT OR REPLACE INTO index_cache (project_id, build_signature, kind, data) + VALUES (?, ?, ?, ?)` + ), + + // Stage metadata + readStageMetadata: this.#db.prepare( + `SELECT data FROM stage_metadata + WHERE project_id = ? AND build_signature = ? AND stage_id = ? AND stage_signature = ?` + ), + writeStageMetadata: this.#db.prepare( + `INSERT OR REPLACE INTO stage_metadata + (project_id, build_signature, stage_id, stage_signature, data) VALUES (?, ?, ?, ?, ?)` + ), + + // Task metadata + readTaskMetadata: this.#db.prepare( + `SELECT data FROM task_metadata + WHERE project_id = ? AND build_signature = ? AND task_name = ? AND type = ?` + ), + writeTaskMetadata: this.#db.prepare( + `INSERT OR REPLACE INTO task_metadata + (project_id, build_signature, task_name, type, data) VALUES (?, ?, ?, ?, ?)` + ), + + // Result metadata + readResultMetadata: this.#db.prepare( + `SELECT data FROM result_metadata + WHERE project_id = ? AND build_signature = ? AND stage_signature = ?` + ), + writeResultMetadata: this.#db.prepare( + `INSERT OR REPLACE INTO result_metadata + (project_id, build_signature, stage_signature, data) VALUES (?, ?, ?, ?)` + ), + }; + } + + /** + * Whether the database connection is open and the database file still exists on disk. + * + * @returns {boolean} + */ + get isValid() { + return this.#db.isOpen && existsSync(this.#dbPath); + } + + // ===== Content (CAS) operations ===== + + /** + * Checks whether content with the given integrity exists in storage + * + * @param {string} integrity SRI integrity string + * @returns {boolean} True if content exists + */ + hasContent(integrity) { + return this.#stmts.hasContent.get(integrity) !== undefined; + } + + /** + * Stores resource content in the CAS + * + * Compresses the buffer with gzip and stores it as a BLOB. + * Deduplicates via INSERT OR IGNORE. + * + * @param {string} integrity SRI integrity string of the uncompressed content + * @param {Buffer} buffer Uncompressed resource content + */ + putContent(integrity, buffer) { + const compressedBuffer = gzipSync(buffer); + this.#stmts.writeContent.run(integrity, compressedBuffer); + } + + /** + * Reads the raw compressed BLOB from the CAS + * + * @param {string} integrity SRI integrity string + * @returns {Buffer} Compressed content buffer + */ + readContentRaw(integrity) { + const row = this.#stmts.readContent.get(integrity); + if (!row) { + throw new Error(`Content not found in CAS for integrity: ${integrity}`); + } + return row.data; + } + + /** + * Reads and decompresses content from the CAS + * + * @param {string} integrity SRI integrity string + * @returns {Buffer} Decompressed content buffer + */ + readContent(integrity) { + return gunzipSync(this.readContentRaw(integrity)); + } + + // ===== Metadata operations ===== + + /** + * Reads resource index cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} kind "source" or "result" + * @returns {object|null} Parsed index cache object or null if not found + */ + readIndexCache(projectId, buildSignature, kind) { + try { + const row = this.#stmts.readIndexCache.get(projectId, buildSignature, kind); + return row ? JSON.parse(row.data) : null; + } catch (err) { + throw new Error( + `Failed to read resource index cache for ` + + `${projectId} / ${buildSignature}: ${err.message}`, + {cause: err} + ); + } + } + + /** + * Writes resource index cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} kind "source" or "result" + * @param {object} index Index object to serialize + */ + writeIndexCache(projectId, buildSignature, kind, index) { + this.#stmts.writeIndexCache.run(projectId, buildSignature, kind, JSON.stringify(index)); + } + + /** + * Reads stage metadata from cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageId Stage identifier + * @param {string} stageSignature Stage signature hash + * @returns {object|null} Parsed stage metadata or null if not found + */ + readStageCache(projectId, buildSignature, stageId, stageSignature) { + try { + const row = this.#stmts.readStageMetadata.get( + projectId, buildSignature, stageId, stageSignature + ); + return row ? JSON.parse(row.data) : null; + } catch (err) { + throw new Error( + `Failed to read stage metadata from cache for ` + + `${projectId} / ${buildSignature} / ${stageId} / ${stageSignature}: ${err.message}`, + {cause: err} + ); + } + } + + /** + * Writes stage metadata to cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageId Stage identifier + * @param {string} stageSignature Stage signature hash + * @param {object} metadata Stage metadata object to serialize + */ + writeStageCache(projectId, buildSignature, stageId, stageSignature, metadata) { + this.#stmts.writeStageMetadata.run( + projectId, buildSignature, stageId, stageSignature, JSON.stringify(metadata) + ); + } + + /** + * Reads task metadata from cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} taskName Task name + * @param {string} type "project" or "dependency" + * @returns {object|null} Parsed task metadata or null if not found + */ + readTaskMetadata(projectId, buildSignature, taskName, type) { + try { + const row = this.#stmts.readTaskMetadata.get( + projectId, buildSignature, taskName, type + ); + return row ? JSON.parse(row.data) : null; + } catch (err) { + throw new Error( + `Failed to read task metadata from cache for ` + + `${projectId} / ${buildSignature} / ${taskName} / ${type}: ${err.message}`, + {cause: err} + ); + } + } + + /** + * Writes task metadata to cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} taskName Task name + * @param {string} type "project" or "dependency" + * @param {object} metadata Task metadata object to serialize + */ + writeTaskMetadata(projectId, buildSignature, taskName, type, metadata) { + this.#stmts.writeTaskMetadata.run( + projectId, buildSignature, taskName, type, JSON.stringify(metadata) + ); + } + + /** + * Reads result metadata from cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageSignature Stage signature hash + * @returns {object|null} Parsed result metadata or null if not found + */ + readResultMetadata(projectId, buildSignature, stageSignature) { + try { + const row = this.#stmts.readResultMetadata.get( + projectId, buildSignature, stageSignature + ); + return row ? JSON.parse(row.data) : null; + } catch (err) { + throw new Error( + `Failed to read result metadata from cache for ` + + `${projectId} / ${buildSignature} / ${stageSignature}: ${err.message}`, + {cause: err} + ); + } + } + + /** + * Writes result metadata to cache + * + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageSignature Stage signature hash + * @param {object} metadata Result metadata object to serialize + */ + writeResultMetadata(projectId, buildSignature, stageSignature, metadata) { + this.#stmts.writeResultMetadata.run( + projectId, buildSignature, stageSignature, JSON.stringify(metadata) + ); + } + + // ===== Batch transactions ===== + + /** + * Begins a metadata batch transaction (outer transaction) + */ + beginMetadataBatch() { + if (!this.#inMetadataBatch) { + this.#db.exec("BEGIN"); + this.#inMetadataBatch = true; + } + } + + /** + * Commits the current metadata batch transaction + */ + endMetadataBatch() { + if (this.#inMetadataBatch) { + this.#db.exec("COMMIT"); + this.#inMetadataBatch = false; + } + } + + /** + * Rolls back the current metadata batch transaction + */ + rollbackMetadataBatch() { + if (this.#inMetadataBatch) { + this.#db.exec("ROLLBACK"); + this.#inMetadataBatch = false; + } + } + + /** + * Begins a content batch transaction. + * Uses SAVEPOINT when nested inside a metadata batch, plain BEGIN otherwise. + */ + beginContentBatch() { + if (this.#inContentBatch) { + return; + } + if (this.#inMetadataBatch) { + this.#db.exec("SAVEPOINT content_batch"); + } else { + this.#db.exec("BEGIN"); + } + this.#inContentBatch = true; + } + + /** + * Commits the current content batch transaction. + * Uses RELEASE when nested inside a metadata batch, plain COMMIT otherwise. + */ + endContentBatch() { + if (!this.#inContentBatch) { + return; + } + if (this.#inMetadataBatch) { + this.#db.exec("RELEASE content_batch"); + } else { + this.#db.exec("COMMIT"); + } + this.#inContentBatch = false; + } + + /** + * Rolls back the current content batch transaction. + * Uses ROLLBACK TO + RELEASE when nested inside a metadata batch, plain ROLLBACK otherwise. + */ + rollbackContentBatch() { + if (!this.#inContentBatch) { + return; + } + if (this.#inMetadataBatch) { + this.#db.exec("ROLLBACK TO content_batch"); + this.#db.exec("RELEASE content_batch"); + } else { + this.#db.exec("ROLLBACK"); + } + this.#inContentBatch = false; + } + + /** + * Closes the database connection + */ + close() { + if (this.#inContentBatch) { + this.rollbackContentBatch(); + } + if (this.#inMetadataBatch) { + this.rollbackMetadataBatch(); + } + this.#db.close(); + } +} diff --git a/packages/project/lib/build/cache/BuildTaskCache.js b/packages/project/lib/build/cache/BuildTaskCache.js new file mode 100644 index 00000000000..d01410f9e50 --- /dev/null +++ b/packages/project/lib/build/cache/BuildTaskCache.js @@ -0,0 +1,294 @@ +import {getLogger} from "@ui5/logger"; +import ResourceRequestManager from "./ResourceRequestManager.js"; +const log = getLogger("build:cache:BuildTaskCache"); + +/** + * @typedef {object} @ui5/project/build/cache/BuildTaskCache~ResourceRequests + * @property {Set} paths Specific resource paths that were accessed + * @property {Set} patterns Glob patterns used to access resources + */ + +/** + * Manages the build cache for a single task + * + * This class tracks all resources accessed by a task (both project and dependency resources) + * and maintains a graph of resource request sets. Each request set represents a unique + * combination of resource accesses, enabling efficient cache invalidation and reuse. + * + * Key features: + * - Tracks resource reads using paths and glob patterns + * - Maintains resource indices for different request combinations + * - Supports incremental updates when resources change + * - Provides cache invalidation based on changed resources + * - Serializes/deserializes cache metadata for persistence + * + * The request graph allows derived request sets (when a task reads additional resources) + * to reuse existing resource indices, optimizing both memory and computation. + * + * @class + */ +export default class BuildTaskCache { + #projectName; + #taskName; + #supportsDifferentialBuilds; + + #projectRequestManager; + #dependencyRequestManager; + + /** + * Creates a new BuildTaskCache instance + * + * @public + * @param {string} projectName Name of the project this task belongs to + * @param {string} taskName Name of the task this cache manages + * @param {boolean} supportsDifferentialBuilds Whether the task supports differential updates + * @param {ResourceRequestManager} [projectRequestManager] Optional pre-existing project request manager from cache + * @param {ResourceRequestManager} [dependencyRequestManager] + * Optional pre-existing dependency request manager from cache + */ + constructor(projectName, taskName, supportsDifferentialBuilds, projectRequestManager, dependencyRequestManager) { + this.#projectName = projectName; + this.#taskName = taskName; + this.#supportsDifferentialBuilds = supportsDifferentialBuilds; + log.verbose(`Initializing BuildTaskCache for task "${taskName}" of project "${this.#projectName}" ` + + `(supportsDifferentialBuilds=${supportsDifferentialBuilds})`); + + this.#projectRequestManager = projectRequestManager ?? + new ResourceRequestManager(projectName, taskName, supportsDifferentialBuilds); + this.#dependencyRequestManager = dependencyRequestManager ?? + new ResourceRequestManager(projectName, taskName, supportsDifferentialBuilds); + } + + /** + * Factory method to restore a BuildTaskCache from cached data + * + * Deserializes previously cached request managers for both project and dependency resources, + * allowing the task cache to resume from a prior build state. + * + * @public + * @param {string} projectName Name of the project + * @param {string} taskName Name of the task + * @param {boolean} supportsDifferentialBuilds Whether the task supports differential updates + * @param {object} projectRequests Cached project request manager data + * @param {object} dependencyRequests Cached dependency request manager data + * @returns {BuildTaskCache} Restored task cache instance + */ + static fromCache(projectName, taskName, supportsDifferentialBuilds, projectRequests, dependencyRequests) { + const projectRequestManager = ResourceRequestManager.fromCache(projectName, taskName, + supportsDifferentialBuilds, projectRequests); + const dependencyRequestManager = ResourceRequestManager.fromCache(projectName, taskName, + supportsDifferentialBuilds, dependencyRequests); + return new BuildTaskCache(projectName, taskName, supportsDifferentialBuilds, + projectRequestManager, dependencyRequestManager); + } + + // ===== METADATA ACCESS ===== + + /** + * Gets the name of the task + * + * @public + * @returns {string} Task name + */ + getTaskName() { + return this.#taskName; + } + + /** + * Checks whether the task supports differential updates + * + * Tasks that support differential updates can use incremental cache invalidation, + * processing only changed resources rather than rebuilding from scratch. + * + * @public + * @returns {boolean} True if differential updates are supported + */ + getSupportsDifferentialBuilds() { + return this.#supportsDifferentialBuilds; + } + + /** + * Checks whether new or modified cache entries exist + * + * Returns true if either the project or dependency request managers have new or + * modified cache entries that need to be persisted. + * + * @public + * @returns {boolean} True if cache entries need to be written + */ + hasNewOrModifiedCacheEntries() { + return this.#projectRequestManager.hasNewOrModifiedCacheEntries() || + this.#dependencyRequestManager.hasNewOrModifiedCacheEntries(); + } + + /** + * Updates project resource indices based on changed resource paths + * + * Processes changed resource paths and updates the project request manager's indices + * accordingly. Only relevant resources (those matching recorded requests) are processed. + * + * @public + * @param {module:@ui5/fs.AbstractReader} projectReader Reader for accessing project resources + * @param {string[]} changedProjectResourcePaths Array of changed project resource paths + * @returns {Promise} True if any index has changed + */ + updateProjectIndices(projectReader, changedProjectResourcePaths) { + return this.#projectRequestManager.updateIndices(projectReader, changedProjectResourcePaths); + } + + /** + * Updates dependency resource indices based on changed resource paths + * + * Processes changed dependency resource paths and updates the dependency request manager's + * indices accordingly. Only relevant resources (those matching recorded requests) are processed. + * + * @public + * @param {module:@ui5/fs.AbstractReader} dependencyReader Reader for accessing dependency resources + * @param {string[]} changedDepResourcePaths Array of changed dependency resource paths + * @returns {Promise} True if any index has changed + */ + updateDependencyIndices(dependencyReader, changedDepResourcePaths) { + return this.#dependencyRequestManager.updateIndices(dependencyReader, changedDepResourcePaths); + } + + /** + * Returns whether this task has any recorded dependency resource requests + * + * @public + * @returns {boolean} + */ + hasDependencyRequests() { + return this.#dependencyRequestManager.hasRequests(); + } + + /** + * Performs a full refresh of the dependency resource index + * + * Since dependency resources may change independently from this project's cache, a full + * refresh of the dependency index is required at the beginning of every build from cache. + * This ensures all dependency resources are current before task execution. + * + * @public + * @param {module:@ui5/fs.AbstractReader} dependencyReader Reader for accessing dependency resources + * @returns {Promise} + */ + refreshDependencyIndices(dependencyReader) { + return this.#dependencyRequestManager.refreshIndices(dependencyReader); + } + + /** + * Gets all project index signatures for this task + * + * Returns signatures from all recorded project-request sets. Each signature represents + * a unique combination of resources, belonging to the current project, that were accessed + * during task execution. These can be used as cache keys for restoring cached task results. + * + * @public + * @returns {string[]} Array of signature strings + * @throws {Error} If resource index is missing for any request set + */ + getProjectIndexSignatures() { + return this.#projectRequestManager.getIndexSignatures(); + } + + /** + * Gets all dependency index signatures for this task + * + * Returns signatures from all recorded dependency-request sets. Each signature represents + * a unique combination of resources, belonging to all dependencies of the current project, + * that were accessed during task execution. These can be used as cache keys for restoring + * cached task results. + * + * @public + * @returns {string[]} Array of signature strings + * @throws {Error} If resource index is missing for any request set + */ + getDependencyIndexSignatures() { + return this.#dependencyRequestManager.getIndexSignatures(); + } + + /** + * Gets all project index delta transitions for differential updates + * + * Returns a map of signature transitions and their associated changed resource paths + * for project resources. Used when tasks support differential updates to identify + * which resources changed between cache states. + * + * @public + * @returns {Map} Map from original signature to delta information + * containing newSignature and changedPaths array + */ + getProjectIndexDeltas() { + return this.#projectRequestManager.getDeltas(); + } + + /** + * Gets all dependency index delta transitions for differential updates + * + * Returns a map of signature transitions and their associated changed resource paths + * for dependency resources. Used when tasks support differential updates to identify + * which dependency resources changed between cache states. + * + * @public + * @returns {Map} Map from original signature to delta information + * containing newSignature and changedPaths array + */ + getDependencyIndexDeltas() { + return this.#dependencyRequestManager.getDeltas(); + } + + /** + * Records resource requests and calculates signatures for the task + * + * This method: + * 1. Processes project and dependency resource requests + * 2. Searches for exact matches in the request graphs + * 3. If found, returns the existing index signatures + * 4. If not found, creates new request sets and resource indices + * 5. Uses tree derivation when possible to reuse parent indices + * + * The returned signatures uniquely identify the set of resources accessed and their + * content, enabling cache lookup for previously executed task results. + * + * @public + * @param {@ui5/project/build/cache/BuildTaskCache~ResourceRequests} projectRequestRecording + * Project resource requests (paths and patterns) + * @param {@ui5/project/build/cache/BuildTaskCache~ResourceRequests|undefined} dependencyRequestRecording + * Dependency resource requests (paths and patterns) + * @param {module:@ui5/fs.AbstractReader} projectReader Reader for accessing project resources + * @param {module:@ui5/fs.AbstractReader} dependencyReader Reader for accessing dependency resources + * @returns {Promise} Array containing [projectSignature, dependencySignature] + */ + async recordRequests(projectRequestRecording, dependencyRequestRecording, projectReader, dependencyReader) { + const { + setId: projectReqSetId, signature: projectReqSignature + } = await this.#projectRequestManager.addRequests(projectRequestRecording, projectReader); + + let dependencyReqSignature; + if (dependencyRequestRecording) { + const { + setId: depReqSetId, signature: depReqSignature + } = await this.#dependencyRequestManager.addRequests(dependencyRequestRecording, dependencyReader); + + this.#projectRequestManager.addAffiliatedRequestSet(projectReqSetId, depReqSetId); + dependencyReqSignature = depReqSignature; + } else { + dependencyReqSignature = this.#dependencyRequestManager.recordNoRequests(); + } + return [projectReqSignature, dependencyReqSignature]; + } + + /** + * Serializes the task cache to plain objects for persistence + * + * Exports both project and dependency resource request graphs in a format suitable + * for JSON serialization. The serialized data can be passed to fromCache() to restore + * the cache state. Returns undefined for request managers with no new or modified entries. + * + * @public + * @returns {Array} Array containing [projectCacheObject, dependencyCacheObject] + */ + toCacheObjects() { + return [this.#projectRequestManager.toCacheObject(), this.#dependencyRequestManager.toCacheObject()]; + } +} diff --git a/packages/project/lib/build/cache/Cache.js b/packages/project/lib/build/cache/Cache.js new file mode 100644 index 00000000000..cf2dd4a1bd6 --- /dev/null +++ b/packages/project/lib/build/cache/Cache.js @@ -0,0 +1,18 @@ +/** + * Cache modes for building UI5 projects + * + * @public + * @readonly + * @enum {string} + * @property {string} Default Use cache if available + * @property {string} Force Use cache only (if it's unavailable or invalid, the build fails) + * @property {string} ReadOnly Do not create or update any cache but make use of a cache if available + * @property {string} Off Do not use any cache and always rebuild + * @module @ui5/project/build/cache/Cache + */ +export default { + Default: "Default", + Force: "Force", + ReadOnly: "ReadOnly", + Off: "Off" +}; diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js new file mode 100644 index 00000000000..ad1007cbcf9 --- /dev/null +++ b/packages/project/lib/build/cache/CacheManager.js @@ -0,0 +1,323 @@ +import path from "node:path"; +import os from "node:os"; +import Configuration from "../../config/Configuration.js"; +import {getLogger} from "@ui5/logger"; +import BuildCacheStorage from "./BuildCacheStorage.js"; + +const log = getLogger("build:cache:CacheManager"); + +// Singleton instances mapped by cache directory path +const cacheManagerInstances = new Map(); + +// Cache version for compatibility management +const CACHE_VERSION = "v0_6"; + +/** + * Manages persistence for the build cache using a unified SQLite-backed storage + * for both metadata and content-addressable resource content + * + * CacheManager delegates metadata operations (index caches, stage metadata, + * task metadata, result metadata) and binary resource content (gzip-compressed + * BLOBs) to BuildCacheStorage (single SQLite database). + * + * The cache is organized by: + * 1. Project ID (package name) + * 2. Build signature (hash of build configuration) + * 3. Stage/task identifiers and signatures + * + * Key features: + * - Content-addressable storage with deduplication + * - Singleton pattern per cache directory + * - Configurable cache location via UI5_DATA_DIR or configuration + * - SQLite-backed storage for fast read/write operations + * + * @class + */ +export default class CacheManager { + #storage; + + /** + * Creates a new CacheManager instance + * + * @private + * @param {string} cacheDir Base directory for the cache + */ + constructor(cacheDir) { + const versionedDir = path.join(cacheDir, CACHE_VERSION); + this.#storage = new BuildCacheStorage(versionedDir); + } + + #isValid() { + return this.#storage.isValid; + } + + /** + * Factory method to create or retrieve a CacheManager instance + * + * Returns a singleton CacheManager for the determined cache directory. + * The cache directory is resolved in this order: + * 1. UI5_DATA_DIR environment variable (resolved relative to cwd) + * 2. ui5DataDir from UI5 configuration file + * 3. Default: ~/.ui5/ + * + * @public + * @param {string} cwd Current working directory for resolving relative paths + * @returns {Promise} Singleton CacheManager instance for the cache directory + */ + static async create(cwd) { + // ENV var should take precedence over the dataDir from the configuration. + let ui5DataDir = process.env.UI5_DATA_DIR; + if (!ui5DataDir) { + const config = await Configuration.fromFile(); + ui5DataDir = config.getUi5DataDir(); + } + if (ui5DataDir) { + ui5DataDir = path.resolve(cwd, ui5DataDir); + } else { + ui5DataDir = path.join(os.homedir(), ".ui5"); + } + const cacheDir = path.join(ui5DataDir, "buildCache"); + log.verbose(`Using build cache directory: ${cacheDir}`); + + if (!cacheManagerInstances.has(cacheDir) || !cacheManagerInstances.get(cacheDir).#isValid()) { + cacheManagerInstances.set(cacheDir, new CacheManager(cacheDir)); + } + return cacheManagerInstances.get(cacheDir); + } + + /** + * Reads resource index cache from storage + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} kind "source" or "result" + * @returns {Promise} Parsed index cache object or null if not found + */ + async readIndexCache(projectId, buildSignature, kind) { + return this.#storage.readIndexCache(projectId, buildSignature, kind); + } + + /** + * Writes resource index cache to storage + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} kind "source" or "result" + * @param {object} index Index object containing resource tree and task metadata + * @returns {Promise} + */ + async writeIndexCache(projectId, buildSignature, kind, index) { + this.#storage.writeIndexCache(projectId, buildSignature, kind, index); + } + + /** + * Reads stage metadata from cache + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageId Stage identifier + * @param {string} stageSignature Stage signature hash + * @returns {Promise} Parsed stage metadata or null if not found + */ + async readStageCache(projectId, buildSignature, stageId, stageSignature) { + return this.#storage.readStageCache(projectId, buildSignature, stageId, stageSignature); + } + + /** + * Writes stage metadata to cache + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageId Stage identifier + * @param {string} stageSignature Stage signature hash + * @param {object} metadata Stage metadata object to serialize + * @returns {Promise} + */ + async writeStageCache(projectId, buildSignature, stageId, stageSignature, metadata) { + this.#storage.writeStageCache(projectId, buildSignature, stageId, stageSignature, metadata); + } + + /** + * Reads task metadata from cache + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} taskName Task name + * @param {string} type "project" or "dependency" + * @returns {Promise} Parsed task metadata or null if not found + */ + async readTaskMetadata(projectId, buildSignature, taskName, type) { + return this.#storage.readTaskMetadata(projectId, buildSignature, taskName, type); + } + + /** + * Writes task metadata to cache + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} taskName Task name + * @param {string} type "project" or "dependency" + * @param {object} metadata Task metadata object to serialize + * @returns {Promise} + */ + async writeTaskMetadata(projectId, buildSignature, taskName, type, metadata) { + this.#storage.writeTaskMetadata(projectId, buildSignature, taskName, type, metadata); + } + + /** + * Reads result metadata from cache + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageSignature Stage signature hash + * @returns {Promise} Parsed result metadata or null if not found + */ + async readResultMetadata(projectId, buildSignature, stageSignature) { + return this.#storage.readResultMetadata(projectId, buildSignature, stageSignature); + } + + /** + * Writes result metadata to cache + * + * @public + * @param {string} projectId Project identifier + * @param {string} buildSignature Build signature hash + * @param {string} stageSignature Stage signature hash + * @param {object} metadata Result metadata object to serialize + * @returns {Promise} + */ + async writeResultMetadata(projectId, buildSignature, stageSignature, metadata) { + this.#storage.writeResultMetadata(projectId, buildSignature, stageSignature, metadata); + } + + /** + * Checks whether content with the given integrity exists in storage + * + * @public + * @param {string} integrity SRI integrity string (e.g., "sha256-...") + * @returns {boolean} True if content exists + */ + hasContent(integrity) { + return this.#storage.hasContent(integrity); + } + + /** + * Reads and decompresses content from the CAS + * + * @public + * @param {string} integrity SRI integrity string + * @returns {Buffer} Decompressed content buffer + */ + readContent(integrity) { + return this.#storage.readContent(integrity); + } + + /** + * Reads the raw compressed BLOB from the CAS + * + * @public + * @param {string} integrity SRI integrity string + * @returns {Buffer} Compressed content buffer + */ + readContentRaw(integrity) { + return this.#storage.readContentRaw(integrity); + } + + /** + * Checks whether content exists for the given integrity hash + * + * @public + * @param {string} integrity Expected integrity hash (e.g., "sha256-...") + * @returns {boolean} True if content exists in storage + * @throws {Error} If integrity is not provided + */ + hasResourceForStage(integrity) { + if (!integrity) { + throw new Error("Integrity hash must be provided to read from cache"); + } + return this.#storage.hasContent(integrity); + } + + /** + * Writes a resource to the content-addressable storage + * + * If the resource content (identified by integrity hash) already exists, + * the write is skipped (deduplication). + * + * @public + * @param {@ui5/fs/Resource} resource Resource to cache + * @returns {Promise} + */ + async writeStageResource(resource) { + const integrity = await resource.getIntegrity(); + const buffer = await resource.getBuffer(); + this.#storage.putContent(integrity, buffer); + } + + /** + * Writes content directly to the CAS with pre-fetched integrity and buffer + * + * @public + * @param {string} integrity SRI integrity string + * @param {Buffer} buffer Uncompressed resource content + */ + putContent(integrity, buffer) { + this.#storage.putContent(integrity, buffer); + } + + /** + * Begins a batch transaction for multiple content writes + */ + beginContentBatch() { + this.#storage.beginContentBatch(); + } + + /** + * Commits the current content batch transaction + */ + endContentBatch() { + this.#storage.endContentBatch(); + } + + /** + * Rolls back the current content batch transaction + */ + rollbackContentBatch() { + this.#storage.rollbackContentBatch(); + } + + /** + * Begins a batch transaction for multiple metadata writes + */ + beginMetadataBatch() { + this.#storage.beginMetadataBatch(); + } + + /** + * Commits the current metadata batch transaction + */ + endMetadataBatch() { + this.#storage.endMetadataBatch(); + } + + /** + * Rolls back the current metadata batch transaction + */ + rollbackMetadataBatch() { + this.#storage.rollbackMetadataBatch(); + } + + /** + * Closes the storage + */ + close() { + this.#storage.close(); + } +} diff --git a/packages/project/lib/build/cache/ProjectBuildCache.js b/packages/project/lib/build/cache/ProjectBuildCache.js new file mode 100644 index 00000000000..3106a0f74cd --- /dev/null +++ b/packages/project/lib/build/cache/ProjectBuildCache.js @@ -0,0 +1,1832 @@ +import {createResource, createProxy, createWriterCollection} from "@ui5/fs/resourceFactory"; +import {getLogger} from "@ui5/logger"; +import {gunzipSync} from "node:zlib"; +import {Readable} from "node:stream"; +import crypto from "node:crypto"; +import BuildTaskCache from "./BuildTaskCache.js"; +import StageCache from "./StageCache.js"; +import ResourceIndex from "./index/ResourceIndex.js"; +import {firstTruthy, matchResourceMetadataStrict} from "./utils.js"; +const log = getLogger("build:cache:ProjectBuildCache"); +import Cache from "./Cache.js"; + +export const INDEX_STATES = Object.freeze({ + RESTORING_PROJECT_INDICES: "restoring_project_indices", + RESTORING_DEPENDENCY_INDICES: "restoring_dependency_indices", + INITIAL: "initial", + FRESH: "fresh", + REQUIRES_UPDATE: "requires_update", +}); + +export const RESULT_CACHE_STATES = Object.freeze({ + PENDING_VALIDATION: "pending_validation", + NO_CACHE: "no_cache", + FRESH_AND_IN_USE: "fresh_and_in_use", +}); + +/** + * @typedef {object} StageMetadata + * @property {Object} resourceMetadata + * Resource metadata indexed by resource path + */ + +/** + * @typedef {object} StageCacheEntry + * @property {string} signature Signature of the cached stage + * @property {@ui5/fs/AbstractReader} stage Reader for the cached stage + * @property {string[]} writtenResourcePaths Array of resource paths written by the task + * @property {Map>} projectTagOperations + * Map of resource paths to their tags that were set or cleared during this stage's execution, for project tags + * @property {Map>} buildTagOperations + * Map of resource paths to their tags that were set or cleared during this stage's execution, for build tags + */ + +export default class ProjectBuildCache { + #taskCache = new Map(); + #stageCache = new StageCache(); + #prefetchedStageReads; + + #project; + #buildSignature; + #cacheManager; + #cacheMode; + #currentProjectReader; + #currentDependencyReader; + #sourceIndex; + #cachedSourceSignature; + #currentStageSignatures = new Map(); + #cachedResultSignature; + #currentResultSignature; + + // Pending changes + #changedProjectSourcePaths = []; + #changedDependencyResourcePaths = []; + #writtenResultResourcePaths = []; + + // Set of integrity hashes known to already exist in CAS from restored stage metadata. + // Populated during the restore phase, consulted during writes to skip redundant CAS lookups. + #knownCasIntegrities = new Set(); + + // Previous build's frozen source resourceMetadata, loaded from cache during #initSourceIndex(). + // Used in #freezeUntransformedSources() to skip re-reading unchanged untransformed source files. + // Updated after each freeze for reuse in subsequent BuildServer builds. + #cachedFrozenSourceMetadata = null; + + #combinedIndexState = INDEX_STATES.RESTORING_PROJECT_INDICES; + #resultCacheState = RESULT_CACHE_STATES.PENDING_VALIDATION; + + /** + * Creates a new ProjectBuildCache instance + * Use ProjectBuildCache.create() instead + * + * @param {@ui5/project/specifications/Project} project Project instance + * @param {string} buildSignature Build signature for the current build + * @param {object} cacheManager Cache manager instance for reading/writing cache data + * @param {string} cacheMode Cache mode to use for building UI5 projects + */ + constructor(project, buildSignature, cacheManager, cacheMode) { + log.verbose( + `ProjectBuildCache for project ${project.getName()} uses build signature ${buildSignature}`); + this.#project = project; + this.#buildSignature = buildSignature; + this.#cacheManager = cacheManager; + this.#cacheMode = cacheMode; + } + + /** + * Factory method to create and initialize a ProjectBuildCache instance + * + * This is the recommended way to create a ProjectBuildCache as it ensures + * proper asynchronous initialization of the resource index and cache loading. + * + * @public + * @param {@ui5/project/specifications/Project} project Project instance + * @param {string} buildSignature Build signature for the current build + * @param {object} cacheManager Cache manager instance + * @param {string} cacheMode Cache mode to use for building UI5 projects + * @returns {Promise<@ui5/project/build/cache/ProjectBuildCache>} Initialized cache instance + */ + static async create(project, buildSignature, cacheManager, cacheMode) { + return new ProjectBuildCache(project, buildSignature, cacheManager, cacheMode); + } + + /** + * Initializes the source index for this project's build cache + * + * This must be called after create() and before any cache operations that depend on the source index. + * Separated from create() to allow parallel initialization of multiple project caches. + * + * @public + * @returns {Promise} + */ + async initSourceIndex() { + // When cache=Off, always reinitialize to clear cached state + if (this.#cacheMode === Cache.Off && this.#combinedIndexState !== INDEX_STATES.RESTORING_PROJECT_INDICES) { + this.#combinedIndexState = INDEX_STATES.RESTORING_PROJECT_INDICES; + } + if (this.#combinedIndexState !== INDEX_STATES.RESTORING_PROJECT_INDICES) { + // Already initialized (e.g. reused across builds) + return; + } + const initStart = performance.now(); + await this.#initSourceIndex(); + if (log.isLevelEnabled("perf")) { + log.perf( + `Initialized source index for project ${this.#project.getName()} ` + + `in ${(performance.now() - initStart).toFixed(2)} ms`); + } + } + + /** + * Sets the dependency reader for accessing dependency resources + * + * The dependency reader is used by tasks to access resources from project + * dependencies. Must be set before tasks that require dependencies are executed. + * + * @public + * @param {@ui5/fs/AbstractReader} dependencyReader Reader for dependency resources + * @returns {Promise} + * Array of changed resource paths since last build, true if cache is fresh, false + * if cache is empty + */ + async prepareProjectBuildAndValidateCache(dependencyReader) { + this.#currentProjectReader = this.#project.getReader(); + + this.#currentDependencyReader = dependencyReader; + + if (this.#combinedIndexState === INDEX_STATES.INITIAL) { + log.verbose(`Project ${this.#project.getName()} has an empty index cache, skipping change processing.`); + return false; + } + + if (this.#combinedIndexState === INDEX_STATES.RESTORING_DEPENDENCY_INDICES) { + if (this.#changedDependencyResourcePaths.length) { + const updateStart = performance.now(); + await this._refreshDependencyIndices(dependencyReader); + if (log.isLevelEnabled("perf")) { + log.perf( + `Initialized dependency indices for project ${this.#project.getName()} ` + + `in ${(performance.now() - updateStart).toFixed(2)} ms`); + } + } else if (log.isLevelEnabled("perf")) { + log.perf( + `Skipping dependency index refresh for project ${this.#project.getName()} ` + + `(no dependency changes propagated)`); + } + this.#combinedIndexState = INDEX_STATES.FRESH; + + // After initializing dependency indices, the result cache must be validated + // This should be it's initial state anyways, so we just verify it here + if (this.#resultCacheState !== RESULT_CACHE_STATES.PENDING_VALIDATION) { + throw new Error(`Unexpected result cache state after restoring dependency indices ` + + `for project ${this.#project.getName()}: ${this.#resultCacheState}`); + } + } + + if (this.#combinedIndexState === INDEX_STATES.REQUIRES_UPDATE) { + const flushStart = performance.now(); + const changesDetected = await this.#flushPendingChanges(); + if (changesDetected) { + this.#resultCacheState = RESULT_CACHE_STATES.PENDING_VALIDATION; + // Force mode: Fail immediately if changes were detected + if (this.#cacheMode === Cache.Force) { + throw new Error( + `Cache is in "Force" mode but cache is stale for project ${this.#project.getName()} ` + + `due to detected source file changes. ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.` + ); + } + } + if (log.isLevelEnabled("perf")) { + log.perf( + `Flushed pending changes for project ${this.#project.getName()} ` + + `in ${(performance.now() - flushStart).toFixed(2)} ms`); + } + this.#combinedIndexState = INDEX_STATES.FRESH; + } + + // When cache=Off, don't validate or use result cache + if (this.#cacheMode === Cache.Off) { + log.verbose(`Cache is in "Off" mode for project ${this.#project.getName()}. ` + + `Skipping result cache validation`); + this.#resultCacheState = RESULT_CACHE_STATES.NO_CACHE; + return false; + } + + if (this.#resultCacheState === RESULT_CACHE_STATES.PENDING_VALIDATION) { + log.verbose(`Project ${this.#project.getName()} cache requires validation due to detected changes.`); + const findStart = performance.now(); + const changedResourcesOrFalse = await this.#findResultCache(); + if (log.isLevelEnabled("perf")) { + log.perf( + `Validated result cache for project ${this.#project.getName()} ` + + `in ${(performance.now() - findStart).toFixed(2)} ms`); + } + if (changedResourcesOrFalse) { + this.#resultCacheState = RESULT_CACHE_STATES.FRESH_AND_IN_USE; + } else { + this.#resultCacheState = RESULT_CACHE_STATES.NO_CACHE; + } + return changedResourcesOrFalse; + } + return this.isFresh(); + } + + /** + * Processes changed resources since last build, updating indices and invalidating tasks as needed + * + * @returns {Promise} + */ + async #flushPendingChanges() { + if (this.#changedProjectSourcePaths.length === 0 && + this.#changedDependencyResourcePaths.length === 0) { + return; + } + let sourceIndexChanged = false; + if (this.#changedProjectSourcePaths.length) { + // Update source index so we can use the signature later as part of the result stage signature + const sourceStart = performance.now(); + sourceIndexChanged = await this.#updateSourceIndex(this.#changedProjectSourcePaths); + if (log.isLevelEnabled("perf")) { + log.perf( + `#flushPendingChanges updateSourceIndex for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - sourceStart).toFixed(2)} ms ` + + `(${this.#changedProjectSourcePaths.length} changed paths, changed=${sourceIndexChanged})`); + } + } + + let depIndicesChanged = false; + if (this.#changedDependencyResourcePaths.length) { + const depStart = performance.now(); + const tasksWithDepRequests = Array.from(this.#taskCache.values()) + .filter((taskCache) => taskCache.hasDependencyRequests()); + await Promise.all(tasksWithDepRequests.map(async (taskCache) => { + const changed = await taskCache + .updateDependencyIndices(this.#currentDependencyReader, this.#changedDependencyResourcePaths); + if (changed) { + depIndicesChanged = true; + } + })); + if (log.isLevelEnabled("perf")) { + log.perf( + `#flushPendingChanges updateDependencyIndices for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - depStart).toFixed(2)} ms ` + + `(${this.#changedDependencyResourcePaths.length} changed paths, ` + + `${tasksWithDepRequests.length}/${this.#taskCache.size} tasks, changed=${depIndicesChanged})`); + } + } + + // Reset pending changes + this.#changedProjectSourcePaths = []; + this.#changedDependencyResourcePaths = []; + + if (sourceIndexChanged || depIndicesChanged) { + // Relevant resources have changed, mark the cache as invalidated + return true; + } else { + log.verbose(`No relevant resource changes detected for project ${this.#project.getName()}`); + } + } + + /** + * Initialize dependency indices for all tasks. This only needs to be called once per build. + * Later builds of the same project during the same overall build can reuse the existing indices + * (they will be updated based on input via dependencyResourcesChanged) + * + * @param {@ui5/fs/AbstractReader} dependencyReader Reader for dependency resources + * @returns {Promise} + */ + async _refreshDependencyIndices(dependencyReader) { + const tasksWithDepRequests = Array.from(this.#taskCache.values()) + .filter((taskCache) => taskCache.hasDependencyRequests()); + await Promise.all(tasksWithDepRequests.map(async (taskCache) => { + await taskCache.refreshDependencyIndices(dependencyReader); + })); + // Reset pending dependency changes since indices are fresh now anyways + this.#changedDependencyResourcePaths = []; + } + + /** + * Checks whether the cache is in a fresh state + * + * @public + * @returns {boolean} True if the cache is fresh + */ + isFresh() { + // When cache=Off, always return false to force rebuilds + if (this.#cacheMode === Cache.Off) { + return false; + } + return this.#combinedIndexState === INDEX_STATES.FRESH && + this.#resultCacheState === RESULT_CACHE_STATES.FRESH_AND_IN_USE; + } + + /** + * Loads a cached result stage from persistent storage if available + * + * Attempts to load a cached result stage using the resource index signature. + * If found, creates a reader for the cached stage and sets it as the project's + * result stage. + * + * @returns {Promise} + * Array of resource paths written by the cached result stage (empty if the result stage remains unchanged), + * or false if no cache found + */ + async #findResultCache() { + const resultSignatures = this.#getPossibleResultStageSignatures(); + if (resultSignatures.includes(this.#currentResultSignature)) { + log.verbose( + `Project ${this.#project.getName()} result stage signature unchanged: ${this.#currentResultSignature}`); + return []; + } + + const res = await firstTruthy(resultSignatures.map(async (resultSignature) => { + const metadata = await this.#cacheManager.readResultMetadata( + this.#project.getId(), this.#buildSignature, resultSignature); + if (!metadata) { + return; + } + return [resultSignature, metadata]; + })); + + if (!res) { + log.verbose( + `No cached stage found for project ${this.#project.getName()}. Searched with ` + + `${resultSignatures.length} possible signatures.`); + return false; + } + const [resultSignature, resultMetadata] = res; + log.verbose(`Found result cache with signature ${resultSignature}`); + const {stageSignatures, sourceStageSignature} = resultMetadata; + + const importStagesStart = log.isLevelEnabled("perf") ? performance.now() : 0; + const writtenResourcePaths = await this.#importStages(stageSignatures); + if (log.isLevelEnabled("perf")) { + log.perf( + `#findResultCache importStages for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - importStagesStart).toFixed(2)} ms ` + + `with ${Object.keys(stageSignatures).length} stages`); + } + + // Restore CAS-backed source reader from the stored source stage + const restoreSourcesStart = log.isLevelEnabled("perf") ? performance.now() : 0; + await this.#restoreFrozenSources(sourceStageSignature); + if (log.isLevelEnabled("perf")) { + log.perf( + `#findResultCache restoreFrozenSources for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - restoreSourcesStart).toFixed(2)} ms`); + } + + log.verbose( + `Using cached result stage for project ${this.#project.getName()} with index signature ${resultSignature}`); + this.#currentResultSignature = resultSignature; + this.#cachedResultSignature = resultSignature; + return writtenResourcePaths; + } + + /** + * Imports cached stages and sets them in the project + * + * @param {Object} stageSignatures Map of stage names to their signatures + * @returns {Promise} Array of resource paths written by all imported stages + */ + async #importStages(stageSignatures) { + const stageNames = Object.keys(stageSignatures); + if (this.#project.getProjectResources().getStage()?.getId() === "initial") { + // Only initialize stages once + this.#project.getProjectResources().initStages(stageNames); + } + const importedStages = await Promise.all(stageNames.map(async (stageName) => { + const stageSignature = stageSignatures[stageName]; + const stageCache = await this.#findStageCache(stageName, [stageSignature]); + if (!stageCache) { + throw new Error(`Inconsistent result cache: Could not find cached stage ` + + `${stageName} with signature ${stageSignature} for project ${this.#project.getName()}`); + } + return [stageName, stageCache]; + })); + this.#project.getProjectResources().useResultStage(); + + // When #currentStageSignatures is empty, this is the initial import from persistent cache. + // The imported stages represent the already-cached state, not actual changes. + // Dependents' dependency indices were restored from the same cache and already reflect these outputs. + // Skip change propagation to avoid redundant dependency index updates in dependents. + const isInitialImport = this.#currentStageSignatures.size === 0; + + const writtenResourcePaths = new Set(); + for (const [stageName, stageCache] of importedStages) { + // Check whether the stage differs form the one currently in use + if (this.#currentStageSignatures.get(stageName)?.join("-") !== stageCache.signature) { + // Set stage + this.#project.getProjectResources().setStage(stageName, stageCache.stage, + stageCache.projectTagOperations, stageCache.buildTagOperations); + + // Store signature for later use in result stage signature calculation + this.#currentStageSignatures.set(stageName, stageCache.signature.split("-")); + + if (!isInitialImport) { + // Cached stage differs from the previous one + // Add all resources written by the cached stage to the set of + // written/potentially changed resources + for (const resourcePath of stageCache.writtenResourcePaths) { + writtenResourcePaths.add(resourcePath); + } + } + } + } + + if (log.isLevelEnabled("perf") && isInitialImport) { + const totalPaths = importedStages.reduce((sum, [, sc]) => sum + sc.writtenResourcePaths.length, 0); + log.perf( + `#importStages: Initial import for project ${this.#project.getName()}, ` + + `suppressed ${totalPaths} resource path propagations`); + } + + return Array.from(writtenResourcePaths); + } + + /** + * Calculates all possible result stage signatures based on current state + * + * @returns {string[]} Array of possible result stage signatures + */ + #getPossibleResultStageSignatures() { + const projectSourceSignature = this.#sourceIndex.getSignature(); + + const taskDependencySignatures = []; + for (const taskCache of this.#taskCache.values()) { + taskDependencySignatures.push(taskCache.getDependencyIndexSignatures()); + } + const dependencySignaturesCombinations = cartesianProduct(taskDependencySignatures); + + return dependencySignaturesCombinations.map((dependencySignatures) => { + const combinedDepSignature = createDependencySignature(dependencySignatures); + return createStageSignature(projectSourceSignature, combinedDepSignature); + }); + } + + /** + * Gets the current result stage signature + * + * @returns {string} Current result stage signature + */ + #getResultStageSignature() { + const projectSourceSignature = this.#sourceIndex.getSignature(); + const dependencySignatures = []; + for (const [, depSignature] of this.#currentStageSignatures.values()) { + dependencySignatures.push(depSignature); + } + const combinedDepSignature = createDependencySignature(dependencySignatures); + return createStageSignature(projectSourceSignature, combinedDepSignature); + } + + // ===== TASK MANAGEMENT ===== + + /** + * Prepares a task for execution by switching to its stage and checking for cached results + * + * This method: + * 1. Switches the project to the task's stage + * 2. Updates task indices if the task has been invalidated + * 3. Attempts to find a cached stage for the task + * 4. Returns whether the task needs to be executed + * + * @public + * @param {string} taskName Name of the task to prepare + * @returns {Promise} + * True if task can use cache, false if task needs execution, + * or an object with cache information for differential updates + */ + async prepareTaskExecutionAndValidateCache(taskName) { + const stageName = this.#getStageNameForTask(taskName); + const taskCache = this.#taskCache.get(taskName); + // Store current project reader (= state of the previous stage) for later use (e.g. in recordTaskResult) + this.#currentProjectReader = this.#project.getReader(); + // Switch project to new stage + this.#project.getProjectResources().useStage(stageName); + log.verbose(`Preparing task execution for task ${taskName} in project ${this.#project.getName()}...`); + if (!taskCache) { + log.verbose(`No task cache found`); + return false; + } + if (this.#writtenResultResourcePaths.length) { + // Update task indices based on source changes and changes from by previous tasks + const updateProjectIndicesStart = performance.now(); + await taskCache.updateProjectIndices(this.#currentProjectReader, this.#writtenResultResourcePaths); + if (log.isLevelEnabled("perf")) { + log.perf( + `Updated project indices for task ${taskName} in project ${this.#project.getName()} ` + + `in ${(performance.now() - updateProjectIndicesStart).toFixed(2)} ms`); + } + } + + // TODO: Implement: + // After index update, try to find cached stages for the new signatures + // let stageSignatures = taskCache.getAffiliatedSignaturePairs(); + + const projectSignatures = taskCache.getProjectIndexSignatures(); + const dependencySignatures = taskCache.getDependencyIndexSignatures(); + const stageSignatures = combineTwoArraysFast( + projectSignatures, + dependencySignatures, + ).map((signaturePair) => { + return createStageSignature(...signaturePair); + }); + + const stageCache = await this.#findStageCache(stageName, stageSignatures); + const oldStageSig = this.#currentStageSignatures.get(stageName)?.join("-"); + if (stageCache) { + this.#project.getProjectResources().setStage(stageName, stageCache.stage, + stageCache.projectTagOperations, stageCache.buildTagOperations); + + // Check whether the stage actually changed + if (stageCache.signature !== oldStageSig) { + // Store new stage signature for later use in result stage signature calculation + this.#currentStageSignatures.set(stageName, stageCache.signature.split("-")); + + // Cached stage likely differs from the previous one (if any) + // Add all resources written by the cached stage to the set of written/potentially changed resources + for (const resourcePath of stageCache.writtenResourcePaths) { + if (!this.#writtenResultResourcePaths.includes(resourcePath)) { + this.#writtenResultResourcePaths.push(resourcePath); + } + } + } + return true; // No need to execute the task + } else { + log.verbose(`No cached stage found for task ${taskName} in project ${this.#project.getName()}. ` + + `Attempting to find delta cached stage...`); + // TODO: Optimize this crazy thing + const projectDeltas = taskCache.getProjectIndexDeltas(); + const depDeltas = taskCache.getDependencyIndexDeltas(); + + // Combine deltas of project stages with cached dependency signatures + const projDeltaSignatures = combineTwoArraysFast( + Array.from(projectDeltas.keys()), + dependencySignatures, + ).map((signaturePair) => { + return createStageSignature(...signaturePair); + }); + // Combine deltas of dependency stages with cached project signatures + const depDeltaSignatures = combineTwoArraysFast( + projectSignatures, + Array.from(depDeltas.keys()), + ).map((signaturePair) => { + return createStageSignature(...signaturePair); + }); + // Combine deltas of both project and dependency stages + const deltaDeltaSignatures = combineTwoArraysFast( + Array.from(projectDeltas.keys()), + Array.from(depDeltas.keys()), + ).map((signaturePair) => { + return createStageSignature(...signaturePair); + }); + const deltaSignatures = [...projDeltaSignatures, ...depDeltaSignatures, ...deltaDeltaSignatures]; + const deltaStageCache = await this.#findStageCache(stageName, deltaSignatures); + if (deltaStageCache) { + // Store dependency signature for later use in result stage signature calculation + const [foundProjectSig, foundDepSig] = deltaStageCache.signature.split("-"); + + // Check whether the stage actually changed + if (oldStageSig !== deltaStageCache.signature) { + // Cached stage likely differs from the previous one (if any) + // Add all resources written by the cached stage to the set of written/potentially changed resources + for (const resourcePath of deltaStageCache.writtenResourcePaths) { + if (!this.#writtenResultResourcePaths.includes(resourcePath)) { + this.#writtenResultResourcePaths.push(resourcePath); + } + } + } + + // Create new signature and determine changed resource paths + const projectDeltaInfo = projectDeltas.get(foundProjectSig); + const dependencyDeltaInfo = depDeltas.get(foundDepSig); + + const newProjSig = projectDeltaInfo?.newSignature ?? foundProjectSig; + const newDepSig = dependencyDeltaInfo?.newSignature ?? foundDepSig; + const newSignature = createStageSignature(newProjSig, newDepSig); + this.#currentStageSignatures.set(stageName, [newProjSig, newDepSig]); + + log.verbose( + `Using delta cached stage for task ${taskName} in project ${this.#project.getName()} ` + + `with original signature ${deltaStageCache.signature} (now ${newSignature}) ` + + `and ${projectDeltaInfo?.changedPaths.length ?? "unknown"} changed project resource paths and ` + + `${dependencyDeltaInfo?.changedPaths.length ?? "unknown"} changed dependency resource paths.`); + + return { + previousStageCache: deltaStageCache, + newSignature: newSignature, + changedProjectResourcePaths: projectDeltaInfo?.changedPaths ?? [], + changedDependencyResourcePaths: dependencyDeltaInfo?.changedPaths ?? [] + }; + } + } + return false; // Task needs to be executed + } + + /** + * Pre-fetches stage cache metadata from persistent storage for the given task. + * Results are stored internally and consumed by #findStageCache when called later. + * + * @public + * @param {string} taskName Task name to prefetch cache for + */ + prefetchStageCache(taskName) { + const taskCache = this.#taskCache.get(taskName); + if (!taskCache) { + return; + } + const stageName = this.#getStageNameForTask(taskName); + + // Compute possible signatures from current index state + const projectSignatures = taskCache.getProjectIndexSignatures(); + const dependencySignatures = taskCache.getDependencyIndexSignatures(); + const stageSignatures = combineTwoArraysFast( + projectSignatures, + dependencySignatures, + ).map((signaturePair) => { + return createStageSignature(...signaturePair); + }); + + if (!stageSignatures.length) { + return; + } + + // Filter out signatures already in memory + const uncachedSignatures = stageSignatures.filter((sig) => + !this.#stageCache.getCacheForSignature(stageName, sig)); + + if (!uncachedSignatures.length) { + return; + } + + // Start reading from disk — store promises, don't await + const prefetchMap = new Map(); + for (const sig of uncachedSignatures) { + prefetchMap.set(sig, this.#cacheManager.readStageCache( + this.#project.getId(), this.#buildSignature, stageName, sig)); + } + this.#prefetchedStageReads = this.#prefetchedStageReads ?? new Map(); + this.#prefetchedStageReads.set(stageName, prefetchMap); + } + + /** + * Attempts to find a cached stage for the given task + * + * Checks both in-memory stage cache and persistent cache storage for a matching + * stage signature. Returns the first matching cached stage found. + * + * @param {string} stageName Name of the stage to find + * @param {string[]} stageSignatures Possible signatures for the stage + * @returns {Promise<@ui5/project/build/cache/ProjectBuildCache~StageCacheEntry|undefined>} + * Cached stage entry or undefined if not found + */ + async #findStageCache(stageName, stageSignatures) { + if (!stageSignatures.length) { + return; + } + // Check cache exists and ensure it's still valid before using it + log.verbose(`Looking for cached stage for task ${stageName} in project ${this.#project.getName()} ` + + `with ${stageSignatures.length} possible signatures:\n - ${stageSignatures.join("\n - ")}`); + for (const stageSignature of stageSignatures) { + const stageCache = this.#stageCache.getCacheForSignature(stageName, stageSignature); + if (stageCache) { + return stageCache; + } + } + + // Check prefetched data + const prefetchMap = this.#prefetchedStageReads?.get(stageName); + if (prefetchMap) { + this.#prefetchedStageReads.delete(stageName); + for (const stageSignature of stageSignatures) { + const promise = prefetchMap.get(stageSignature); + if (promise) { + const stageMetadata = await promise; + if (stageMetadata) { + log.verbose(`Found prefetched cached stage for task ${stageName} ` + + `with signature ${stageSignature}`); + return this.#processStageCacheMetadata(stageName, stageSignature, stageMetadata); + } + } + } + // Filter out already-checked signatures from disk lookup + stageSignatures = stageSignatures.filter((sig) => !prefetchMap.has(sig)); + if (!stageSignatures.length) { + return; + } + } + + // TODO: If list of signatures is longer than N, + // retrieve all available signatures from cache manager first. + // Later maybe add a bloom filter for even larger sets + const stageCache = await firstTruthy(stageSignatures.map(async (stageSignature) => { + const stageMetadata = await this.#cacheManager.readStageCache( + this.#project.getId(), this.#buildSignature, stageName, stageSignature); + if (!stageMetadata) { + return; + } + log.verbose(`Found cached stage for task ${stageName} with signature ${stageSignature}`); + return this.#processStageCacheMetadata(stageName, stageSignature, stageMetadata); + })); + return stageCache; + } + + /** + * Processes stage cache metadata into a stage cache entry + * + * @param {string} stageName Name of the stage + * @param {string} stageSignature Signature of the stage + * @param {object} stageMetadata Raw metadata from cache + * @returns {object} Stage cache entry + */ + #processStageCacheMetadata(stageName, stageSignature, stageMetadata) { + const {resourceMapping, resourceMetadata, projectTagOperations, buildTagOperations} = stageMetadata; + let writtenResourcePaths; + let stageReader; + if (resourceMapping) { + writtenResourcePaths = []; + // Restore writer collection + const readers = resourceMetadata.map((metadata) => { + writtenResourcePaths.push(...Object.keys(metadata)); + return this.#createReaderForStageCache( + stageName, stageSignature, metadata); + }); + + const writerMapping = Object.create(null); + for (const [resourcePath, metadataIndex] of Object.entries(resourceMapping)) { + if (!readers[metadataIndex]) { + throw new Error(`Inconsistent stage cache: No resource metadata ` + + `found at index ${metadataIndex} for resource ${resourcePath}`); + } + writerMapping[resourcePath] = readers[metadataIndex]; + } + + stageReader = createWriterCollection({ + name: `Restored cached stage ${stageName} for project ${this.#project.getName()}`, + writerMapping, + }); + } else { + writtenResourcePaths = Object.keys(resourceMetadata); + stageReader = this.#createReaderForStageCache(stageName, stageSignature, resourceMetadata); + } + + this.#collectKnownIntegrities(resourceMetadata); + + return { + signature: stageSignature, + stage: stageReader, + writtenResourcePaths, + projectTagOperations: tagOpsToMap(projectTagOperations), + buildTagOperations: tagOpsToMap(buildTagOperations), + }; + } + + /** + * Records the result of a task execution and updates the cache + * + * This method: + * 1. Creates a signature for the executed task based on its resource requests + * 2. Stores the resulting stage in the stage cache using that signature + * 3. Invalidates downstream tasks if they depend on written resources + * 4. Removes the task from the invalidated tasks list + * + * @public + * @param {string} taskName Name of the executed task + * @param {@ui5/project/build/cache/BuildTaskCache~ResourceRequests} projectResourceRequests + * Resource requests for project resources + * @param {@ui5/project/build/cache/BuildTaskCache~ResourceRequests|undefined} dependencyResourceRequests + * Resource requests for dependency resources + * @param {object} cacheInfo Cache information for differential updates + * @param {boolean} supportsDifferentialBuilds Whether the task supports differential updates + * @returns {Promise} + */ + async recordTaskResult( + taskName, projectResourceRequests, dependencyResourceRequests, cacheInfo, supportsDifferentialBuilds + ) { + const recordStart = performance.now(); + if (!this.#taskCache.has(taskName)) { + // Initialize task cache + this.#taskCache.set(taskName, + new BuildTaskCache(this.#project.getName(), taskName, supportsDifferentialBuilds)); + } + log.verbose(`Recording results of task ${taskName} in project ${this.#project.getName()}...`); + const taskCache = this.#taskCache.get(taskName); + + // Identify resources written by task + const stage = this.#project.getProjectResources().getStage(); + const stageWriter = stage.getWriter(); + const writtenResources = await stageWriter.byGlob("/**/*"); + const writtenResourcePaths = writtenResources.map((res) => res.getOriginalPath()); + let {projectTagOperations, buildTagOperations} = + this.#project.getProjectResources().getResourceTagOperations(); + + let stageSignature; + if (cacheInfo) { + // Merge tag operations from the previous stage cache with the current delta's tag operations. + // Delta builds only record tags set during the delta execution, so we need to include + // tags from the original full build. Current delta ops take precedence over previous ops. + if (cacheInfo.previousStageCache.projectTagOperations) { + projectTagOperations = new Map([ + ...cacheInfo.previousStageCache.projectTagOperations, + ...projectTagOperations, + ]); + } + if (cacheInfo.previousStageCache.buildTagOperations) { + buildTagOperations = new Map([ + ...cacheInfo.previousStageCache.buildTagOperations, + ...buildTagOperations, + ]); + } + + // Import the previous stage cache's tag operations into the tag collections so that + // subsequent tasks can access them. Delta builds only record tags set during delta + // execution, so the previous build's tags must be imported explicitly. + this.#project.getProjectResources().importTagOperations( + cacheInfo.previousStageCache.projectTagOperations, + cacheInfo.previousStageCache.buildTagOperations); + + stageSignature = cacheInfo.newSignature; + // Add resources from previous stage cache to current stage + let reader; + if (cacheInfo.previousStageCache.stage.byGlob) { + // Reader instance + reader = cacheInfo.previousStageCache.stage; + } else { + // Stage instance + reader = cacheInfo.previousStageCache.stage.getWriter() ?? + cacheInfo.previousStageCache.stage.getCachedWriter(); + } + const mergeStart = performance.now(); + const previousWrittenResources = await reader.byGlob("/**/*"); + let mergedCount = 0; + for (const res of previousWrittenResources) { + if (!writtenResourcePaths.includes(res.getOriginalPath())) { + await stageWriter.write(res); + mergedCount++; + } + } + if (log.isLevelEnabled("perf")) { + log.perf( + `recordTaskResult delta merge for task ${taskName} ` + + `in project ${this.#project.getName()} completed in ` + + `${(performance.now() - mergeStart).toFixed(2)} ms ` + + `(${previousWrittenResources.length} previous, ${mergedCount} merged)`); + } + } else { + // Calculate signature for executed task + const recordReqStart = performance.now(); + const currentSignaturePair = await taskCache.recordRequests( + projectResourceRequests, + dependencyResourceRequests, + this.#currentProjectReader, + this.#currentDependencyReader + ); + if (log.isLevelEnabled("perf")) { + log.perf( + `recordTaskResult recordRequests for task ${taskName} ` + + `in project ${this.#project.getName()} completed in ` + + `${(performance.now() - recordReqStart).toFixed(2)} ms`); + } + // If provided, set dependency signature for later use in result stage signature calculation + const stageName = this.#getStageNameForTask(taskName); + this.#currentStageSignatures.set(stageName, currentSignaturePair); + stageSignature = createStageSignature(...currentSignaturePair); + } + + log.verbose(`Caching stage for task ${taskName} in project ${this.#project.getName()} ` + + `with signature ${stageSignature}`); + + // Store resulting stage in stage cache + this.#stageCache.addSignature( + this.#getStageNameForTask(taskName), stageSignature, this.#project.getProjectResources().getStage(), + writtenResourcePaths, projectTagOperations, buildTagOperations); + + // Update task cache with new metadata + log.verbose(`Task ${taskName} produced ${writtenResourcePaths.length} resources`); + + for (const resourcePath of writtenResourcePaths) { + if (!this.#writtenResultResourcePaths.includes(resourcePath)) { + this.#writtenResultResourcePaths.push(resourcePath); + } + } + // Reset current project reader + this.#currentProjectReader = null; + if (log.isLevelEnabled("perf")) { + log.perf( + `recordTaskResult for task ${taskName} in project ${this.#project.getName()} ` + + `completed in ${(performance.now() - recordStart).toFixed(2)} ms ` + + `(${writtenResourcePaths.length} written resources, delta=${!!cacheInfo})`); + } + } + + /** + * Returns the task cache for a specific task + * + * @public + * @param {string} taskName Name of the task + * @returns {@ui5/project/build/cache/BuildTaskCache|undefined} + * The task cache or undefined if not found + */ + getTaskCache(taskName) { + return this.#taskCache.get(taskName); + } + + /** + * Records changed source files of the project and marks cache as requiring validation. + * This method must not be called during creation of the ProjectBuildCache or while the project is being built to + * avoid inconsistent result and cache corruption. + * + * @public + * @param {string[]} changedPaths Changed project source file paths + */ + projectSourcesChanged(changedPaths) { + for (const resourcePath of changedPaths) { + if (!this.#changedProjectSourcePaths.includes(resourcePath)) { + this.#changedProjectSourcePaths.push(resourcePath); + } + } + if (this.#combinedIndexState !== INDEX_STATES.INITIAL) { + // If there is an index cache, mark it as requiring update + this.#combinedIndexState = INDEX_STATES.REQUIRES_UPDATE; + } + } + + /** + * Records changed dependency resources and marks cache as requiring validation. + * This method must not be called during creation of the ProjectBuildCache or while the project is being built to + * avoid inconsistent result and cache corruption. + * + * @public + * @param {string[]} changedPaths Changed dependency resource paths + */ + dependencyResourcesChanged(changedPaths) { + for (const resourcePath of changedPaths) { + if (!this.#changedDependencyResourcePaths.includes(resourcePath)) { + this.#changedDependencyResourcePaths.push(resourcePath); + } + } + if (this.#combinedIndexState !== INDEX_STATES.INITIAL) { + // If there is an index cache, mark it as requiring update + this.#combinedIndexState = INDEX_STATES.REQUIRES_UPDATE; + } + } + + /** + * Initializes project stages for the given tasks + * + * Creates stage names for each task and initializes them in the project. + * This must be called before task execution begins. + * + * @public + * @param {string[]} taskNames Array of task names to initialize stages for + * @returns {Promise} + */ + async setTasks(taskNames) { + const stageNames = taskNames.map((taskName) => this.#getStageNameForTask(taskName)); + this.#project.getProjectResources().initStages(stageNames); + + // TODO: Rename function? We simply use it to have a point in time right before the project is built + } + + /** + * Re-reads all source files from disk and compares them against the source index + * to detect whether any source files were modified, added, or deleted during the build. + * + * Uses metadata-only comparison via matchResourceMetadataStrict (skipping tags, + * since tags are build artifacts that always differ from fresh disk reads). + * + * @returns {Promise} True if source changes were detected during the build + */ + async #revalidateSourceIndex() { + const sourceReader = this.#project.getSourceReader(); + const globStart = performance.now(); + const currentResources = await sourceReader.byGlob("/**/*"); + if (log.isLevelEnabled("perf")) { + log.perf( + `#revalidateSourceIndex byGlob for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - globStart).toFixed(2)} ms ` + + `(${currentResources.length} resources)`); + } + + const tree = this.#sourceIndex.getTree(); + const indexedPaths = new Set(this.#sourceIndex.getResourcePaths()); + const currentPaths = new Set(); + + for (const resource of currentResources) { + const resourcePath = resource.getPath(); + currentPaths.add(resourcePath); + + const node = tree.getResourceByPath(resourcePath); + if (!node) { + // File was added during the build + log.verbose(`Source file added during build: ${resourcePath}`); + return true; + } + + const cachedMetadata = { + integrity: node.integrity, + lastModified: node.lastModified, + size: node.size, + inode: node.inode, + }; + const isUnchanged = await matchResourceMetadataStrict( + resource, cachedMetadata, tree.getIndexTimestamp() + ); + if (!isUnchanged) { + // File was modified during the build + log.verbose(`Source file modified during build: ${resourcePath}`); + return true; + } + } + + // Check for removed files + for (const indexedPath of indexedPaths) { + if (!currentPaths.has(indexedPath)) { + log.verbose(`Source file removed during build: ${indexedPath}`); + return true; + } + } + + return false; + } + + /** + * Write untransformed source files (not overlayed by any build task) to the CAS + * and persist their metadata in the stage cache. + * + * This enables downstream projects to read dependency source files from the CAS + * snapshot instead of the live filesystem, preventing race conditions from source + * changes between project builds. + * + * In subsequent builds where the source index signature hasn't changed, the stored + * metadata can be used to recreate a CAS-backed reader without rebuilding the dependency. + */ + async #freezeUntransformedSources() { + const transformedPaths = new Set(this.#writtenResultResourcePaths); + const untransformedPaths = this.#sourceIndex.getResourcePaths() + .filter((p) => !transformedPaths.has(p)); + + if (untransformedPaths.length === 0) { + log.verbose( + `All source files of project ${this.#project.getName()} are overlayed by build tasks`); + this.#cachedFrozenSourceMetadata = null; + return; + } + + const sourceSignature = this.#sourceIndex.getSignature(); + const previousMetadata = this.#cachedFrozenSourceMetadata; + + let resourceMetadata; + if (previousMetadata) { + // Delta path: reuse previous metadata for unchanged untransformed paths + const pathsToRead = []; + const reusedMetadata = Object.create(null); + for (const p of untransformedPaths) { + if (previousMetadata[p]) { + reusedMetadata[p] = previousMetadata[p]; + } else { + pathsToRead.push(p); + } + } + + if (pathsToRead.length === 0) { + // Fast path: all metadata reused from previous build + resourceMetadata = reusedMetadata; + if (log.isLevelEnabled("perf")) { + log.perf( + `#freezeUntransformedSources for project ${this.#project.getName()}: ` + + `reused all ${untransformedPaths.length} entries from previous metadata`); + } + } else { + // Delta path: read only new/newly-untransformed paths + const sourceReader = this.#project.getSourceReader(); + const readStart = log.isLevelEnabled("perf") ? performance.now() : 0; + const resources = await Promise.all(pathsToRead.map(async (resourcePath) => { + const resource = await sourceReader.byPath(resourcePath); + if (!resource) { + throw new Error( + `Source file ${resourcePath} not found during CAS freeze ` + + `for project ${this.#project.getName()}`); + } + return resource; + })); + if (log.isLevelEnabled("perf")) { + log.perf( + `#freezeUntransformedSources byPath reads for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - readStart).toFixed(2)} ms ` + + `(${resources.length} of ${untransformedPaths.length} resources)`); + } + + const writeStart = log.isLevelEnabled("perf") ? performance.now() : 0; + const deltaMetadata = await this.#writeStageResources( + resources, "source", sourceSignature, this.#knownCasIntegrities); + if (log.isLevelEnabled("perf")) { + log.perf( + `#freezeUntransformedSources writeStageResources for project ` + + `${this.#project.getName()} ` + + `completed in ${(performance.now() - writeStart).toFixed(2)} ms`); + } + + // Merge: reused entries + delta entries + resourceMetadata = reusedMetadata; + for (const [path, meta] of Object.entries(deltaMetadata)) { + resourceMetadata[path] = meta; + } + } + } else { + // Cold cache: read all untransformed sources (no previous metadata available) + const sourceReader = this.#project.getSourceReader(); + const readStart = log.isLevelEnabled("perf") ? performance.now() : 0; + const resources = await Promise.all(untransformedPaths.map(async (resourcePath) => { + const resource = await sourceReader.byPath(resourcePath); + if (!resource) { + throw new Error( + `Source file ${resourcePath} not found during CAS freeze ` + + `for project ${this.#project.getName()}`); + } + return resource; + })); + if (log.isLevelEnabled("perf")) { + log.perf( + `#freezeUntransformedSources byPath reads for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - readStart).toFixed(2)} ms ` + + `(${resources.length} resources)`); + } + + const writeStart = log.isLevelEnabled("perf") ? performance.now() : 0; + resourceMetadata = await this.#writeStageResources( + resources, "source", sourceSignature, this.#knownCasIntegrities); + if (log.isLevelEnabled("perf")) { + log.perf( + `#freezeUntransformedSources writeStageResources for project ` + + `${this.#project.getName()} ` + + `completed in ${(performance.now() - writeStart).toFixed(2)} ms`); + } + } + + this.#collectKnownIntegrities(resourceMetadata); + + // Persist source stage metadata in the stage cache + await this.#cacheManager.writeStageCache( + this.#project.getId(), this.#buildSignature, "source", sourceSignature, + {resourceMetadata}); + + log.verbose( + `Stored ${untransformedPaths.length} untransformed source files of project ` + + `${this.#project.getName()} in CAS with signature ${sourceSignature}`); + + // Create CAS-backed proxy reader for the untransformed source files + const casSourceReader = this.#createReaderForStageCache("source", sourceSignature, resourceMetadata); + this.#project.getProjectResources().setFrozenSourceReader(casSourceReader); + + // Retain for potential reuse in subsequent BuildServer builds + this.#cachedFrozenSourceMetadata = resourceMetadata; + } + + /** + * Restores the CAS-backed reader for untransformed source files from a previous build's + * cached stage metadata. + * + * @param {string} sourceStageSignature The source index signature used when the source + * stage was persisted + */ + async #restoreFrozenSources(sourceStageSignature) { + const stageMetadata = await this.#cacheManager.readStageCache( + this.#project.getId(), this.#buildSignature, "source", sourceStageSignature); + + if (!stageMetadata) { + log.verbose( + `No cached source stage metadata found for project ${this.#project.getName()} ` + + `with signature ${sourceStageSignature}. Skipping frozen source restore.`); + return; + } + + const {resourceMetadata} = stageMetadata; + this.#collectKnownIntegrities(resourceMetadata); + log.verbose( + `Restored frozen source files for project ${this.#project.getName()} from CAS`); + + const casSourceReader = this.#createReaderForStageCache( + "source", sourceStageSignature, resourceMetadata); + + this.#project.getProjectResources().setFrozenSourceReader(casSourceReader); + } + + /** + * Signals that all tasks have completed and switches to the result stage + * + * This finalizes the build process by switching the project to use the + * final result stage containing all build outputs. + * Also updates the result resource index accordingly. + * + * @public + * @returns {Promise} Array of changed resource paths since the last build + * @throws {Error} If source files were modified during the build + */ + async allTasksCompleted() { + const allTasksStart = performance.now(); + this.#project.getProjectResources().useResultStage(); + + const revalidateStart = performance.now(); + const sourceChangedDuringBuild = await this.#revalidateSourceIndex(); + if (log.isLevelEnabled("perf")) { + log.perf( + `allTasksCompleted #revalidateSourceIndex for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - revalidateStart).toFixed(2)} ms ` + + `(changed=${sourceChangedDuringBuild})`); + } + if (sourceChangedDuringBuild) { + throw new Error( + `Detected changes to source files of project ${this.#project.getName()} during the build. ` + + `The build result may be inconsistent and will not be used. ` + + `Build cache has not been updated.`); + } + + // Write untransformed source files to CAS for downstream consumer protection + const freezeStart = performance.now(); + await this.#freezeUntransformedSources(); + if (log.isLevelEnabled("perf")) { + log.perf( + `allTasksCompleted #freezeUntransformedSources for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - freezeStart).toFixed(2)} ms`); + } + + if (this.#combinedIndexState === INDEX_STATES.INITIAL) { + this.#combinedIndexState = INDEX_STATES.FRESH; + } + this.#resultCacheState = RESULT_CACHE_STATES.FRESH_AND_IN_USE; + const changedPaths = this.#writtenResultResourcePaths; + + this.#currentResultSignature = this.#getResultStageSignature(); + + // Reset updated resource paths + this.#writtenResultResourcePaths = []; + if (log.isLevelEnabled("perf")) { + log.perf( + `allTasksCompleted for project ${this.#project.getName()} ` + + `completed in ${(performance.now() - allTasksStart).toFixed(2)} ms ` + + `(${changedPaths.length} changed paths)`); + } + return changedPaths; + } + + buildFinished() { + this.#project.getProjectResources().buildFinished(); + } + + /** + * Generates the stage name for a given task + * + * @param {string} taskName Name of the task + * @returns {string} Stage name in the format "task/{taskName}" + */ + #getStageNameForTask(taskName) { + return `task/${taskName}`; + } + + /** + * Initializes the resource index from cache or creates a new one + * + * This method attempts to load a cached resource index. If found, it validates + * the index against current source files and invalidates affected tasks if + * resources have changed. If no cache exists, creates a fresh index. + * + * @returns {Promise} + * @throws {Error} If cached index signature doesn't match computed signature + */ + async #initSourceIndex() { + const sourceReader = this.#project.getSourceReader(); + + if (this.#cacheMode === Cache.Off) { + // Caching disabled: Create fresh index + log.verbose(`Cache is in "Off" mode. ` + + `Initializing fresh source index for project ${this.#project.getName()}`); + this.#sourceIndex = await ResourceIndex.create(await sourceReader.byGlob("/**/*"), + Date.now()); + this.#combinedIndexState = INDEX_STATES.INITIAL; + // Clear any existing task cache from previous builds + this.#taskCache.clear(); + this.#stageCache = new StageCache(); + // Reset ProjectResources to initial stage if it exists (clear any cached result stage) + const currentStage = this.#project.getProjectResources().getStage(); + if (currentStage && currentStage.getId() !== "initial") { + this.#project.getProjectResources().useStage("initial"); + } + return; + } + + const [resources, indexCache] = await Promise.all([ + await sourceReader.byGlob("/**/*"), + await this.#cacheManager.readIndexCache(this.#project.getId(), this.#buildSignature, "source"), + ]); + if (indexCache) { + log.verbose(`Using cached resource index for project ${this.#project.getName()}`); + // Create and diff resource index + const {resourceIndex, changedPaths} = + await ResourceIndex.fromCacheWithDelta(indexCache, resources, Date.now()); + + // Pre-populate knownCasIntegrities from the previous build's frozen source stage. + // The source stage metadata records which resources were actually written to CAS + // by the previous build's #freezeUntransformedSources. Using this instead of the + // source index tree ensures we only skip CAS writes for resources that genuinely + // exist in CAS (the tree may contain integrities from newly added or modified files + // that were never written to CAS). + const cachedSourceSignature = indexCache.indexTree.root.hash; + if (cachedSourceSignature) { + const sourceStageMetadata = await this.#cacheManager.readStageCache( + this.#project.getId(), this.#buildSignature, "source", cachedSourceSignature); + if (sourceStageMetadata?.resourceMetadata) { + this.#collectKnownIntegrities(sourceStageMetadata.resourceMetadata); + this.#cachedFrozenSourceMetadata = sourceStageMetadata.resourceMetadata; + } + } + + // Import task caches + const buildTaskCaches = await Promise.all( + indexCache.tasks.map(async ([taskName, supportsDifferentialBuilds]) => { + const projectRequests = await this.#cacheManager.readTaskMetadata( + this.#project.getId(), this.#buildSignature, taskName, "project"); + if (!projectRequests) { + throw new Error(`Failed to load project request cache for task ` + + `${taskName} in project ${this.#project.getName()}`); + } + const dependencyRequests = await this.#cacheManager.readTaskMetadata( + this.#project.getId(), this.#buildSignature, taskName, "dependencies"); + if (!dependencyRequests) { + throw new Error(`Failed to load dependency request cache for task ` + + `${taskName} in project ${this.#project.getName()}`); + } + return BuildTaskCache.fromCache(this.#project.getName(), taskName, !!supportsDifferentialBuilds, + projectRequests, dependencyRequests); + }) + ); + // Ensure taskCache is filled in the order of task execution + for (const buildTaskCache of buildTaskCaches) { + this.#taskCache.set(buildTaskCache.getTaskName(), buildTaskCache); + } + + // Force mode: Fail if cache is stale (source files changed OR pending changes exist) + if (this.#cacheMode === Cache.Force && + (changedPaths.length > 0 || this.#changedProjectSourcePaths.length > 0)) { + const totalChanges = changedPaths.length + this.#changedProjectSourcePaths.length; + throw new Error( + `Cache is in "Force" mode but cache is stale for project ${this.#project.getName()} ` + + `due to ${totalChanges} changed source file(s). ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.` + ); + } + + if (!changedPaths.length) { + // Source index is up-to-date with no changes + this.#cachedSourceSignature = resourceIndex.getSignature(); + } + this.#sourceIndex = resourceIndex; + // Since all source files are part of the result, declare any detected changes as newly written resources + this.#writtenResultResourcePaths = changedPaths; + // Now awaiting initialization of dependency indices + this.#combinedIndexState = INDEX_STATES.RESTORING_DEPENDENCY_INDICES; + } else { + if (this.#cacheMode === Cache.Force) { + throw new Error(`Cache is in "Force" mode but no cache found for project ${this.#project.getName()}. ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.`); + } + // No index cache found, create new index + this.#sourceIndex = await ResourceIndex.create(resources, Date.now()); + this.#combinedIndexState = INDEX_STATES.INITIAL; + } + log.verbose( + `Initialized source index for project ${this.#project.getName()} ` + + `with signature ${this.#sourceIndex.getSignature()}`); + } + + /** + * Updates the source index with changed resource paths + * + * @param {string[]} changedResourcePaths Array of changed resource paths + * @returns {Promise} True if changes were detected, false otherwise + */ + async #updateSourceIndex(changedResourcePaths) { + const sourceReader = this.#project.getSourceReader(); + + const resources = []; + const removedResourcePaths = []; + await Promise.all(changedResourcePaths.map(async (resourcePath) => { + const resource = await sourceReader.byPath(resourcePath); + if (resource) { + resources.push(resource); + } else { + removedResourcePaths.push(resourcePath); + } + })); + const {removed} = await this.#sourceIndex.removeResources(removedResourcePaths); + const {added, updated} = await this.#sourceIndex.upsertResources(resources, Date.now()); + + if (removed.length || added.length || updated.length) { + log.verbose(`Source resource index for project ${this.#project.getName()} updated: ` + + `${removed.length} removed, ${added.length} added, ${updated.length} updated resources. ` + + `New signature: ${this.#sourceIndex.getSignature()}`); + const changedPaths = [...removed, ...added, ...updated]; + // Since all source files are part of the result, declare any detected changes as newly written resources + for (const resourcePath of changedPaths) { + if (!this.#writtenResultResourcePaths.includes(resourcePath)) { + this.#writtenResultResourcePaths.push(resourcePath); + } + } + return true; + } + return false; + } + + // ===== CACHE SERIALIZATION ===== + + /** + * Stores all cache data to persistent storage + * + * This method: + * 1. Stores the signatures of all stages that lead to the current build result + * 2. Writes all pending task stage caches to persistent storage + * 3. Writes task request metadata to persistent storage + * 4. Writes the source resource index to persistent storage + * + * @public + * @returns {Promise} + */ + async writeCache() { + // OFF or ReadOnly modes: Skip all cache writes + if (this.#cacheMode === Cache.Off || this.#cacheMode === Cache.ReadOnly) { + log.verbose( + `Skipping cache write for project ${this.#project.getName()} ` + + `(cache mode: ${this.#cacheMode})` + ); + return; + } + + // Default and Force modes: Write cache normally + const cacheWriteStart = performance.now(); + this.#cacheManager.beginMetadataBatch(); + try { + await Promise.all([ + this.#writeResultCache(), + + this.#writeTaskStageCache(), + this.#writeTaskRequestCache(), + + this.#writeSourceIndex(), + ]); + this.#cacheManager.endMetadataBatch(); + } catch (err) { + this.#cacheManager.rollbackMetadataBatch(); + throw err; + } + if (log.isLevelEnabled("perf")) { + log.perf( + `Wrote build cache for project ${this.#project.getName()} in ` + + `${(performance.now() - cacheWriteStart).toFixed(2)} ms`); + } + } + + /** + * Stores the signatures of all stages that lead to the current build result. This can be used to + * recreate the build result + * + * @returns {Promise} + */ + async #writeResultCache() { + const stageSignature = this.#currentResultSignature; + if (stageSignature === this.#cachedResultSignature) { + // No changes to already cached result stage + return; + } + log.verbose(`Storing result metadata for project ${this.#project.getName()} ` + + `using result stage signature ${stageSignature}`); + const stageSignatures = Object.create(null); + for (const [stageName, stageSigs] of this.#currentStageSignatures.entries()) { + stageSignatures[stageName] = stageSigs.join("-"); + } + + const metadata = { + stageSignatures, + sourceStageSignature: this.#sourceIndex.getSignature(), + }; + await this.#cacheManager.writeResultMetadata( + this.#project.getId(), this.#buildSignature, stageSignature, metadata); + } + + /** + * Writes all pending task stage caches to persistent storage + * + * @returns {Promise} + */ + async #writeTaskStageCache() { + if (!this.#stageCache.hasPendingCacheQueue()) { + return; + } + // Store stage caches + log.verbose(`Storing stage caches for project ${this.#project.getName()} ` + + `with build signature ${this.#buildSignature}`); + const stageQueue = this.#stageCache.flushCacheQueue(); + await Promise.all(stageQueue.map(async ([stageId, stageSignature]) => { + const {stage, projectTagOperations, buildTagOperations} = + this.#stageCache.getCacheForSignature(stageId, stageSignature); + const writer = stage.getWriter(); + + let metadata; + if (writer.getMapping) { + const writerMapping = writer.getMapping(); + // Ensure unique readers are used + const readers = Array.from(new Set(Object.values(writerMapping))); + // Map mapping entries to reader indices + const resourceMapping = Object.create(null); + for (const [virPath, reader] of Object.entries(writerMapping)) { + const readerIdx = readers.indexOf(reader); + resourceMapping[virPath] = readerIdx; + } + + const resourceMetadata = await Promise.all(readers.map(async (reader, idx) => { + const resources = await reader.byGlob("/**/*"); + + return await this.#writeStageResources( + resources, stageId, stageSignature, this.#knownCasIntegrities); + })); + this.#collectKnownIntegrities(resourceMetadata); + + metadata = {resourceMapping, resourceMetadata}; + } else { + const resources = await writer.byGlob("/**/*"); + const resourceMetadata = await this.#writeStageResources( + resources, stageId, stageSignature, this.#knownCasIntegrities); + this.#collectKnownIntegrities(resourceMetadata); + metadata = {resourceMetadata}; + } + metadata.projectTagOperations = tagOpsToObject(projectTagOperations); + metadata.buildTagOperations = tagOpsToObject(buildTagOperations); + await this.#cacheManager.writeStageCache( + this.#project.getId(), this.#buildSignature, stageId, stageSignature, metadata); + })); + } + + /** + * Extracts integrity hashes from resource metadata and adds them to the known CAS set. + * Handles both the array form (WriterCollection stages) and the plain object form. + * + * @param {Object|Array>} resourceMetadata + */ + #collectKnownIntegrities(resourceMetadata) { + const metadataObjects = Array.isArray(resourceMetadata) + ? resourceMetadata : [resourceMetadata]; + for (const metadataObj of metadataObjects) { + for (const meta of Object.values(metadataObj)) { + if (meta.integrity) { + this.#knownCasIntegrities.add(meta.integrity); + } + } + } + } + + /** + * Writes stage resources to persistent storage and returns their metadata + * + * @param {@ui5/fs/Resource[]} resources Array of resources to write + * @param {string} stageId Stage identifier + * @param {string} stageSignature Stage signature + * @param {Set} [knownCasIntegrities] Set of integrity hashes known to exist in CAS + * @returns {Promise>} Resource metadata indexed by path + */ + async #writeStageResources(resources, stageId, stageSignature, knownCasIntegrities) { + const resourceMetadata = Object.create(null); + let casSkipped = 0; + + // Phase 1: Gather resource data (async I/O for integrity and buffer) + const toWrite = []; + await Promise.all(resources.map(async (res) => { + const integrity = await res.getIntegrity(); + + if (knownCasIntegrities?.has(integrity)) { + casSkipped++; + } else { + const buffer = await res.getBuffer(); + toWrite.push({integrity, buffer}); + } + + resourceMetadata[res.getOriginalPath()] = { + inode: res.getInode(), + lastModified: res.getLastModified(), + size: await res.getSize(), + integrity, + }; + })); + + // Phase 2: Batch write to SQLite in a single transaction + if (toWrite.length > 0) { + this.#cacheManager.beginContentBatch(); + try { + for (const {integrity, buffer} of toWrite) { + this.#cacheManager.putContent(integrity, buffer); + } + this.#cacheManager.endContentBatch(); + } catch (err) { + this.#cacheManager.rollbackContentBatch(); + throw err; + } + } + + if (log.isLevelEnabled("perf") && casSkipped > 0) { + log.perf( + `#writeStageResources for stage ${stageId}: ` + + `${casSkipped} CAS skipped, ${resources.length - casSkipped} CAS written`); + } + return resourceMetadata; + } + + /** + * Writes task request metadata to persistent storage + * + * @returns {Promise} + */ + async #writeTaskRequestCache() { + // Store task caches + for (const [taskName, taskCache] of this.#taskCache) { + if (taskCache.hasNewOrModifiedCacheEntries()) { + const [projectRequests, dependencyRequests] = taskCache.toCacheObjects(); + log.verbose(`Storing task cache metadata for task ${taskName} in project ${this.#project.getName()}`); + const writes = []; + if (projectRequests) { + writes.push(this.#cacheManager.writeTaskMetadata( + this.#project.getId(), this.#buildSignature, taskName, "project", projectRequests)); + } + if (dependencyRequests) { + writes.push(this.#cacheManager.writeTaskMetadata( + this.#project.getId(), this.#buildSignature, taskName, "dependencies", dependencyRequests)); + } + await Promise.all(writes); + } + } + } + + /** + * Writes the source index cache to persistent storage + * + * @returns {Promise} + */ + async #writeSourceIndex() { + if (this.#cachedSourceSignature === this.#sourceIndex.getSignature()) { + // No changes to already cached result index + return; + } + log.verbose(`Storing resource index cache for project ${this.#project.getName()} ` + + `with build signature ${this.#buildSignature}`); + const sourceIndexObject = this.#sourceIndex.toCacheObject(); + const tasks = []; + for (const [taskName, taskCache] of this.#taskCache) { + tasks.push([taskName, taskCache.getSupportsDifferentialBuilds() ? 1 : 0]); + } + await this.#cacheManager.writeIndexCache(this.#project.getId(), this.#buildSignature, "source", { + ...sourceIndexObject, + tasks, + }); + } + + /** + * Creates a proxy reader for accessing cached stage resources + * + * The reader provides virtual access to cached resources by loading them from + * the cache storage on demand. Resource metadata is used to validate cache entries. + * + * @param {string} stageId Identifier for the stage (e.g., "result" or "task/{taskName}") + * @param {string} stageSignature Signature hash of the stage + * @param {Object} resourceMetadata Metadata for all cached resources + * @returns {@ui5/fs/AbstractReader} Proxy reader for cached resources + */ + #createReaderForStageCache(stageId, stageSignature, resourceMetadata) { + const allResourcePaths = Object.keys(resourceMetadata); + return createProxy({ + name: `Cache reader for task ${stageId} in project ${this.#project.getName()}`, + listResourcePaths: () => { + return allResourcePaths; + }, + getResource: async (virPath) => { + if (!(virPath in resourceMetadata)) { + return null; + } + const {lastModified, size, integrity, inode} = resourceMetadata[virPath]; + if (size === undefined || lastModified === undefined || + integrity === undefined) { + throw new Error(`Incomplete metadata for resource ${virPath} of task ${stageId} ` + + `in project ${this.#project.getName()}`); + } + + return createResource({ + path: virPath, + sourceMetadata: { + adapter: "CAS_SQLITE", + contentModified: false, + }, + createStream: () => { + const compressed = this.#cacheManager.readContentRaw(integrity); + return Readable.from(gunzipSync(compressed)); + }, + createBuffer: () => { + return this.#cacheManager.readContent(integrity); + }, + byteSize: size, + lastModified, + integrity, + inode, + project: this.#project, + }); + } + }); + } +} + +/** + * Computes the cartesian product of an array of arrays + * + * @param {Array} arrays Array of arrays to compute the product of + * @returns {Array} Array of all possible combinations + */ +function cartesianProduct(arrays) { + if (arrays.length === 0) return [[]]; + if (arrays.some((arr) => arr.length === 0)) return []; + + let result = [[]]; + + for (const array of arrays) { + const temp = []; + for (const resultItem of result) { + for (const item of array) { + temp.push([...resultItem, item]); + } + } + result = temp; + } + + return result; +} + +/** + * Fast combination of two arrays into pairs + * + * Creates all possible pairs by combining each element from the first array + * with each element from the second array. + * + * @param {Array} array1 First array + * @param {Array} array2 Second array + * @returns {Array} Array of two-element pairs + */ +function combineTwoArraysFast(array1, array2) { + const len1 = array1.length; + const len2 = array2.length; + const result = new Array(len1 * len2); + + let idx = 0; + for (let i = 0; i < len1; i++) { + for (let j = 0; j < len2; j++) { + result[idx++] = [array1[i], array2[j]]; + } + } + + return result; +} + +/** + * Creates a combined stage signature from project and dependency signatures + * + * @param {string} projectSignature Project resource signature + * @param {string} dependencySignature Dependency resource signature + * @returns {string} Combined stage signature in format "projectSignature-dependencySignature" + */ +function createStageSignature(projectSignature, dependencySignature) { + return `${projectSignature}-${dependencySignature}`; +} + +/** + * Creates a combined signature hash from multiple stage dependency signatures + * + * @param {string[]} stageDependencySignatures Array of dependency signatures to combine + * @returns {string} SHA-256 hash of the combined signatures + */ +function createDependencySignature(stageDependencySignatures) { + return crypto.createHash("sha256").update(stageDependencySignatures.join("")).digest("hex"); +} + +function tagOpsToMap(tagOps) { + const map = new Map(); + for (const [resourcePath, tags] of Object.entries(tagOps)) { + map.set(resourcePath, new Map(Object.entries(tags))); + } + return map; +} + +/** + * @param {Map>} tagOps + * Map of resource paths to their tag operations + */ +function tagOpsToObject(tagOps) { + const obj = Object.create(null); + for (const [resourcePath, tags] of tagOps.entries()) { + obj[resourcePath] = Object.fromEntries(tags.entries()); + } + return obj; +} diff --git a/packages/project/lib/build/cache/ResourceRequestGraph.js b/packages/project/lib/build/cache/ResourceRequestGraph.js new file mode 100644 index 00000000000..fb152ec7e55 --- /dev/null +++ b/packages/project/lib/build/cache/ResourceRequestGraph.js @@ -0,0 +1,684 @@ +const ALLOWED_REQUEST_TYPES = new Set(["path", "patterns"]); + +/** + * Represents a single resource request with type and value + * + * A request can be either a path-based request (single resource) or a pattern-based + * request (multiple resources via glob patterns). + * + * @class + */ +export class Request { + /** + * Creates a new Request instance + * + * @public + * @param {string} type Either 'path' or 'patterns' + * @param {string|string[]} value The request value (string for path, array for patterns) + * @throws {Error} If type is invalid or value type doesn't match request type + */ + constructor(type, value) { + if (!ALLOWED_REQUEST_TYPES.has(type)) { + throw new Error(`Invalid request type: ${type}`); + } + + // Validate value type based on request type + if (type === "path" && typeof value !== "string") { + throw new Error(`Request type '${type}' requires value to be a string`); + } + + this.type = type; + this.value = value; + } + + /** + * Creates a canonical string representation for comparison + * + * Converts the request to a unique key string that can be used for equality + * checks and set operations. + * + * @public + * @returns {string} Canonical key in format "type:value" or "type:[pattern1,pattern2,...]" + */ + toKey() { + if (Array.isArray(this.value)) { + return `${this.type}:${JSON.stringify(this.value)}`; + } + return `${this.type}:${this.value}`; + } + + /** + * Creates a Request instance from a key string + * + * Inverse operation of toKey(), reconstructing a Request from its string representation. + * + * @public + * @param {string} key Key in format "type:value" or "type:[...]" + * @returns {Request} Reconstructed Request instance + */ + static fromKey(key) { + const colonIndex = key.indexOf(":"); + const type = key.substring(0, colonIndex); + const valueStr = key.substring(colonIndex + 1); + + // Check if value is a JSON array + if (valueStr.startsWith("[")) { + const value = JSON.parse(valueStr); + return new Request(type, value); + } + + return new Request(type, valueStr); + } + + /** + * Checks equality with another Request + * + * Compares both type and value, handling array values correctly. + * + * @public + * @param {Request} other Request to compare with + * @returns {boolean} True if requests are equal + */ + equals(other) { + if (this.type !== other.type) { + return false; + } + + if (Array.isArray(this.value) && Array.isArray(other.value)) { + if (this.value.length !== other.value.length) { + return false; + } + return this.value.every((val, idx) => val === other.value[idx]); + } + + return this.value === other.value; + } +} + +/** + * Represents a node in the request set graph + * + * Each node stores a delta of requests added at this level, with an optional parent + * reference. The full request set is computed by traversing up the parent chain. + * This enables efficient storage through delta encoding. + * + * @class + */ +class RequestSetNode { + /** + * Creates a new RequestSetNode instance + * + * @param {number} id Unique node identifier + * @param {number|null} [parent=null] Parent node ID or null for root nodes + * @param {Request[]} [addedRequests=[]] Requests added in this node (delta from parent) + * @param {*} [metadata={}] Associated metadata + */ + constructor(id, parent = null, addedRequests = [], metadata = {}) { + this.id = id; + this.parent = parent; // NodeId or null + this.addedRequests = new Set(addedRequests.map((r) => r.toKey())); + this.metadata = metadata; + + // Cached materialized set (lazy computed) + this._fullSetCache = null; + this._cacheValid = false; + } + + /** + * Gets the full materialized set of requests for this node + * + * Computes the complete set of requests by traversing up the parent chain + * and collecting all added requests. Results are cached for performance. + * + * @param {ResourceRequestGraph} graph The graph containing this node + * @returns {Set} Set of request keys + */ + getMaterializedSet(graph) { + if (this._cacheValid && this._fullSetCache !== null) { + return new Set(this._fullSetCache); + } + + const result = new Set(); + let current = this; + + // Walk up parent chain, collecting all added requests + while (current !== null) { + for (const requestKey of current.addedRequests) { + result.add(requestKey); + } + current = current.parent ? graph.getNode(current.parent) : null; + } + + // Cache the result + this._fullSetCache = result; + this._cacheValid = true; + + return new Set(result); + } + + /** + * Invalidates the materialized set cache + * + * Should be called when the graph structure changes to ensure the cached + * materialized set is recomputed on next access. + */ + invalidateCache() { + this._cacheValid = false; + this._fullSetCache = null; + } + + /** + * Gets the full set of requests as Request objects + * + * Similar to getMaterializedSet but returns Request instances instead of keys. + * + * @param {ResourceRequestGraph} graph The graph containing this node + * @returns {Request[]} Array of Request objects + */ + getMaterializedRequests(graph) { + const keys = this.getMaterializedSet(graph); + return Array.from(keys).map((key) => Request.fromKey(key)); + } + + /** + * Gets only the requests added in this node (delta) + * + * Returns the requests added at this level, not including parent requests. + * This is the delta that was stored in the node. + * + * @returns {Request[]} Array of Request objects added in this node + */ + getAddedRequests() { + return Array.from(this.addedRequests).map((key) => Request.fromKey(key)); + } + + /** + * Gets the parent node ID + * + * @returns {number|null} Parent node ID or null if this is a root node + */ + getParentId() { + return this.parent; + } +} + +/** + * Graph managing request set nodes with delta encoding + * + * This graph structure optimizes storage of multiple related request sets by using + * delta encoding - each node stores only the requests added relative to its parent. + * This is particularly efficient when request sets have significant overlap. + * + * The graph automatically finds the best parent for new request sets to minimize + * the delta size and maintain efficient storage. + * + * @class + */ +export default class ResourceRequestGraph { + /** + * Creates a new ResourceRequestGraph instance + * + * @public + */ + constructor() { + this.nodes = new Map(); // nodeId -> RequestSetNode + this.nextId = 1; + } + + /** + * Gets a node by ID + * + * @public + * @param {number} nodeId Node identifier + * @returns {RequestSetNode|undefined} The node or undefined if not found + */ + getNode(nodeId) { + return this.nodes.get(nodeId); + } + + /** + * Gets all node IDs in the graph + * + * @public + * @returns {number[]} Array of all node IDs + */ + getAllNodeIds() { + return Array.from(this.nodes.keys()); + } + + /** + * Calculates which requests need to be added (delta) + * + * Determines the difference between a new request set and a parent's materialized set, + * returning only the requests that need to be stored in the delta. + * + * @param {Request[]} newRequestSet New request set + * @param {Set} parentSet Parent's materialized set (as keys) + * @returns {Request[]} Array of requests to add + */ + _calculateAddedRequests(newRequestSet, parentSet) { + const newKeys = new Set(newRequestSet.map((r) => r.toKey())); + const addedKeys = newKeys.difference(parentSet); + + return Array.from(addedKeys).map((key) => Request.fromKey(key)); + } + + /** + * Adds a new request set to the graph + * + * Automatically finds the best parent node (largest subset) and stores only + * the delta of requests. If no suitable parent is found, creates a root node. + * + * @public + * @param {Request[]} requests Array of Request objects + * @param {*} [metadata=null] Optional metadata to store with this node + * @returns {number} The new node ID + */ + addRequestSet(requests, metadata = null) { + const nodeId = this.nextId++; + + // Find best parent + const parentId = this.findBestParent(requests); + + if (parentId === null) { + // No existing nodes, or no suitable parent - create root node + const node = new RequestSetNode(nodeId, null, requests, metadata); + this.nodes.set(nodeId, node); + return nodeId; + } + + // Create node with delta from best parent + const parentNode = this.getNode(parentId); + const parentSet = parentNode.getMaterializedSet(this); + const addedRequests = this._calculateAddedRequests(requests, parentSet); + + const node = new RequestSetNode(nodeId, parentId, addedRequests, metadata); + this.nodes.set(nodeId, node); + + return nodeId; + } + + /** + * Finds the best parent for a new request set + * + * Searches for the existing node with the largest subset of the new request set. + * This minimizes the delta size and optimizes storage efficiency. + * + * @public + * @param {Request[]} requestSet Array of Request objects + * @returns {number|null} Parent node ID or null if no suitable parent exists + */ + findBestParent(requestSet) { + if (this.nodes.size === 0) { + return null; + } + + const queryKeys = new Set(requestSet.map((r) => r.toKey())); + let bestParent = null; + let greatestSubset = -1; + + // Compare against all existing nodes + for (const [nodeId, node] of this.nodes) { + const nodeSet = node.getMaterializedSet(this); + + // Check if nodeSet is a subset of queryKeys + const isSubset = nodeSet.isSubsetOf(queryKeys); + + // We want the parent the greatest overlap + if (isSubset && nodeSet.size > greatestSubset) { + bestParent = nodeId; + greatestSubset = nodeSet.size; + } + } + + return bestParent; + } + + /** + * Finds a node with an identical request set + * + * Searches for an existing node whose materialized request set exactly matches + * the given request set. Used to avoid creating duplicate nodes. + * + * @public + * @param {Request[]} requests Array of Request objects + * @returns {number|null} Node ID of exact match, or null if no match found + */ + findExactMatch(requests) { + // Convert to request keys for comparison + const queryKeys = new Set(requests.map((req) => new Request(req.type, req.value).toKey())); + + // Must have same size to be identical + const querySize = queryKeys.size; + + for (const [nodeId, node] of this.nodes) { + const nodeSet = node.getMaterializedSet(this); + + // Quick size check first + if (nodeSet.size !== querySize) { + continue; + } + + // Check if sets are identical (same size + subset = equality) + if (nodeSet.isSubsetOf(queryKeys)) { + return nodeId; + } + } + + return null; + } + + /** + * Gets metadata associated with a node + * + * @public + * @param {number} nodeId Node identifier + * @returns {*} Metadata or null if node not found + */ + getMetadata(nodeId) { + const node = this.getNode(nodeId); + return node ? node.metadata : null; + } + + /** + * Updates metadata for a node + * + * @public + * @param {number} nodeId Node identifier + * @param {*} metadata New metadata value + */ + setMetadata(nodeId, metadata) { + const node = this.getNode(nodeId); + if (node) { + node.metadata = metadata; + } + } + + /** + * Gets all unique requests across all nodes in the graph + * + * Collects the union of all materialized request sets from every node. + * + * @public + * @returns {Request[]} Array of all unique Request objects in the graph + */ + getAllRequests() { + const allRequestKeys = new Set(); + + for (const node of this.nodes.values()) { + const nodeSet = node.getMaterializedSet(this); + for (const key of nodeSet) { + allRequestKeys.add(key); + } + } + + return Array.from(allRequestKeys).map((key) => Request.fromKey(key)); + } + + /** + * Gets statistics about the graph structure + * + * Provides metrics about the graph's efficiency, including node count, + * average requests per node, storage overhead, and tree depth statistics. + * + * @public + * @returns {object} Statistics object + * @returns {number} return.nodeCount Total number of nodes + * @returns {number} return.averageRequestsPerNode Average materialized requests per node + * @returns {number} return.averageStoredDeltaSize Average stored delta size per node + * @returns {number} return.averageDepth Average depth in the tree + * @returns {number} return.maxDepth Maximum depth in the tree + * @returns {number} return.compressionRatio Ratio of stored deltas to total requests (lower is better) + */ + getStats() { + let totalRequests = 0; + let totalStoredDeltas = 0; + const depths = []; + + for (const node of this.nodes.values()) { + totalRequests += node.getMaterializedSet(this).size; + totalStoredDeltas += node.addedRequests.size; + + // Calculate depth + let depth = 0; + let current = node; + while (current.parent !== null) { + depth++; + current = this.getNode(current.parent); + } + depths.push(depth); + } + + return { + nodeCount: this.nodes.size, + averageRequestsPerNode: this.nodes.size > 0 ? totalRequests / this.nodes.size : 0, + averageStoredDeltaSize: this.nodes.size > 0 ? totalStoredDeltas / this.nodes.size : 0, + averageDepth: depths.length > 0 ? depths.reduce((a, b) => a + b, 0) / depths.length : 0, + maxDepth: depths.length > 0 ? Math.max(...depths) : 0, + compressionRatio: totalRequests > 0 ? totalStoredDeltas / totalRequests : 1 + }; + } + + /** + * Gets the number of nodes in the graph + * + * @public + * @returns {number} Node count + */ + getSize() { + return this.nodes.size; + } + + /** + * Iterates through nodes in breadth-first order (by depth level) + * + * Parents are always yielded before their children, allowing efficient traversal + * where you can check parent nodes first and only examine deltas of subtrees as needed. + * + * @public + * @generator + * @yields {object} Node information + * @yields {number} return.nodeId Node identifier + * @yields {RequestSetNode} return.node Node instance + * @yields {number} return.depth Depth level in the tree + * @yields {number|null} return.parentId Parent node ID or null for root nodes + * + * @example + * // Traverse all nodes, checking parents before children + * for (const {nodeId, node, depth, parentId} of graph.traverseByDepth()) { + * const delta = node.getAddedRequests(); + * const fullSet = node.getMaterializedRequests(graph); + * console.log(`Node ${nodeId} at depth ${depth}: +${delta.length} requests`); + * } + * + * @example + * // Early termination: find first matching node without processing children + * for (const {nodeId, node} of graph.traverseByDepth()) { + * if (nodeMatchesQuery(node)) { + * console.log(`Found match at node ${nodeId}`); + * break; // Stop traversal + * } + * } + */ + * traverseByDepth() { + if (this.nodes.size === 0) { + return; + } + + // Build children map for efficient traversal + const childrenMap = new Map(); // parentId -> [childIds] + const rootNodes = []; + + for (const [nodeId, node] of this.nodes) { + if (node.parent === null) { + rootNodes.push(nodeId); + } else { + if (!childrenMap.has(node.parent)) { + childrenMap.set(node.parent, []); + } + childrenMap.get(node.parent).push(nodeId); + } + } + + // Breadth-first traversal using a queue + const queue = rootNodes.map((nodeId) => ({nodeId, depth: 0})); + + while (queue.length > 0) { + const {nodeId, depth} = queue.shift(); + const node = this.getNode(nodeId); + + // Yield current node + yield { + nodeId, + node, + depth, + parentId: node.parent + }; + + // Enqueue children for next depth level + const children = childrenMap.get(nodeId); + if (children) { + for (const childId of children) { + queue.push({nodeId: childId, depth: depth + 1}); + } + } + } + } + + /** + * Iterates through nodes starting from a specific node, traversing its subtree + * + * Useful for examining only a portion of the graph rooted at a particular node. + * + * @public + * @generator + * @param {number} startNodeId Node ID to start traversal from + * @yields {object} Node information + * @yields {number} return.nodeId Node identifier + * @yields {RequestSetNode} return.node Node instance + * @yields {number} return.depth Relative depth from the start node + * @yields {number|null} return.parentId Parent node ID or null + * + * @example + * // Traverse only the subtree under a specific node + * const matchNodeId = graph.findBestParent(query); + * for (const {nodeId, node, depth} of graph.traverseSubtree(matchNodeId)) { + * console.log(`Processing node ${nodeId} at relative depth ${depth}`); + * } + */ + * traverseSubtree(startNodeId) { + const startNode = this.getNode(startNodeId); + if (!startNode) { + return; + } + + // Build children map + const childrenMap = new Map(); + for (const [nodeId, node] of this.nodes) { + if (node.parent !== null) { + if (!childrenMap.has(node.parent)) { + childrenMap.set(node.parent, []); + } + childrenMap.get(node.parent).push(nodeId); + } + } + + // Breadth-first traversal starting from the specified node + const queue = [{nodeId: startNodeId, depth: 0}]; + + while (queue.length > 0) { + const {nodeId, depth} = queue.shift(); + const node = this.getNode(nodeId); + + yield { + nodeId, + node, + depth, + parentId: node.parent + }; + + // Enqueue children + const children = childrenMap.get(nodeId); + if (children) { + for (const childId of children) { + queue.push({nodeId: childId, depth: depth + 1}); + } + } + } + } + + /** + * Gets all children node IDs for a given parent node + * + * @public + * @param {number} parentId Parent node identifier + * @returns {number[]} Array of child node IDs + */ + getChildren(parentId) { + const children = []; + for (const [nodeId, node] of this.nodes) { + if (node.parent === parentId) { + children.push(nodeId); + } + } + return children; + } + + /** + * Exports graph structure for serialization + * + * Converts the graph to a plain object suitable for JSON serialization. + * Metadata is not included in the export and must be handled separately. + * + * @public + * @returns {object} Graph structure + * @returns {Array} return.nodes Array of node objects with id, parent, and addedRequests + * @returns {number} return.nextId Next available node ID + */ + toCacheObject() { + const nodes = []; + + for (const [nodeId, node] of this.nodes) { + nodes.push({ + id: nodeId, + parent: node.parent, + addedRequests: Array.from(node.addedRequests) + }); + } + + return {nodes, nextId: this.nextId}; + } + + /** + * Creates a graph from a serialized cache object + * + * Reconstructs the graph structure from a plain object produced by toCacheObject(). + * Metadata must be restored separately if needed. + * + * @public + * @param {object} metadata Serialized graph structure + * @param {Array} metadata.nodes Array of node objects with id, parent, and addedRequests + * @param {number} metadata.nextId Next available node ID + * @returns {ResourceRequestGraph} Reconstructed graph instance + */ + static fromCache(metadata) { + const graph = new ResourceRequestGraph(); + + // Restore nextId + graph.nextId = metadata.nextId; + + // Recreate all nodes + for (const nodeData of metadata.nodes) { + const {id, parent, addedRequests} = nodeData; + + // Convert request keys back to Request instances + const requestInstances = addedRequests.map((key) => Request.fromKey(key)); + + // Create node directly + const node = new RequestSetNode(id, parent, requestInstances); + graph.nodes.set(id, node); + } + + return graph; + } +} diff --git a/packages/project/lib/build/cache/ResourceRequestManager.js b/packages/project/lib/build/cache/ResourceRequestManager.js new file mode 100644 index 00000000000..f7a211f1e4b --- /dev/null +++ b/packages/project/lib/build/cache/ResourceRequestManager.js @@ -0,0 +1,726 @@ +import micromatch from "micromatch"; +import ResourceRequestGraph, {Request} from "./ResourceRequestGraph.js"; +import ResourceIndex from "./index/ResourceIndex.js"; +import TreeRegistry from "./index/TreeRegistry.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:cache:ResourceRequestManager"); + +/** + * Manages resource requests and their associated indices for a single task + * + * Tracks all resources accessed by a task during execution and maintains resource indices + * for cache validation and differential updates. Supports both full and delta-based caching + * strategies. + * + * @class + */ +class ResourceRequestManager { + #taskName; + #projectName; + #requestGraph; + + #treeRegistries = []; + #treeUpdateDeltas = new Map(); + + #hasNewOrModifiedCacheEntries; + #useDifferentialUpdate; + #unusedAtLeastOnce; + + /** + * Creates a new ResourceRequestManager instance + * + * @param {string} projectName Name of the project + * @param {string} taskName Name of the task + * @param {boolean} useDifferentialUpdate Whether to track differential updates + * @param {ResourceRequestGraph} [requestGraph] Optional pre-existing request graph from cache + * @param {boolean} [unusedAtLeastOnce=false] Whether the task has been unused at least once + */ + constructor(projectName, taskName, useDifferentialUpdate, requestGraph, unusedAtLeastOnce = false) { + this.#projectName = projectName; + this.#taskName = taskName; + this.#useDifferentialUpdate = useDifferentialUpdate; + this.#unusedAtLeastOnce = unusedAtLeastOnce; + if (requestGraph) { + this.#requestGraph = requestGraph; + this.#hasNewOrModifiedCacheEntries = false; // Using cache + } else { + this.#requestGraph = new ResourceRequestGraph(); + this.#hasNewOrModifiedCacheEntries = true; + } + } + + /** + * Factory method to restore a ResourceRequestManager from cached data + * + * Deserializes a previously cached request graph and its associated resource indices, + * including both root indices and delta indices for differential updates. + * + * @param {string} projectName Name of the project + * @param {string} taskName Name of the task + * @param {boolean} useDifferentialUpdate Whether to track differential updates + * @param {object} cacheData Cached metadata object + * @param {object} cacheData.requestSetGraph Serialized request graph + * @param {Array} cacheData.rootIndices Array of root resource indices + * @param {Array} [cacheData.deltaIndices] Array of delta resource indices + * @param {boolean} [cacheData.unusedAtLeastOnce] Whether the task has been unused + * @returns {ResourceRequestManager} Restored manager instance + */ + static fromCache(projectName, taskName, useDifferentialUpdate, { + requestSetGraph, rootIndices, deltaIndices, unusedAtLeastOnce + }) { + const requestGraph = ResourceRequestGraph.fromCache(requestSetGraph); + const resourceRequestManager = new ResourceRequestManager( + projectName, taskName, useDifferentialUpdate, requestGraph, unusedAtLeastOnce); + const registries = new Map(); + // Restore root resource indices + for (const {nodeId, resourceIndex: serializedIndex} of rootIndices) { + const metadata = requestGraph.getMetadata(nodeId); + const registry = resourceRequestManager.#newTreeRegistry(); + registries.set(nodeId, registry); + metadata.resourceIndex = ResourceIndex.fromCacheShared(serializedIndex, registry); + } + // Restore delta resource indices + if (deltaIndices) { + for (const {nodeId, addedResourceIndex} of deltaIndices) { + const node = requestGraph.getNode(nodeId); + const {resourceIndex: parentResourceIndex} = requestGraph.getMetadata(node.getParentId()); + const registry = registries.get(node.getParentId()); + if (!registry) { + throw new Error(`Missing tree registry for parent of node ID ${nodeId} of task ` + + `'${taskName}' of project '${projectName}'`); + } + const resourceIndex = parentResourceIndex.deriveTreeWithIndex(addedResourceIndex); + + requestGraph.setMetadata(nodeId, { + resourceIndex, + }); + } + } + return resourceRequestManager; + } + + /** + * Gets all project index signatures for this task + * + * Returns signatures from all recorded project-request sets. Each signature represents + * a unique combination of resources belonging to the current project that were accessed + * during task execution. This can be used to form cache keys for restoring cached task results. + * + * @public + * @returns {string[]} Array of signature strings + * @throws {Error} If resource index is missing for any request set + */ + getIndexSignatures() { + const requestSetIds = this.#requestGraph.getAllNodeIds(); + const signatures = requestSetIds.map((requestSetId) => { + const {resourceIndex} = this.#requestGraph.getMetadata(requestSetId); + if (!resourceIndex) { + throw new Error(`Resource index missing for request set ID ${requestSetId}`); + } + return resourceIndex.getSignature(); + }); + if (this.#unusedAtLeastOnce) { + signatures.push("X"); // Signature for when no requests were made + } + return signatures; + } + + /** + * Updates all indices based on current resources without delta tracking + * + * Performs a full refresh of all resource indices by fetching current resources + * and updating or removing indexed resources as needed. Does not track changes + * between the old and new state. + * + * @public + * @param {module:@ui5/fs.AbstractReader} reader Reader for accessing project resources + * @returns {Promise} + */ + async refreshIndices(reader) { + if (this.#requestGraph.getSize() === 0) { + // No requests recorded -> No updates necessary + return; + } + + const refreshStart = log.isLevelEnabled("perf") ? performance.now() : 0; + let totalResourcesFetched = 0; + let totalResourcesRemoved = 0; + const resourceCache = new Map(); + const nodeEntries = Array.from(this.#requestGraph.traverseByDepth()); + + await Promise.all(nodeEntries.map(async ({nodeId}) => { + const {resourceIndex} = this.#requestGraph.getMetadata(nodeId); + if (!resourceIndex) { + throw new Error(`Missing resource index for request set ID ${nodeId}`); + } + const addedRequests = this.#requestGraph.getNode(nodeId).getAddedRequests(); + const resourcesToUpdate = await this.#getResourcesForRequests(addedRequests, reader, resourceCache); + + // Determine resources to remove + const indexedResourcePaths = resourceIndex.getResourcePaths(); + const currentResourcePaths = resourcesToUpdate.map((res) => res.getOriginalPath()); + const resourcesToRemove = indexedResourcePaths.filter((resPath) => { + return !currentResourcePaths.includes(resPath); + }); + totalResourcesRemoved += resourcesToRemove.length; + if (resourcesToRemove.length) { + await resourceIndex.removeResources(resourcesToRemove); + } + totalResourcesFetched += resourcesToUpdate.length; + if (resourcesToUpdate.length) { + await resourceIndex.upsertResources(resourcesToUpdate); + } + })); + if (log.isLevelEnabled("perf")) { + log.perf( + `refreshIndices for task '${this.#taskName}' of project '${this.#projectName}' ` + + `completed in ${(performance.now() - refreshStart).toFixed(2)} ms: ` + + `${totalResourcesFetched} resources fetched, ${totalResourcesRemoved} resources removed`); + } + + await this.#flushTreeChangesWithoutDiffTracking(); + } + + /** + * Filters relevant resource changes and updates the indices if necessary + * + * Processes changed resource paths, identifies which request sets are affected, + * and updates their resource indices accordingly. Supports both full updates and + * differential tracking based on the manager configuration. + * + * @public + * @param {module:@ui5/fs.AbstractReader} reader Reader for accessing project resources + * @param {string[]} changedResourcePaths Array of changed project resource paths + * @returns {Promise} True if any changes were detected, false otherwise + */ + async updateIndices(reader, changedResourcePaths) { + const matchingRequestSetIds = []; + const updatesByRequestSetId = new Map(); + if (this.#requestGraph.getSize() === 0) { + // No requests recorded -> No updates necessary + return false; + } + + // Process all nodes, parents before children + for (const {nodeId, node, parentId} of this.#requestGraph.traverseByDepth()) { + const addedRequests = node.getAddedRequests(); // Resource requests added at this level + let relevantUpdates; + if (addedRequests.length) { + relevantUpdates = this.#matchResourcePaths(addedRequests, changedResourcePaths); + } else { + relevantUpdates = []; + } + if (parentId) { + // Include updates from parent nodes + const parentUpdates = updatesByRequestSetId.get(parentId); + if (parentUpdates && parentUpdates.length) { + relevantUpdates.push(...parentUpdates); + } + } + if (relevantUpdates.length) { + updatesByRequestSetId.set(nodeId, relevantUpdates); + matchingRequestSetIds.push(nodeId); + } + } + if (!matchingRequestSetIds.length) { + return false; // No relevant changes for any request set + } + + const resourceCache = new Map(); + + const fetchStart = log.isLevelEnabled("perf") ? performance.now() : 0; + let cacheHits = 0; + let cacheMisses = 0; + + // Phase 1: Collect all unique paths to fetch + const allPathsToFetch = new Set(); + for (const requestSetId of matchingRequestSetIds) { + const resourcePathsToUpdate = updatesByRequestSetId.get(requestSetId); + for (const resourcePath of resourcePathsToUpdate) { + if (!resourceCache.has(resourcePath)) { + allPathsToFetch.add(resourcePath); + cacheMisses++; + } else { + cacheHits++; + } + } + } + + // Phase 2: Batch-fetch in parallel + if (allPathsToFetch.size > 0) { + await Promise.all(Array.from(allPathsToFetch).map(async (resourcePath) => { + const resource = await reader.byPath(resourcePath); + resourceCache.set(resourcePath, resource ?? null); + })); + } + if (log.isLevelEnabled("perf")) { + log.perf( + `updateIndices for task '${this.#taskName}' of project '${this.#projectName}' ` + + `resource fetch completed in ${(performance.now() - fetchStart).toFixed(2)} ms: ` + + `${cacheHits} cache hits, ${cacheMisses} cache misses`); + } + + // Phase 3: Process each request set from cache + for (const requestSetId of matchingRequestSetIds) { + const {resourceIndex} = this.#requestGraph.getMetadata(requestSetId); + if (!resourceIndex) { + throw new Error(`Missing resource index for request set ID ${requestSetId}`); + } + + const resourcePathsToUpdate = updatesByRequestSetId.get(requestSetId); + const resourcesToUpdate = []; + const removedResourcePaths = []; + for (const resourcePath of resourcePathsToUpdate) { + const resource = resourceCache.get(resourcePath); + if (resource) { + resourcesToUpdate.push(resource); + } else { + removedResourcePaths.push(resourcePath); + } + } + if (removedResourcePaths.length) { + await resourceIndex.removeResources(removedResourcePaths); + } + if (resourcesToUpdate.length) { + await resourceIndex.upsertResources(resourcesToUpdate); + } + } + let hasChanges; + if (this.#useDifferentialUpdate) { + hasChanges = await this.#flushTreeChangesWithDiffTracking(); + } else { + hasChanges = await this.#flushTreeChangesWithoutDiffTracking(); + } + if (hasChanges) { + this.#hasNewOrModifiedCacheEntries = true; + } + return hasChanges; + } + + /** + * Returns whether any resource requests have been recorded + * + * @public + * @returns {boolean} + */ + hasRequests() { + return this.#requestGraph.getSize() > 0; + } + + /** + * Matches changed resources against a set of requests + * + * Tests each request against the changed resource paths using exact path matching + * for 'path'/'dep-path' requests and glob pattern matching for 'patterns'/'dep-patterns' requests. + * + * @param {Request[]} resourceRequests - Array of resource requests to match against + * @param {string[]} resourcePaths - Changed project resource paths + * @returns {string[]} Array of matched resource paths + */ + #matchResourcePaths(resourceRequests, resourcePaths) { + const matchedResources = []; + for (const {type, value} of resourceRequests) { + if (type === "path") { + if (resourcePaths.includes(value) && !matchedResources.includes(value)) { + matchedResources.push(value); + } + } else { + const globMatches = micromatch(resourcePaths, value, { + dot: true + }); + for (const match of globMatches) { + if (!matchedResources.includes(match)) { + matchedResources.push(match); + } + } + } + } + return matchedResources; + } + + /** + * Flushes all tree registries to apply batched updates without tracking changes + * + * Commits all pending tree modifications but does not record the specific changes + * (added, updated, removed resources). Used when differential updates are disabled. + * + * @returns {Promise} True if any changes were detected, false otherwise + */ + async #flushTreeChangesWithoutDiffTracking() { + const results = await this.#flushTreeChanges(); + + // Check for changes + for (const res of results) { + if (res.added.length || res.updated.length || res.unchanged.length || res.removed.length) { + return true; + } + } + return false; + } + + /** + * Flushes all tree registries to apply batched updates while tracking changes + * + * Commits all pending tree modifications and records detailed information about + * which resources were added, updated, or removed. Used when differential updates + * are enabled to support incremental cache invalidation. + * + * @returns {Promise} True if any changes were detected, false otherwise + */ + async #flushTreeChangesWithDiffTracking() { + const requestSetIds = this.#requestGraph.getAllNodeIds(); + const previousTreeSignatures = new Map(); + // Record current signatures and create mapping between trees and request sets + requestSetIds.map((requestSetId) => { + const {resourceIndex} = this.#requestGraph.getMetadata(requestSetId); + if (!resourceIndex) { + throw new Error(`Resource index missing for request set ID ${requestSetId}`); + } + // Remember the original signature + previousTreeSignatures.set(resourceIndex.getTree(), [requestSetId, resourceIndex.getSignature()]); + }); + const results = await this.#flushTreeChanges(); + let hasChanges = false; + for (const res of results) { + if (res.added.length || res.updated.length || res.unchanged.length || res.removed.length) { + hasChanges = true; + } + for (const [tree, diff] of res.treeStats) { + const [requestSetId, originalSignature] = previousTreeSignatures.get(tree); + const newSignature = tree.getRootHash(); + this.#addDeltaEntry(requestSetId, originalSignature, newSignature, diff); + } + } + return hasChanges; + } + + /** + * Flushes all tree registries to apply batched updates + * + * Commits all pending tree modifications across all registries in parallel. + * Must be called after operations that schedule updates via registries. + * + * @returns {Promise>} Array of flush results from all registries, + * each containing added, updated, unchanged, and removed resource paths + */ + async #flushTreeChanges() { + const flushStart = log.isLevelEnabled("perf") ? performance.now() : 0; + const results = await Promise.all(this.#treeRegistries.map((registry) => registry.flush())); + if (log.isLevelEnabled("perf")) { + log.perf( + `#flushTreeChanges for task '${this.#taskName}' of project '${this.#projectName}' ` + + `completed in ${(performance.now() - flushStart).toFixed(2)} ms ` + + `across ${this.#treeRegistries.length} registries`); + } + return results; + } + + /** + * Adds or updates a delta entry for tracking resource index changes + * + * Records the transition from an original signature to a new signature along with + * the specific resources that changed. Accumulates changes across multiple updates. + * + * @param {string} requestSetId Identifier of the request set + * @param {string} originalSignature Original resource index signature + * @param {string} newSignature New resource index signature + * @param {object} diff Object containing arrays of added, updated, unchanged, and removed resource paths + */ + #addDeltaEntry(requestSetId, originalSignature, newSignature, diff) { + if (!this.#treeUpdateDeltas.has(requestSetId)) { + this.#treeUpdateDeltas.set(requestSetId, { + originalSignature, + newSignature, + diff + }); + return; + } + const entry = this.#treeUpdateDeltas.get(requestSetId); + + entry.previousSignatures ??= []; + entry.previousSignatures.push(entry.originalSignature); + entry.originalSignature = originalSignature; + entry.newSignature = newSignature; + + const {added, updated, unchanged, removed} = entry.diff; + for (const resourcePath of diff.added) { + if (!added.includes(resourcePath)) { + added.push(resourcePath); + } + } + for (const resourcePath of diff.updated) { + if (!updated.includes(resourcePath)) { + updated.push(resourcePath); + } + } + for (const resourcePath of diff.unchanged) { + if (!unchanged.includes(resourcePath)) { + unchanged.push(resourcePath); + } + } + for (const resourcePath of diff.removed) { + if (!removed.includes(resourcePath)) { + removed.push(resourcePath); + } + } + } + + /** + * Gets all delta entries for differential cache updates + * + * Returns a map of signature transitions and their associated changed resource paths. + * Only includes deltas where no resources were removed, as removed resources prevent + * differential updates. + * + * @public + * @returns {Map} Map from original signature to delta information + * containing newSignature and changedPaths array + */ + getDeltas() { + const deltas = new Map(); + for (const {originalSignature, newSignature, diff} of this.#treeUpdateDeltas.values()) { + let changedPaths; + if (diff) { + const {added, updated, removed} = diff; + if (removed.length) { + // Cannot use differential build if a resource has been removed + continue; + } + changedPaths = Array.from(new Set([...added, ...updated])); + } else { + changedPaths = []; + } + deltas.set(originalSignature, { + newSignature, + changedPaths, + }); + } + return deltas; + } + + /** + * Adds a new set of resource requests and returns their signature + * + * Processes recorded resource requests (both path and pattern-based), creates or reuses + * a request set in the graph, and returns the resulting resource index signature. + * + * @public + * @param {object} requestRecording Project resource requests + * @param {string[]} requestRecording.paths Array of requested resource paths + * @param {Array} requestRecording.patterns Array of glob pattern arrays + * @param {module:@ui5/fs.AbstractReader} reader Reader for accessing project resources + * @returns {Promise} Object containing setId and signature of the resource index + */ + async addRequests(requestRecording, reader) { + const projectRequests = []; + for (const pathRead of requestRecording.paths) { + projectRequests.push(new Request("path", pathRead)); + } + for (const patterns of requestRecording.patterns) { + projectRequests.push(new Request("patterns", patterns)); + } + return await this.#addRequestSet(projectRequests, reader); + } + + /** + * Records that a task made no resource requests + * + * Marks the manager as having been unused at least once and returns a special + * signature indicating no requests were made. + * + * @public + * @returns {string} Special signature "X" indicating no requests + */ + recordNoRequests() { + if (!this.#unusedAtLeastOnce) { + this.#hasNewOrModifiedCacheEntries = true; + } + this.#unusedAtLeastOnce = true; + return "X"; // Signature for when no requests were made + } + + /** + * Adds a request set and creates or reuses a resource index + * + * Attempts to find an existing matching request set to reuse. If not found, creates + * a new request set with either a derived or fresh resource index based on whether + * a parent request set exists. + * + * @param {Request[]} requests Array of resource requests + * @param {module:@ui5/fs.AbstractReader} reader Reader for accessing project resources + * @returns {Promise} Object containing setId and signature of the resource index + */ + async #addRequestSet(requests, reader) { + this.#hasNewOrModifiedCacheEntries = true; + // Try to find an existing request set that we can reuse + let setId = this.#requestGraph.findExactMatch(requests); + let resourceIndex; + if (setId) { + // Reuse existing resource index. + // Note: This index has already been updated before the task executed, so no update is necessary here + resourceIndex = this.#requestGraph.getMetadata(setId).resourceIndex; + } else { + // New request set, check whether we can create a delta + const metadata = {}; // Will populate with resourceIndex below + setId = this.#requestGraph.addRequestSet(requests, metadata); + + const requestSet = this.#requestGraph.getNode(setId); + const parentId = requestSet.getParentId(); + if (parentId) { + const {resourceIndex: parentResourceIndex} = this.#requestGraph.getMetadata(parentId); + // Add resources from delta to index + const addedRequests = requestSet.getAddedRequests(); + const resourcesToAdd = + await this.#getResourcesForRequests(addedRequests, reader); + if (!resourcesToAdd.length) { + throw new Error(`Unexpected empty added resources for request set ID ${setId} ` + + `of task '${this.#taskName}' of project '${this.#projectName}'`); + } + log.verbose(`Task '${this.#taskName}' of project '${this.#projectName}' ` + + `created derived resource index for request set ID ${setId} ` + + `based on parent ID ${parentId} with ${resourcesToAdd.length} additional resources`); + resourceIndex = await parentResourceIndex.deriveTree(resourcesToAdd); + } else { + const resourcesRead = + await this.#getResourcesForRequests(requests, reader); + resourceIndex = await ResourceIndex.createShared(resourcesRead, Date.now(), this.#newTreeRegistry()); + } + metadata.resourceIndex = resourceIndex; + } + return { + setId, + signature: resourceIndex.getSignature(), + }; + } + + /** + * Associates a request set from this manager with one from another manager + * + * @public + * @param {string} ourRequestSetId Request set ID from this manager + * @param {string} foreignRequestSetId Request set ID from another manager + * @todo Implementation pending + */ + addAffiliatedRequestSet(ourRequestSetId, foreignRequestSetId) { + // TODO + } + + /** + * Creates and registers a new tree registry + * + * Tree registries enable batched updates across multiple derived trees, + * improving performance when multiple indices share common subtrees. + * + * @returns {TreeRegistry} New tree registry instance + */ + #newTreeRegistry() { + const registry = new TreeRegistry(); + this.#treeRegistries.push(registry); + return registry; + } + + /** + * Retrieves resources for a set of resource requests + * + * Processes different request types: + * - 'path': Retrieves single resource by path from the given reader + * - 'patterns': Retrieves resources matching glob patterns from the given reader + * + * @param {Request[]|Array<{type: string, value: string|string[]}>} resourceRequests - Resource requests to process + * @param {module:@ui5/fs.AbstractReader} reader - Resource reader + * @param {Map} [resourceCache] + * @returns {Promise>} Array of matched resources + */ + async #getResourcesForRequests(resourceRequests, reader, resourceCache) { + const resourcesMap = new Map(); + await Promise.all(resourceRequests.map(async ({type, value}) => { + if (type === "path") { + if (resourcesMap.has(value)) { + // Resource already found + return; + } + if (resourceCache?.has(value)) { + const cachedResource = resourceCache.get(value); + resourcesMap.set(cachedResource.getOriginalPath(), cachedResource); + } + const resource = await reader.byPath(value); + if (resource) { + resourcesMap.set(resource.getOriginalPath(), resource); + } + } else if (type === "patterns") { + const matchedResources = await reader.byGlob(value); + for (const resource of matchedResources) { + resourcesMap.set(resource.getOriginalPath(), resource); + } + } + })); + return Array.from(resourcesMap.values()); + } + + /** + * Checks whether new or modified cache entries exist + * + * Returns false if the manager was restored from cache and no modifications were made. + * Returns true if this is a new manager or if new request sets have been added. + * + * @public + * @returns {boolean} True if cache entries need to be written + */ + hasNewOrModifiedCacheEntries() { + return this.#hasNewOrModifiedCacheEntries; + } + + /** + * Serializes the manager to a plain object for persistence + * + * Exports the resource request graph and all resource indices in a format suitable + * for JSON serialization. The serialized data can be passed to fromCache() to restore + * the manager state. Returns undefined if no new or modified cache entries exist. + * + * @public + * @returns {object|undefined} Serialized cache metadata or undefined if no changes + * @returns {object} return.requestSetGraph Serialized request graph + * @returns {Array} return.rootIndices Array of root resource indices with node IDs + * @returns {Array} return.deltaIndices Array of delta resource indices with node IDs + * @returns {boolean} return.unusedAtLeastOnce Whether the task has been unused + */ + toCacheObject() { + if (!this.#hasNewOrModifiedCacheEntries) { + return; + } + const rootIndices = []; + const deltaIndices = []; + for (const {nodeId, parentId} of this.#requestGraph.traverseByDepth()) { + const {resourceIndex} = this.#requestGraph.getMetadata(nodeId); + if (!resourceIndex) { + throw new Error(`Missing resource index for node ID ${nodeId}`); + } + if (!parentId) { + rootIndices.push({ + nodeId, + resourceIndex: resourceIndex.toCacheObject(), + }); + } else { + const {resourceIndex: rootResourceIndex} = this.#requestGraph.getMetadata(parentId); + if (!rootResourceIndex) { + throw new Error(`Missing root resource index for parent ID ${parentId}`); + } + // Store the metadata for all added resources. Note: Those resources might not be available + // in the current tree. In that case we store an empty array. + const addedResourceIndex = resourceIndex.getAddedResourceIndex(rootResourceIndex); + deltaIndices.push({ + nodeId, + addedResourceIndex, + }); + } + } + return { + requestSetGraph: this.#requestGraph.toCacheObject(), + rootIndices, + deltaIndices, + unusedAtLeastOnce: this.#unusedAtLeastOnce, + }; + } +} + +export default ResourceRequestManager; diff --git a/packages/project/lib/build/cache/StageCache.js b/packages/project/lib/build/cache/StageCache.js new file mode 100644 index 00000000000..50699044b86 --- /dev/null +++ b/packages/project/lib/build/cache/StageCache.js @@ -0,0 +1,111 @@ +/** + * @typedef {object} StageCacheEntry + * @property {object} stage The cached stage instance (typically a reader or writer) + * @property {string[]} writtenResourcePaths Array of resource paths written during stage execution + * @property {Map>} resourceTagOperations + * Map of resource paths to their tags that were set or cleared during this stage's execution + */ + +/** + * In-memory cache for build stage results + * + * Manages cached build stages by their signatures, allowing quick lookup and reuse + * of previously executed build stages. Each stage is identified by a stage ID + * (e.g., "task/taskName") and a signature (content hash of input resources). + * + * The cache maintains a queue of added signatures that need to be persisted, + * enabling batch writes to persistent storage. + * + * Key features: + * - Fast in-memory lookup by stage ID and signature + * - Tracks written resources for cache invalidation + * - Supports batch persistence via flush queue + * - Multiple signatures per stage ID (for different input combinations) + * + * @class + */ +export default class StageCache { + #stageIdToSignatures = new Map(); + #cacheQueue = []; + + /** + * Adds a stage signature to the cache + * + * Stores the stage instance and its written resources under the given stage ID + * and signature. The signature is added to the flush queue for later persistence. + * + * Multiple signatures can exist for the same stage ID, representing different + * input resource combinations that produce different outputs. + * + * @public + * @param {string} stageId Identifier for the stage (e.g., "task/generateBundle") + * @param {string} signature Content hash signature of the stage's input resources + * @param {object} stageInstance The stage instance to cache (typically a reader or writer) + * @param {string[]} writtenResourcePaths Array of resource paths written during this stage + * @param {Map>} projectTagOperations + * @param {Map>} buildTagOperations + * Map of resource paths to their tags that were set or cleared during this stage's execution + */ + addSignature(stageId, signature, stageInstance, writtenResourcePaths, projectTagOperations, buildTagOperations) { + if (!this.#stageIdToSignatures.has(stageId)) { + this.#stageIdToSignatures.set(stageId, new Map()); + } + const signatureToStageInstance = this.#stageIdToSignatures.get(stageId); + signatureToStageInstance.set(signature, { + signature, + stage: stageInstance, + writtenResourcePaths, + projectTagOperations, + buildTagOperations, + }); + this.#cacheQueue.push([stageId, signature]); + } + + /** + * Retrieves cached stage data for a specific signature + * + * Looks up a previously cached stage by its ID and signature. Returns null + * if either the stage ID or signature is not found in the cache. + * + * @public + * @param {string} stageId Identifier for the stage to look up + * @param {string} signature Signature hash to match + * @returns {StageCacheEntry|null} Cached stage entry with stage instance and written paths, + * or null if not found + */ + getCacheForSignature(stageId, signature) { + if (!this.#stageIdToSignatures.has(stageId)) { + return null; + } + const signatureToStageInstance = this.#stageIdToSignatures.get(stageId); + return signatureToStageInstance.get(signature) || null; + } + + /** + * Retrieves and clears the cache queue + * + * Returns all stage signatures that have been added since the last flush, + * then resets the queue. The returned entries should be persisted to storage. + * + * Each queue entry is a tuple of [stageId, signature] that can be used to + * retrieve the full stage data via getCacheForSignature(). + * + * @public + * @returns {Array<[string, string]>} Array of [stageId, signature] tuples to persist + */ + flushCacheQueue() { + const queue = this.#cacheQueue; + this.#cacheQueue = []; + return queue; + } + + /** + * Checks if there are pending entries in the cache queue + * + * @public + * @returns {boolean} True if there are entries to flush, false otherwise + */ + hasPendingCacheQueue() { + return this.#cacheQueue.length > 0; + } +} diff --git a/packages/project/lib/build/cache/index/HashTree.js b/packages/project/lib/build/cache/index/HashTree.js new file mode 100644 index 00000000000..182472e5e61 --- /dev/null +++ b/packages/project/lib/build/cache/index/HashTree.js @@ -0,0 +1,835 @@ +import crypto from "node:crypto"; +import path from "node:path/posix"; +import TreeNode from "./TreeNode.js"; +import {matchResourceMetadataStrict} from "../utils.js"; + +/** + * @typedef {object} @ui5/project/build/cache/index/HashTree~ResourceMetadata + * @property {string} path Resource path using POSIX separators, prefixed with a slash (e.g. "/resources/file.js") + * @property {number} size File size in bytes + * @property {number} lastModified Last modification timestamp + * @property {number|undefined} inode File inode identifier + * @property {string} integrity Content hash + * @property {Object|null} [tags] Resource tags (key-value pairs) + */ + +/** + * Compare two tag objects for equality. + * Treats null, undefined, and empty {} as equivalent (no tags). + * + * @param {Object|null} a + * @param {Object|null} b + * @returns {boolean} + */ +export function tagsEqual(a, b) { + const aEmpty = !a || Object.keys(a).length === 0; + const bEmpty = !b || Object.keys(b).length === 0; + if (aEmpty && bEmpty) return true; + if (aEmpty !== bEmpty) return false; + const aKeys = Object.keys(a).sort(); + const bKeys = Object.keys(b).sort(); + if (aKeys.length !== bKeys.length) return false; + return aKeys.every((key, i) => key === bKeys[i] && a[key] === b[key]); +} + +/** + * Directory-based Merkle Tree for efficient resource tracking with hierarchical structure. + * + * Computes deterministic SHA256 hashes for resources and directories, enabling: + * - Fast change detection via root hash comparison + * - Structural sharing through derived trees (memory efficient) + * - Batch upsert and removal operations + * + * Primary use case: Build caching systems where multiple related resource trees + * (e.g., source files, build artifacts) need to be tracked and synchronized efficiently. + */ +export default class HashTree { + #indexTimestamp; + /** + * Create a new HashTree + * + * @param {Array|null} resources + * Initial resources to populate the tree. Each resource should have a path and optional metadata. + * @param {object} options + * @param {number} [options.indexTimestamp] Timestamp of the latest resource metadata update + * @param {TreeNode} [options._root] Internal: pre-existing root node for derived trees (enables structural sharing) + */ + constructor(resources = null, options = {}) { + this.root = options._root || new TreeNode("", "directory"); + this.#indexTimestamp = options.indexTimestamp; + + if (resources && !options._root) { + this._buildTree(resources); + } else if (resources && options._root) { + // Derived tree: insert additional resources into shared structure + for (const resource of resources) { + this._insertResourceWithSharing(resource.path, resource); + } + // Recompute hashes for newly added paths + this._computeHash(this.root); + } + } + + /** + * Shallow copy a directory node (copies node, shares children) + * + * @param {TreeNode} dirNode + * @returns {TreeNode} + * @private + */ + _shallowCopyDirectory(dirNode) { + if (dirNode.type !== "directory") { + throw new Error("Can only shallow copy directory nodes"); + } + + const copy = new TreeNode(dirNode.name, "directory", { + hash: dirNode.hash ? Buffer.from(dirNode.hash) : null, + children: new Map(dirNode.children) // Shallow copy of Map (shares TreeNode references) + }); + + return copy; + } + + /** + * Build tree from resource list + * + * @param {Array<{path: string, integrity?: string}>} resources + * @private + */ + _buildTree(resources) { + // Sort resources by path for deterministic ordering + const sortedResources = [...resources].sort((a, b) => a.path.localeCompare(b.path)); + + // Insert each resource into the tree + for (const resource of sortedResources) { + this._insertResource(resource.path, resource); + } + + // Compute all hashes bottom-up + this._computeHash(this.root); + } + + /** + * Insert a resource with structural sharing for derived trees + * Implements copy-on-write: only copies directories that will be modified + * + * Key optimization: When adding "a/b/c/file.js", only copies: + * - Directory "c" (will get new child) + * Directories "a" and "b" remain shared references if they existed. + * + * This preserves memory efficiency when derived trees have different + * resources in some paths but share others. + * + * @param {string} resourcePath + * @param {object} resourceData + * @private + */ + _insertResourceWithSharing(resourcePath, resourceData) { + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + let current = this.root; + + // Navigate and copy-on-write for all directories in the path + for (let i = 0; i < parts.length - 1; i++) { + const dirName = parts[i]; + + if (!current.children.has(dirName)) { + // Create new directory from here + const newDir = new TreeNode(dirName, "directory"); + current.children.set(dirName, newDir); + current = newDir; + } else { + // Directory exists - need to copy it because we'll modify its children + const existing = current.children.get(dirName); + if (existing.type !== "directory") { + throw new Error(`Path conflict: ${dirName} exists as resource but expected directory`); + } + + // Shallow copy to preserve copy-on-write semantics + const copiedDir = this._shallowCopyDirectory(existing); + current.children.set(dirName, copiedDir); + current = copiedDir; + } + } + + // Insert the resource + const resourceName = parts[parts.length - 1]; + + // if (current.children.has(resourceName)) { + // throw new Error(`Duplicate resource path: ${resourcePath}`); + // } + + const resourceNode = new TreeNode(resourceName, "resource", { + integrity: resourceData.integrity, + lastModified: resourceData.lastModified, + size: resourceData.size, + inode: resourceData.inode, + tags: resourceData.tags || null + }); + + current.children.set(resourceName, resourceNode); + } + + /** + * Insert a resource into the directory tree + * + * @param {string} resourcePath + * @param {object} resourceData + * @param {string} [resourceData.integrity] - Content hash for regular resources + * @param {number} [resourceData.lastModified] - Last modified timestamp + * @param {number} [resourceData.size] - File size in bytes + * @param {number} [resourceData.inode] - File system inode number + * @param {Object|null} [resourceData.tags] - Resource tags (key-value pairs) + * @private + */ + _insertResource(resourcePath, resourceData) { + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + let current = this.root; + + // Navigate/create directory structure + for (let i = 0; i < parts.length - 1; i++) { + const dirName = parts[i]; + + if (!current.children.has(dirName)) { + current.children.set(dirName, new TreeNode(dirName, "directory")); + } + + current = current.children.get(dirName); + + if (current.type !== "directory") { + throw new Error(`Path conflict: ${dirName} exists as resource but expected directory`); + } + } + + // Insert the resource + const resourceName = parts[parts.length - 1]; + + if (current.children.has(resourceName)) { + throw new Error(`Duplicate resource path: ${resourcePath}`); + } + + const resourceNode = new TreeNode(resourceName, "resource", { + integrity: resourceData.integrity, + lastModified: resourceData.lastModified, + size: resourceData.size, + inode: resourceData.inode, + tags: resourceData.tags || null + }); + + current.children.set(resourceName, resourceNode); + } + + /** + * Compute hash for a node and all its children (recursive) + * + * For resource nodes, the hash incorporates the resource name, integrity, and tags + * (when present). Tags are sorted by key for deterministic hashing. + * Resources with no tags (null, undefined, or empty {}) produce the same hash + * as tagless resources for backward compatibility. + * + * @param {TreeNode} node + * @returns {Buffer} + * @private + */ + _computeHash(node) { + if (node.type === "resource") { + // Resource hash — includes tags when present for cache invalidation + let hashInput = `resource:${node.name}:${node.integrity}`; + if (node.tags && Object.keys(node.tags).length > 0) { + const sortedKeys = Object.keys(node.tags).sort(); + const tagString = sortedKeys.map((k) => `${k}=${String(node.tags[k])}`).join(","); + hashInput += `:tags(${tagString})`; + } + node.hash = this._hashData(hashInput); + } else { + // Directory hash - compute from sorted children + const childHashes = []; + + // Sort children by name for deterministic ordering + const sortedChildren = Array.from(node.children.entries()) + .sort((a, b) => a[0].localeCompare(b[0])); + + for (const [, child] of sortedChildren) { + this._computeHash(child); // Recursively compute child hashes + childHashes.push(child.hash); + } + + // Combine all child hashes + if (childHashes.length === 0) { + // Empty directory + node.hash = this._hashData(`dir:${node.name}:empty`); + } else { + const combined = Buffer.concat(childHashes); + node.hash = crypto.createHash("sha256") + .update(`dir:${node.name}:`) + .update(combined) + .digest(); + } + } + + return node.hash; + } + + /** + * Hash a string + * + * @param {string} data + * @returns {Buffer} + * @private + */ + _hashData(data) { + return crypto.createHash("sha256").update(data).digest(); + } + + /** + * Get the root hash as a hex string + * + * @returns {string} + */ + getRootHash() { + if (!this.root.hash) { + this._computeHash(this.root); + } + return this.root.hash.toString("hex"); + } + + /** + * Get the index timestamp + * + * @returns {number} + */ + getIndexTimestamp() { + return this.#indexTimestamp; + } + + setIndexTimestamp(timestamp) { + if (timestamp) { + this.#indexTimestamp = timestamp; + } + } + + /** + * Find a node by path + * + * @param {string} resourcePath + * @returns {TreeNode|null} + * @private + */ + _findNode(resourcePath) { + if (!resourcePath || resourcePath === "" || resourcePath === ".") { + return this.root; + } + + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + let current = this.root; + + for (const part of parts) { + if (!current.children.has(part)) { + return null; + } + current = current.children.get(part); + } + + return current; + } + + /** + * Upsert multiple resources (insert if new, update if exists). + * + * Intelligently determines whether each resource is new (insert) or existing (update). + * Applies operations immediately with optimized hash recomputation. + * + * Automatically creates missing parent directories during insertion. + * Skips resources whose metadata and tags haven't changed (optimization). + * A tag-only change (content unchanged but tags differ) is treated as an update. + * + * @param {Array<@ui5/fs/Resource>} resources - Array of Resource instances to upsert + * @param {number} newIndexTimestamp Timestamp at which the provided resources have been indexed + * @returns {Promise<{added: Array, updated: Array, unchanged: Array}>} + * Status report: arrays of paths by operation type. + */ + async upsertResources(resources, newIndexTimestamp) { + if (!resources || resources.length === 0) { + return {added: [], updated: [], unchanged: []}; + } + + const added = []; + const updated = []; + const unchanged = []; + const affectedPaths = new Set(); + + // Phase 1: Filter out clearly-unchanged resources using cheap sync checks. + // This avoids async/Promise overhead and unnecessary I/O for the common case + // where most resources haven't changed (lastModified short-circuit). + const needsIO = []; + + for (const resource of resources) { + const resourcePath = resource.getOriginalPath(); + const existingNode = this.getResourceByPath(resourcePath); + + if (!existingNode) { + // New resource — always needs I/O + needsIO.push({resource, resourcePath, existingNode: null, isNew: true}); + continue; + } + + // Replicate matchResourceMetadataStrict's fast path (sync, no I/O): + // If lastModified matches and is not at risk of race condition, content is unchanged. + const currentLastModified = resource.getLastModified(); + if (currentLastModified === existingNode.lastModified && + this.#indexTimestamp && currentLastModified !== this.#indexTimestamp) { + // Content definitely unchanged — check tags + if (tagsEqual(existingNode.tags, resource.getTags())) { + unchanged.push(resourcePath); + } else { + // Tag-only change — update tags without I/O + existingNode.tags = resource.getTags(); + this._computeHash(existingNode); + updated.push(resourcePath); + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + for (let i = 0; i < parts.length; i++) { + affectedPaths.add(parts.slice(0, i).join(path.sep)); + } + } + continue; + } + + // Potentially changed — needs I/O to determine + needsIO.push({resource, resourcePath, existingNode, isNew: false}); + } + + // Phase 2: Resolve I/O concurrently for resources that need it. + // Only new or potentially-changed resources reach this point. + if (needsIO.length > 0) { + const preResolved = await Promise.all(needsIO.map(async ({resource, resourcePath, existingNode, isNew}) => { + if (isNew) { + const [integrity, size] = await Promise.all([ + resource.getIntegrity(), + resource.getSize() + ]); + return { + resourcePath, + existingNode: null, + isNew: true, + integrity, + size, + lastModified: resource.getLastModified(), + inode: resource.getInode(), + tags: resource.getTags() + }; + } + + // Existing resource with potential change — use matchResourceMetadataStrict + // for the remaining checks (size comparison, integrity comparison) + const currentMetadata = { + integrity: existingNode.integrity, + lastModified: existingNode.lastModified, + size: existingNode.size, + inode: existingNode.inode + }; + + const isUnchanged = + await matchResourceMetadataStrict(resource, currentMetadata, this.#indexTimestamp); + + if (isUnchanged) { + return {resourcePath, existingNode, isUnchanged: true, tags: resource.getTags()}; + } + + // Changed — integrity/size are cached in the Resource from matchResourceMetadataStrict + const [integrity, size] = await Promise.all([ + resource.getIntegrity(), + resource.getSize() + ]); + return { + resourcePath, + existingNode, + isNew: false, + isUnchanged: false, + integrity, + size, + lastModified: resource.getLastModified(), + inode: resource.getInode(), + tags: resource.getTags() + }; + })); + + // Phase 3: Apply resolved results to the tree + for (const resolved of preResolved) { + if (resolved.isUnchanged) { + if (tagsEqual(resolved.existingNode.tags, resolved.tags)) { + unchanged.push(resolved.resourcePath); + } else { + resolved.existingNode.tags = resolved.tags; + this._computeHash(resolved.existingNode); + updated.push(resolved.resourcePath); + const parts = resolved.resourcePath.split(path.sep).filter((p) => p.length > 0); + for (let i = 0; i < parts.length; i++) { + affectedPaths.add(parts.slice(0, i).join(path.sep)); + } + } + continue; + } + + const parts = resolved.resourcePath.split(path.sep).filter((p) => p.length > 0); + + if (resolved.isNew) { + this._insertResource(resolved.resourcePath, { + integrity: resolved.integrity, + lastModified: resolved.lastModified, + size: resolved.size, + inode: resolved.inode, + tags: resolved.tags + }); + + const resourceNode = this._findNode(resolved.resourcePath); + this._computeHash(resourceNode); + added.push(resolved.resourcePath); + } else { + resolved.existingNode.integrity = resolved.integrity; + resolved.existingNode.lastModified = resolved.lastModified; + resolved.existingNode.size = resolved.size; + resolved.existingNode.inode = resolved.inode; + resolved.existingNode.tags = resolved.tags; + + this._computeHash(resolved.existingNode); + updated.push(resolved.resourcePath); + } + + for (let i = 0; i < parts.length; i++) { + affectedPaths.add(parts.slice(0, i).join(path.sep)); + } + } + } + + // Recompute directory hashes bottom-up + const sortedPaths = Array.from(affectedPaths).sort((a, b) => { + const depthA = a ? a.split(path.sep).length : 0; + const depthB = b ? b.split(path.sep).length : 0; + if (depthA !== depthB) return depthB - depthA; + return a.localeCompare(b); + }); + + for (const dirPath of sortedPaths) { + const node = this._findNode(dirPath); + if (node && node.type === "directory") { + this._computeHash(node); + } + } + this.setIndexTimestamp(newIndexTimestamp); + return {added, updated, unchanged}; + } + + /** + * Remove multiple resources efficiently. + * + * Removes resources immediately and recomputes affected ancestor hashes. + * + * @param {Array} resourcePaths - Array of resource paths to remove + * @returns {Promise<{removed: Array, notFound: Array}>} + * Status report: 'removed' contains successfully removed paths, 'notFound' contains paths that didn't exist. + */ + async removeResources(resourcePaths) { + if (!resourcePaths || resourcePaths.length === 0) { + return {removed: [], notFound: []}; + } + + // Immediate mode + const removed = []; + const notFound = []; + const affectedPaths = new Set(); + + for (const resourcePath of resourcePaths) { + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + + if (parts.length === 0) { + throw new Error("Cannot remove root"); + } + + // Navigate to parent, keeping track of the path + const pathNodes = [this.root]; + let current = this.root; + let pathExists = true; + + for (let i = 0; i < parts.length - 1; i++) { + if (!current.children.has(parts[i])) { + pathExists = false; + break; + } + current = current.children.get(parts[i]); + pathNodes.push(current); + } + + if (!pathExists) { + notFound.push(resourcePath); + continue; + } + + // Remove resource + const resourceName = parts[parts.length - 1]; + const wasRemoved = current.children.delete(resourceName); + + if (wasRemoved) { + removed.push(resourcePath); + + // Clean up empty parent directories bottom-up + for (let i = parts.length - 1; i > 0; i--) { + const parentNode = pathNodes[i]; + if (parentNode.children.size === 0) { + // Directory is empty, remove it from its parent + const grandparentNode = pathNodes[i - 1]; + grandparentNode.children.delete(parts[i - 1]); + } else { + // Directory still has children, stop cleanup + break; + } + } + + // Mark ancestors for recomputation (only up to where directories still exist) + for (let i = 0; i < parts.length; i++) { + const ancestorPath = parts.slice(0, i).join(path.sep); + if (this._findNode(ancestorPath)) { + affectedPaths.add(ancestorPath); + } + } + } else { + notFound.push(resourcePath); + } + } + + // Recompute directory hashes bottom-up + const sortedPaths = Array.from(affectedPaths).sort((a, b) => { + const depthA = a ? a.split(path.sep).length : 0; + const depthB = b ? b.split(path.sep).length : 0; + if (depthA !== depthB) return depthB - depthA; + return a.localeCompare(b); + }); + + for (const dirPath of sortedPaths) { + const node = this._findNode(dirPath); + if (node && node.type === "directory") { + this._computeHash(node); + } + } + + return {removed, notFound}; + } + + /** + * Recompute hashes for all ancestor directories up to root. + * + * Used after modifications to ensure the entire path from the modified + * resource/directory up to the root has correct hash values. + * + * @param {string} resourcePath - Path to resource or directory that was modified + * @private + */ + _recomputeAncestorHashes(resourcePath) { + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + + // Recompute from deepest to root + for (let i = parts.length; i >= 0; i--) { + const dirPath = parts.slice(0, i).join(path.sep); + const node = this._findNode(dirPath); + if (node && node.type === "directory") { + this._computeHash(node); + } + } + } + + /** + * Get hash for a specific directory. + * + * Useful for checking if a specific subtree has changed without comparing the entire tree. + * + * @param {string} dirPath - Path to directory + * @returns {string} Directory hash as hex string + * @throws {Error} If path not found or path is not a directory + */ + getDirectoryHash(dirPath) { + const node = this._findNode(dirPath); + if (!node) { + throw new Error(`Path not found: ${dirPath}`); + } + if (node.type !== "directory") { + throw new Error(`Path is not a directory: ${dirPath}`); + } + return node.hash.toString("hex"); + } + + /** + * Check if a directory's contents have changed by comparing hashes. + * + * Efficient way to detect changes in a subtree without comparing individual files. + * + * @param {string} dirPath - Path to directory + * @param {string} previousHash - Previous hash to compare against + * @returns {boolean} true if directory contents changed, false otherwise + */ + hasDirectoryChanged(dirPath, previousHash) { + const currentHash = this.getDirectoryHash(dirPath); + return currentHash !== previousHash; + } + + /** + * Get tree statistics. + * + * Provides summary information about tree size and structure. + * + * @returns {{resources: number, directories: number, maxDepth: number, rootHash: string}} + * Statistics object with counts and root hash + */ + getStats() { + let resourceCount = 0; + let dirCount = 0; + let maxDepth = 0; + + const traverse = (node, depth) => { + maxDepth = Math.max(maxDepth, depth); + + if (node.type === "resource") { + resourceCount++; + } else { + dirCount++; + for (const child of node.children.values()) { + traverse(child, depth + 1); + } + } + }; + + traverse(this.root, 0); + + return { + resources: resourceCount, + directories: dirCount, + maxDepth, + rootHash: this.getRootHash() + }; + } + + /** + * Serialize tree to JSON + * + * @returns {object} + */ + toCacheObject() { + return { + version: 1, + root: this.root.toJSON(), + }; + } + + /** + * Deserialize tree from JSON + * + * @param {object} data + * @param {object} [options] + * @returns {HashTree} + */ + static fromCache(data, options = {}) { + if (data.version !== 1) { + throw new Error(`Unsupported version: ${data.version}`); + } + + const tree = new HashTree(null, options); + tree.root = TreeNode.fromJSON(data.root); + + return tree; + } + + /** + * Validate tree structure and hashes + * + * Currently unused, but possibly useful future integrity checks. + * + * @returns {boolean} + */ + validate() { + const errors = []; + + const validateNode = (node, currentPath) => { + // Recompute hash + const originalHash = node.hash; + this._computeHash(node); + + if (!originalHash.equals(node.hash)) { + errors.push(`Hash mismatch at ${currentPath || "root"}`); + } + + // Restore original (in case validation is non-destructive) + node.hash = originalHash; + + // Recurse for directories + if (node.type === "directory") { + for (const [name, child] of node.children) { + const childPath = currentPath ? path.join(currentPath, name) : name; + validateNode(child, childPath); + } + } + }; + + validateNode(this.root, ""); + + if (errors.length > 0) { + throw new Error(`Validation failed:\n${errors.join("\n")}`); + } + + return true; + } + + /** + * Create a deep clone of this tree. + * + * Unlike deriveTree(), this creates a completely independent copy + * with no shared node references. + * + * @returns {HashTree} New independent tree instance + */ + clone() { + const cloned = new HashTree(); + cloned.root = this.root.clone(); + return cloned; + } + + /** + * Get resource node by path + * + * @param {string} resourcePath + * @returns {TreeNode|null} + */ + getResourceByPath(resourcePath) { + const node = this._findNode(resourcePath); + return node && node.type === "resource" ? node : null; + } + + /** + * Check if a path exists in the tree + * + * @param {string} resourcePath + * @returns {boolean} + */ + hasPath(resourcePath) { + return this._findNode(resourcePath) !== null; + } + + /** + * Get all resource paths in sorted order + * + * @returns {Array} + */ + getResourcePaths() { + const paths = []; + + const traverse = (node, currentPath) => { + if (node.type === "resource") { + paths.push(currentPath); + } else { + for (const [name, child] of node.children) { + const childPath = currentPath ? path.join(currentPath, name) : name; + traverse(child, childPath); + } + } + }; + + traverse(this.root, "/"); + return paths.sort(); + } +} diff --git a/packages/project/lib/build/cache/index/ResourceIndex.js b/packages/project/lib/build/cache/index/ResourceIndex.js new file mode 100644 index 00000000000..e345922da6b --- /dev/null +++ b/packages/project/lib/build/cache/index/ResourceIndex.js @@ -0,0 +1,300 @@ +/** + * @module @ui5/project/build/cache/index/ResourceIndex + * @description Manages an indexed view of build resources with hash-based tracking. + * + * ResourceIndex provides efficient resource tracking through hash tree structures, + * enabling fast delta detection and signature calculation for build caching. + */ +import HashTree from "./HashTree.js"; +import SharedHashTree from "./SharedHashTree.js"; +import {createResourceIndex} from "../utils.js"; + +/** + * Manages an indexed view of build resources with content-based hashing. + * + * ResourceIndex wraps a HashTree to provide resource indexing capabilities for build caching. + * It maintains resource metadata (path, integrity, size, modification time) and computes + * signatures for change detection. The index supports efficient updates and can be + * persisted/restored from cache. + * + * @example + * // Create from resources + * const index = await ResourceIndex.create(resources, registry); + * const signature = index.getSignature(); + * + * @example + * // Update with delta detection + * const {changedPaths, resourceIndex} = await ResourceIndex.fromCacheWithDelta( + * cachedIndex, + * currentResources + * ); + */ +export default class ResourceIndex { + #tree; + + /** + * Creates a new ResourceIndex instance. + * + * @param {HashTree} tree - The hash tree containing resource metadata + * @private + */ + constructor(tree) { + this.#tree = tree; + } + + /** + * Creates a new ResourceIndex from a set of resources. + * + * Builds a hash tree from the provided resources, computing content hashes + * and metadata for each resource. The resulting index can be used for + * signature calculation and change tracking. + * + * @param {Array<@ui5/fs/Resource>} resources - Resources to index + * @param {number} indexTimestamp Timestamp at which the provided resources have been indexed + * @returns {Promise} A new resource index + * @public + */ + static async create(resources, indexTimestamp) { + const resourceIndex = await createResourceIndex(resources); + const tree = new HashTree(resourceIndex, {indexTimestamp}); + return new ResourceIndex(tree); + } + + /** + * Creates a new shared ResourceIndex from a set of resources. + * + * Creates a SharedHashTree that coordinates updates through a TreeRegistry. + * Use this for scenarios where multiple indices need to share nodes and + * coordinate batch updates. + * + * @param {Array<@ui5/fs/Resource>} resources - Resources to index + * @param {number} indexTimestamp Timestamp at which the provided resources have been indexed + * @param {TreeRegistry} registry - Registry for coordinated batch updates + * @returns {Promise} A new resource index with shared tree + * @public + */ + static async createShared(resources, indexTimestamp, registry) { + const resourceIndex = await createResourceIndex(resources); + const tree = new SharedHashTree(resourceIndex, registry, {indexTimestamp}); + return new ResourceIndex(tree); + } + + /** + * Restores a ResourceIndex from cache and applies delta updates. + * + * Takes a cached index and a current set of resources, then: + * 1. Identifies removed resources (in cache but not in current set) + * 2. Identifies added/updated resources (new or modified since cache) + * 3. Returns both the updated index and list of all changed paths + * + * This method is optimized for incremental builds where most resources + * remain unchanged between builds. + * + * @param {object} indexCache - Cached index object from previous build + * @param {number} indexCache.indexTimestamp - Timestamp of cached index + * @param {object} indexCache.indexTree - Cached hash tree structure + * @param {Array<@ui5/fs/Resource>} resources - Current resources to compare against cache + * @param {number} newIndexTimestamp Timestamp at which the provided resources have been indexed + * @returns {Promise<{changedPaths: string[], resourceIndex: ResourceIndex}>} + * Object containing array of all changed resource paths and the updated index + * @public + */ + static async fromCacheWithDelta(indexCache, resources, newIndexTimestamp) { + const {indexTimestamp, indexTree} = indexCache; + const tree = HashTree.fromCache(indexTree, {indexTimestamp}); + const currentResourcePaths = new Set(resources.map((resource) => resource.getOriginalPath())); + const removedPaths = tree.getResourcePaths().filter((resourcePath) => { + return !currentResourcePaths.has(resourcePath); + }); + const {removed} = await tree.removeResources(removedPaths); + const {added, updated} = await tree.upsertResources(resources, newIndexTimestamp); + return { + changedPaths: [...added, ...updated, ...removed], + resourceIndex: new ResourceIndex(tree), + }; + } + + /** + * Restores a shared ResourceIndex from cache and applies delta updates. + * + * Same as fromCacheWithDelta, but creates a SharedHashTree that coordinates + * updates through a TreeRegistry. + * + * @param {object} indexCache - Cached index object from previous build + * @param {number} indexCache.indexTimestamp - Timestamp of cached index + * @param {object} indexCache.indexTree - Cached hash tree structure + * @param {Array<@ui5/fs/Resource>} resources - Current resources to compare against cache + * @param {number} newIndexTimestamp Timestamp at which the provided resources have been indexed + * @param {TreeRegistry} registry - Registry for coordinated batch updates + * @returns {Promise<{changedPaths: string[], resourceIndex: ResourceIndex}>} + * Object containing array of all changed resource paths and the updated index + * @public + */ + static async fromCacheWithDeltaShared(indexCache, resources, newIndexTimestamp, registry) { + const {indexTimestamp, indexTree} = indexCache; + const tree = SharedHashTree.fromCache(indexTree, registry, {indexTimestamp}); + const currentResourcePaths = new Set(resources.map((resource) => resource.getOriginalPath())); + const removedPaths = tree.getResourcePaths().filter((resourcePath) => { + return !currentResourcePaths.has(resourcePath); + }); + await tree.removeResources(removedPaths); + await tree.upsertResources(resources, newIndexTimestamp); + // For shared trees, we need to flush the registry to get results + const {added, updated, removed} = await registry.flush(); + return { + changedPaths: [...added, ...updated, ...removed], + resourceIndex: new ResourceIndex(tree), + }; + } + + /** + * Restores a ResourceIndex from cached metadata. + * + * Reconstructs the resource index from cached metadata without performing + * content hash verification. Useful when the cache is known to be valid + * and fast restoration is needed. + * + * @param {object} indexCache - Cached index object + * @param {number} indexCache.indexTimestamp - Timestamp of cached index + * @param {object} indexCache.indexTree - Cached hash tree structure + * @returns {ResourceIndex} Restored resource index + * @public + */ + static fromCache(indexCache) { + const {indexTimestamp, indexTree} = indexCache; + const tree = HashTree.fromCache(indexTree, {indexTimestamp}); + return new ResourceIndex(tree); + } + + /** + * Restores a shared ResourceIndex from cached metadata. + * + * Same as fromCache, but creates a SharedHashTree that coordinates + * updates through a TreeRegistry. + * + * @param {object} indexCache - Cached index object + * @param {number} indexCache.indexTimestamp - Timestamp of cached index + * @param {object} indexCache.indexTree - Cached hash tree structure + * @param {TreeRegistry} registry - Registry for coordinated batch updates + * @returns {ResourceIndex} Restored resource index with shared tree + * @public + */ + static fromCacheShared(indexCache, registry) { + const {indexTimestamp, indexTree} = indexCache; + const tree = SharedHashTree.fromCache(indexTree, registry, {indexTimestamp}); + return new ResourceIndex(tree); + } + + getTree() { + return this.#tree; + } + + /** + * Creates a deep copy of this ResourceIndex. + * + * The cloned index has its own hash tree but shares the same timestamp + * as the original. Useful for creating independent index variations. + * + * @returns {ResourceIndex} A cloned resource index + * @public + */ + clone() { + const cloned = new ResourceIndex(this.#tree.clone()); + return cloned; + } + + /** + * Creates a derived ResourceIndex by adding additional resources. + * + * Derives a new hash tree from the current tree by incorporating + * additional resources. The original index remains unchanged. + * This is useful for creating task-specific resource views. + * + * @param {Array<@ui5/fs/Resource>} additionalResources - Resources to add to the derived index + * @returns {Promise} A new resource index with the additional resources + * @public + */ + async deriveTree(additionalResources) { + const resourceIndex = await createResourceIndex(additionalResources); + return new ResourceIndex(this.#tree.deriveTree(resourceIndex)); + } + + deriveTreeWithIndex(resourceIndex) { + return new ResourceIndex(this.#tree.deriveTree(resourceIndex)); + } + + /** + * Compares this index against a base index and returns metadata + * for resources that have been added in this index. + * + * @param {ResourceIndex} baseIndex - The base resource index to compare against + * @returns {Array<@ui5/project/build/cache/index/HashTree~ResourceMetadata>} + */ + getAddedResourceIndex(baseIndex) { + return this.#tree.getAddedResources(baseIndex.getTree()); + } + + /** + * Inserts or updates resources in the index. + * + * For each resource: + * - If it exists in the index and has changed, it's updated + * - If it doesn't exist in the index, it's added + * - If it exists and hasn't changed, no action is taken + * + * @param {Array<@ui5/fs/Resource>} resources - Resources to upsert + * @param {number} newIndexTimestamp Timestamp at which the provided resources have been indexed + * @returns {Promise<{added: string[], updated: string[]}>} + * Object with arrays of added and updated resource paths + * @public + */ + async upsertResources(resources, newIndexTimestamp) { + return await this.#tree.upsertResources(resources, newIndexTimestamp); + } + + /** + * Removes resources from the index. + * + * @param {Array} resourcePaths - Paths of resources to remove + */ + async removeResources(resourcePaths) { + return await this.#tree.removeResources(resourcePaths); + } + + getResourcePaths() { + return this.#tree.getResourcePaths(); + } + + /** + * Computes the signature hash for this resource index. + * + * The signature is the root hash of the underlying hash tree, + * representing the combined state of all indexed resources. + * Any change to any resource will result in a different signature. + * + * @returns {string} SHA-256 hash signature of the resource index + * @public + */ + getSignature() { + return this.#tree.getRootHash(); + } + + /** + * Serializes the ResourceIndex to a cache object. + * + * Converts the index to a plain object suitable for JSON serialization + * and storage in the build cache. The cached object can be restored + * using fromCache() or fromCacheWithDelta(). + * + * @returns {object} Cache object containing timestamp and tree structure + * @returns {number} return.indexTimestamp - Timestamp when index was created + * @returns {object} return.indexTree - Serialized hash tree structure + * @public + */ + toCacheObject() { + return { + indexTimestamp: this.#tree.getIndexTimestamp(), + indexTree: this.#tree.toCacheObject(), + }; + } +} diff --git a/packages/project/lib/build/cache/index/SharedHashTree.js b/packages/project/lib/build/cache/index/SharedHashTree.js new file mode 100644 index 00000000000..002479d7aba --- /dev/null +++ b/packages/project/lib/build/cache/index/SharedHashTree.js @@ -0,0 +1,206 @@ +import path from "node:path/posix"; +import HashTree from "./HashTree.js"; +import TreeNode from "./TreeNode.js"; + +/** + * Shared HashTree that coordinates updates through a TreeRegistry. + * + * This variant of HashTree is designed for scenarios where multiple trees need + * to share nodes and coordinate batch updates. All modifications (upserts and removals) + * are delegated to the registry, which applies them atomically across all registered trees. + * + * Key differences from base HashTree: + * - Requires a TreeRegistry instance + * - upsertResources() and removeResources() return undefined (results available via registry.flush()) + * - Derived trees share the same registry + * - Changes to shared nodes propagate to all trees + * + * @extends HashTree + */ +export default class SharedHashTree extends HashTree { + /** + * Create a new SharedHashTree + * + * @param {Array|null} resources + * Initial resources to populate the tree. Each resource should have a path and optional metadata. + * @param {TreeRegistry} registry Required registry for coordinated batch updates across multiple trees + * @param {object} options + * @param {number} [options.indexTimestamp] Timestamp of the latest resource metadata update + * @param {TreeNode} [options._root] Internal: pre-existing root node for derived trees (enables structural sharing) + */ + constructor(resources = null, registry, options = {}) { + if (!registry) { + throw new Error("SharedHashTree requires a registry option"); + } + + super(resources, options); + + this.registry = registry; + this.registry.register(this); + } + + /** + * Schedule resource upserts (insert or update) to be applied during registry flush. + * + * Unlike base HashTree, this method doesn't immediately modify the tree. + * Instead, it schedules operations with the registry for batch processing. + * Call registry.flush() to apply all pending operations atomically. + * + * @param {Array<@ui5/fs/Resource>} resources - Array of Resource instances to upsert + * @param {number} newIndexTimestamp Timestamp at which the provided resources have been indexed + * @returns {Promise} Returns undefined; results available via registry.flush() + */ + async upsertResources(resources, newIndexTimestamp) { + if (!resources || resources.length === 0) { + return; + } + + for (const resource of resources) { + this.registry.scheduleUpsert(resource, newIndexTimestamp, this); + } + } + + /** + * Schedule resource removals to be applied during registry flush. + * + * Unlike base HashTree, this method doesn't immediately modify the tree. + * Instead, it schedules operations with the registry for batch processing. + * Call registry.flush() to apply all pending operations atomically. + * + * @param {Array} resourcePaths - Array of resource paths to remove + * @returns {Promise} Returns undefined; results available via registry.flush() + */ + async removeResources(resourcePaths) { + if (!resourcePaths || resourcePaths.length === 0) { + return; + } + + for (const resourcePath of resourcePaths) { + this.registry.scheduleRemoval(resourcePath); + } + } + + /** + * Create a derived shared tree that shares subtrees with this tree. + * + * The derived tree shares the same registry and will participate in + * coordinated batch updates. Changes to shared nodes propagate to all trees. + * + * @param {Array} additionalResources + * Resources to add to the derived tree (in addition to shared resources from parent) + * @returns {SharedHashTree} New shared tree sharing subtrees and registry with this tree + */ + deriveTree(additionalResources = []) { + // Shallow copy root to allow adding new top-level directories + const derivedRoot = this._shallowCopyDirectory(this.root); + + // Create derived tree with shared root and same registry + const derived = new SharedHashTree(additionalResources, this.registry, { + _root: derivedRoot + }); + + // Register the derived tree with parent tree reference + if (this.registry) { + this.registry.register(derived, this); + } + + return derived; + } + + /** + * For a tree derived from a base tree, get the list of resource nodes + * that were added compared to the base tree. + * + * @param {HashTree} rootTree - The base tree to compare against + * @returns {Array} + * Array of added resource metadata + */ + getAddedResources(rootTree) { + const added = []; + + const traverse = (node, currentPath, implicitlyAdded = false) => { + if (implicitlyAdded) { + // We're in a subtree that's entirely new - add all resources + if (node.type === "resource") { + added.push({ + path: currentPath, + integrity: node.integrity, + size: node.size, + lastModified: node.lastModified, + inode: node.inode, + tags: node.tags + }); + } + } else { + const baseNode = rootTree._findNode(currentPath); + if (baseNode && baseNode === node) { + // Node exists in base tree and is the same object (structural sharing) + // Neither node nor children are added + return; + } else if (baseNode && node.type === "directory") { + // Directory exists in both trees but may have been shallow-copied + // Check children individually - only process children that differ + for (const [name, child] of node.children) { + const childPath = currentPath ? path.join(currentPath, name) : name; + const baseChild = baseNode.children.get(name); + + if (!baseChild || baseChild !== child) { + // Child doesn't exist in base or is different - determine if added + if (!baseChild) { + // Entirely new - all descendants are added + traverse(child, childPath, true); + } else { + // Child was modified/replaced - recurse normally + traverse(child, childPath, false); + } + } + // If baseChild === child, skip it (shared) + } + return; // Don't continue with normal traversal + } else if (!baseNode && node.type === "resource") { + // Resource doesn't exist in base tree - it's added + added.push({ + path: currentPath, + integrity: node.integrity, + size: node.size, + lastModified: node.lastModified, + inode: node.inode, + tags: node.tags + }); + return; + } else if (!baseNode && node.type === "directory") { + // Directory doesn't exist in base tree - all children are added + implicitlyAdded = true; + } + } + + if (node.type === "directory") { + for (const [name, child] of node.children) { + const childPath = currentPath ? path.join(currentPath, name) : name; + traverse(child, childPath, implicitlyAdded); + } + } + }; + traverse(this.root, "/"); + return added; + } + + /** + * Deserialize tree from JSON + * + * @param {object} data + * @param {TreeRegistry} registry Required registry for coordinated batch updates across multiple trees + * @param {object} [options] + * @returns {HashTree} + */ + static fromCache(data, registry, options = {}) { + if (data.version !== 1) { + throw new Error(`Unsupported version: ${data.version}`); + } + + const tree = new SharedHashTree(null, registry, options); + tree.root = TreeNode.fromJSON(data.root); + + return tree; + } +} diff --git a/packages/project/lib/build/cache/index/TreeNode.js b/packages/project/lib/build/cache/index/TreeNode.js new file mode 100644 index 00000000000..d85c8b9071c --- /dev/null +++ b/packages/project/lib/build/cache/index/TreeNode.js @@ -0,0 +1,123 @@ +import path from "node:path/posix"; + +/** + * Represents a node in the directory-based Merkle tree + */ +export default class TreeNode { + /** + * @param {string} name Resource name or directory name + * @param {"resource"|"directory"} type Node type + * @param {object} [options] + * @param {Buffer|null} [options.hash] Pre-computed hash + * @param {string} [options.integrity] Resource content hash + * @param {number} [options.lastModified] Last modified timestamp + * @param {number} [options.size] File size in bytes + * @param {number} [options.inode] File system inode number + * @param {Object|null} [options.tags] Resource tags (key-value pairs) + * @param {Map} [options.children] Child nodes (for directory nodes) + */ + constructor(name, type, options = {}) { + this.name = name; // resource name or directory name + this.type = type; // 'resource' | 'directory' + this.hash = options.hash || null; // Buffer + + // Resource node properties + this.integrity = options.integrity; // Resource content hash + this.lastModified = options.lastModified; // Last modified timestamp + this.size = options.size; // File size in bytes + this.inode = options.inode; // File system inode number + this.tags = options.tags || null; // Resource tags (key-value pairs) + + // Directory node properties + this.children = options.children || new Map(); // name -> TreeNode + } + + /** + * Get full path from root to this node + * + * @param {string} parentPath + * @returns {string} + */ + getPath(parentPath = "") { + return parentPath ? path.join(parentPath, this.name) : this.name; + } + + /** + * Serialize to JSON + * + * @returns {object} + */ + toJSON() { + const obj = { + name: this.name, + type: this.type, + hash: this.hash ? this.hash.toString("hex") : null + }; + + if (this.type === "resource") { + obj.integrity = this.integrity; + obj.lastModified = this.lastModified; + obj.size = this.size; + obj.inode = this.inode; + obj.tags = this.tags; + } else { + obj.children = {}; + for (const [name, child] of this.children) { + obj.children[name] = child.toJSON(); + } + } + + return obj; + } + + /** + * Deserialize from JSON + * + * @param {object} data + * @returns {TreeNode} + */ + static fromJSON(data) { + const options = { + hash: data.hash ? Buffer.from(data.hash, "hex") : null, + integrity: data.integrity, + lastModified: data.lastModified, + size: data.size, + inode: data.inode, + tags: data.tags || null + }; + + if (data.type === "directory" && data.children) { + options.children = new Map(); + for (const [name, childData] of Object.entries(data.children)) { + options.children.set(name, TreeNode.fromJSON(childData)); + } + } + + return new TreeNode(data.name, data.type, options); + } + + /** + * Create a deep copy of this node + * + * @returns {TreeNode} + */ + clone() { + const options = { + hash: this.hash ? Buffer.from(this.hash) : null, + integrity: this.integrity, + lastModified: this.lastModified, + size: this.size, + inode: this.inode, + tags: this.tags ? {...this.tags} : null + }; + + if (this.type === "directory") { + options.children = new Map(); + for (const [name, child] of this.children) { + options.children.set(name, child.clone()); + } + } + + return new TreeNode(this.name, this.type, options); + } +} diff --git a/packages/project/lib/build/cache/index/TreeRegistry.js b/packages/project/lib/build/cache/index/TreeRegistry.js new file mode 100644 index 00000000000..c0ea67ca3d6 --- /dev/null +++ b/packages/project/lib/build/cache/index/TreeRegistry.js @@ -0,0 +1,645 @@ +import path from "node:path/posix"; +import TreeNode from "./TreeNode.js"; +import {tagsEqual} from "./HashTree.js"; +import {matchResourceMetadataStrict} from "../utils.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:cache:index:TreeRegistry"); + +/** + * Registry for coordinating batch updates across multiple Merkle trees that share nodes by reference. + * + * When multiple trees (e.g., derived trees) share directory and resource nodes through structural sharing, + * direct mutations would be visible to all trees simultaneously. The TreeRegistry provides a transaction-like + * mechanism to batch and coordinate updates: + * + * 1. Changes are scheduled via scheduleUpsert() and scheduleRemoval() without immediately modifying trees + * 2. During flush(), all pending operations are applied atomically across all registered trees + * 3. Shared nodes are modified only once, with changes propagating to all trees that reference them + * 4. Directory hashes are recomputed efficiently in a single bottom-up pass + * + * This approach ensures consistency when multiple trees represent filtered views of the same underlying data. + * + * @property {Set} trees - All registered HashTree/SharedHashTree instances + * @property {Map} + * pendingUpserts - Resource path to resource and source tree mappings for scheduled upserts + * @property {Set} pendingRemovals - Resource paths scheduled for removal + * @property {Map>} derivedTrees + * Maps parent trees to their directly derived children + */ +export default class TreeRegistry { + trees = new Set(); + pendingUpserts = new Map(); + pendingRemovals = new Set(); + pendingTimestampUpdate; + derivedTrees = new Map(); // parent -> Set of derived trees + + /** + * Register a HashTree or SharedHashTree instance with this registry for coordinated updates. + * + * Once registered, the tree will participate in all batch operations triggered by flush(). + * Multiple trees can share the same underlying nodes through structural sharing. + * + * @param {import('./SharedHashTree.js').default} tree - HashTree or SharedHashTree instance to register + * @param {import('./SharedHashTree.js').default} [parentTree] - Parent tree if this is a derived tree + */ + register(tree, parentTree = null) { + this.trees.add(tree); + + if (parentTree) { + if (!this.derivedTrees.has(parentTree)) { + this.derivedTrees.set(parentTree, new Set()); + } + this.derivedTrees.get(parentTree).add(tree); + } + } + + /** + * Remove a HashTree or SharedHashTree instance from this registry. + * + * After unregistering, the tree will no longer participate in batch operations. + * Any pending operations scheduled before unregistration will still be applied during flush(). + * + * @param {import('./SharedHashTree.js').default} tree - HashTree or SharedHashTree instance to unregister + */ + unregister(tree) { + this.trees.delete(tree); + + // Remove from derivedTrees mappings + this.derivedTrees.delete(tree); + for (const [, derivedSet] of this.derivedTrees) { + derivedSet.delete(tree); + } + } + + /** + * Get all trees derived from a given tree (recursively). + * + * @param {import('./SharedHashTree.js').default} tree - The parent tree + * @returns {Set} Set of all derived trees (direct and transitive) + */ + _getDerivedTrees(tree) { + const result = new Set(); + const directDerived = this.derivedTrees.get(tree); + + if (directDerived) { + for (const derived of directDerived) { + result.add(derived); + // Recursively get trees derived from derived + for (const transitive of this._getDerivedTrees(derived)) { + result.add(transitive); + } + } + } + + return result; + } + + /** + * Check if targetTree is the same as or derived from sourceTree. + * + * @param {import('./SharedHashTree.js').default} sourceTree - The source/parent tree + * @param {import('./SharedHashTree.js').default} targetTree - The tree to check + * @returns {boolean} True if targetTree is sourceTree or derived from it + */ + _isTreeOrDerived(sourceTree, targetTree) { + if (sourceTree === targetTree) { + return true; + } + return this._getDerivedTrees(sourceTree).has(targetTree); + } + + /** + * Schedule a resource upsert (insert or update) to be applied during flush(). + * + * If a resource with the same path doesn't exist, it will be inserted (including creating + * any necessary parent directories). If it exists, its metadata will be updated if changed. + * Scheduling an upsert cancels any pending removal for the same resource path. + * + * When sourceTree is specified, new resources will only be inserted into that tree and + * any trees derived from it. Updates to existing resources will still propagate to all + * trees that share the resource node. + * + * @param {@ui5/fs/Resource} resource - Resource instance to upsert + * @param {number} [newIndexTimestamp] - Timestamp at which the provided resources have been indexed + * @param {import('./SharedHashTree.js').default} [sourceTree] - Tree that initiated this upsert + * (for controlling insert propagation) + */ + scheduleUpsert(resource, newIndexTimestamp, sourceTree = null) { + const resourcePath = resource.getOriginalPath(); + this.pendingUpserts.set(resourcePath, {resource, sourceTree}); + // Cancel any pending removal for this path + this.pendingRemovals.delete(resourcePath); + if (newIndexTimestamp) { + this.pendingTimestampUpdate = newIndexTimestamp; + } + } + + /** + * Schedule a resource removal to be applied during flush(). + * + * The resource will be removed from all registered trees that contain it. + * Scheduling a removal cancels any pending upsert for the same resource path. + * Removals are processed before upserts during flush() to handle replacement scenarios. + * + * @param {string} resourcePath - POSIX-style path to the resource (e.g., "src/main.js") + */ + scheduleRemoval(resourcePath) { + this.pendingRemovals.add(resourcePath); + // Cancel any pending upsert for this path + this.pendingUpserts.delete(resourcePath); + } + + /** + * Apply all pending upserts and removals atomically across all registered trees. + * + * This method processes scheduled operations in three phases: + * + * Phase 1: Process removals + * - Delete resource nodes from all trees that contain them + * - Mark affected ancestor directories for hash recomputation + * + * Phase 2: Process upserts (inserts and updates) + * - Group operations by parent directory for efficiency + * - For inserts: only create in source tree and its derived trees + * - For updates: apply to all trees that share the resource node + * - Skip updates for resources with unchanged metadata and tags + * - Detect tag-only changes and treat them as updates + * - Track modified nodes to avoid duplicate updates to shared nodes + * + * Phase 3: Recompute directory hashes + * - Sort affected directories by depth (deepest first) + * - Recompute hashes bottom-up to root + * - Each shared node is updated once, visible to all trees + * + * After successful completion, all pending operations are cleared. + * + * @returns {Promise<{added: string[], updated: string[], unchanged: string[], removed: string[], + * treeStats: Map}>} + * Object containing arrays of resource paths categorized by operation result, + * plus per-tree statistics showing which resource paths were added/updated/unchanged/removed in each tree + */ + async flush() { + if (this.pendingUpserts.size === 0 && this.pendingRemovals.size === 0) { + return { + added: [], + updated: [], + unchanged: [], + removed: [], + treeStats: new Map() + }; + } + + // Track added, updated, unchanged, and removed resources + const addedResources = []; + const updatedResources = []; + const unchangedResources = []; + const removedResources = []; + + const perfEnabled = log.isLevelEnabled("perf"); + const phase1Start = perfEnabled ? performance.now() : 0; + + // Track per-tree statistics + const treeStats = new Map(); + for (const tree of this.trees) { + treeStats.set(tree, {added: [], updated: [], unchanged: [], removed: []}); + } + + // Track which resource nodes we've already modified to handle shared nodes + const modifiedNodes = new Set(); + // Track which resource nodes we've already confirmed unchanged to skip redundant checks + const unchangedNodes = new Set(); + + // Track all affected trees and the paths that need recomputation + const affectedTrees = new Map(); // tree -> Set of directory paths needing recomputation + + // 1. Handle removals first + for (const resourcePath of this.pendingRemovals) { + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + const resourceName = parts[parts.length - 1]; + const parentPath = parts.slice(0, -1).join(path.sep); + + // Track which trees have this resource before deletion (for shared nodes) + const treesWithResource = []; + for (const tree of this.trees) { + const parentNode = tree._findNode(parentPath); + if (parentNode && parentNode.type === "directory" && parentNode.children.has(resourceName)) { + treesWithResource.push({tree, parentNode, pathNodes: this._getPathNodes(tree, parts)}); + } + } + + // Perform deletion once and track for all trees that had it + if (treesWithResource.length > 0) { + const {parentNode} = treesWithResource[0]; + parentNode.children.delete(resourceName); + + // Clean up empty parent directories in all affected trees + for (const {tree, pathNodes} of treesWithResource) { + // Clean up empty parent directories bottom-up + for (let i = parts.length - 1; i > 0; i--) { + const currentDirNode = pathNodes[i]; + if (currentDirNode && currentDirNode.children.size === 0) { + // Directory is empty, remove it from its parent + const parentDirNode = pathNodes[i - 1]; + if (parentDirNode) { + parentDirNode.children.delete(parts[i - 1]); + } + } else { + // Directory still has children, stop cleanup for this tree + break; + } + } + + if (!affectedTrees.has(tree)) { + affectedTrees.set(tree, new Set()); + } + + // Mark ancestors for recomputation (only up to where directories still exist) + for (let i = 0; i < parts.length; i++) { + const ancestorPath = parts.slice(0, i).join(path.sep); + if (tree._findNode(ancestorPath)) { + affectedTrees.get(tree).add(ancestorPath); + } + } + + // Track per-tree removal + treeStats.get(tree).removed.push(resourcePath); + } + + if (!removedResources.includes(resourcePath)) { + removedResources.push(resourcePath); + } + } + } + + // 2. Handle upserts - group by directory + const upsertsByDir = new Map(); // parentPath -> [{resourceName, resource, fullPath, sourceTree}] + + const phase2Start = perfEnabled ? performance.now() : 0; + let matchMetadataStrictCalls = 0; + let matchMetadataUnchanged = 0; + let modifiedNodesSkips = 0; + + for (const [resourcePath, {resource, sourceTree}] of this.pendingUpserts) { + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + const resourceName = parts[parts.length - 1]; + const parentPath = parts.slice(0, -1).join(path.sep); + + if (!upsertsByDir.has(parentPath)) { + upsertsByDir.set(parentPath, []); + } + upsertsByDir.get(parentPath).push({resourceName, resource, fullPath: resourcePath, sourceTree}); + } + + // Pre-resolve I/O for resources that don't exist in any tree (genuinely new). + // For existing resources, matchResourceMetadataStrict's lastModified short-circuit + // avoids I/O in the common case, so those are handled serially in the loop below. + const resolvedNewMetadata = new Map(); + const newResourcePaths = []; + for (const [resourcePath, {resource}] of this.pendingUpserts) { + let existsInAnyTree = false; + const parts = resourcePath.split(path.sep).filter((p) => p.length > 0); + const resourceName = parts[parts.length - 1]; + const parentPath = parts.slice(0, -1).join(path.sep); + for (const tree of this.trees) { + const parentNode = tree._findNode(parentPath); + if (parentNode?.children?.get(resourceName)) { + existsInAnyTree = true; + break; + } + } + if (!existsInAnyTree) { + newResourcePaths.push({resourcePath, resource}); + } + } + if (newResourcePaths.length > 0) { + await Promise.all(newResourcePaths.map(async ({resourcePath, resource}) => { + const [integrity, size] = await Promise.all([ + resource.getIntegrity(), + resource.getSize() + ]); + resolvedNewMetadata.set(resourcePath, { + integrity, + size, + lastModified: resource.getLastModified(), + inode: resource.getInode(), + tags: resource.getTags?.() ?? resource.tags ?? null + }); + })); + } + + // Apply upserts + for (const [parentPath, upserts] of upsertsByDir) { + for (const tree of this.trees) { + // Check if parent directory exists in this tree + let parentNode = tree._findNode(parentPath); + + let dirModified = false; + for (const upsert of upserts) { + let resourceNode = parentNode?.children?.get(upsert.resourceName); + + if (!resourceNode) { + // INSERT: Check derivation rules + if (upsert.sourceTree !== null) { + // Source tree specified - only insert into source tree and its derived trees + if (!this._isTreeOrDerived(upsert.sourceTree, tree)) { + // This tree is not the source tree or derived from it - skip insert + continue; + } + } + // If sourceTree is null, insert into all trees (backward compatibility) + + // Ensure parent directory exists (only for trees we're inserting into) + if (!parentNode) { + parentNode = this._ensureDirectoryPath( + tree, parentPath.split(path.sep).filter((p) => p.length > 0)); + } + + if (parentNode.type !== "directory") { + continue; + } + + // Use pre-resolved metadata if available, otherwise resolve now + const resolved = resolvedNewMetadata.get(upsert.fullPath); + + // Create new resource node + resourceNode = new TreeNode(upsert.resourceName, "resource", { + integrity: resolved?.integrity ?? await upsert.resource.getIntegrity(), + lastModified: resolved?.lastModified ?? upsert.resource.getLastModified(), + size: resolved?.size ?? await upsert.resource.getSize(), + inode: resolved?.inode ?? upsert.resource.getInode(), + tags: resolved?.tags ?? upsert.resource.getTags?.() ?? upsert.resource.tags ?? null + }); + parentNode.children.set(upsert.resourceName, resourceNode); + modifiedNodes.add(resourceNode); + dirModified = true; + + // Track per-tree addition + treeStats.get(tree).added.push(upsert.fullPath); + + if (!addedResources.includes(upsert.fullPath)) { + addedResources.push(upsert.fullPath); + } + } else if (resourceNode.type === "resource") { + // UPDATE: Check if modified + if (modifiedNodes.has(resourceNode)) { + // Node was already modified by another tree (shared node) + // Still count it as an update for this tree since the change affects it + modifiedNodesSkips++; + treeStats.get(tree).updated.push(upsert.fullPath); + dirModified = true; + } else if (unchangedNodes.has(resourceNode)) { + // Already confirmed unchanged in another tree — just check tags + const currentTags = + upsert.resource.getTags?.() ?? upsert.resource.tags ?? null; + if (!tagsEqual(resourceNode.tags, currentTags)) { + // Tags changed — treat as update + resourceNode.tags = currentTags; + modifiedNodes.add(resourceNode); + unchangedNodes.delete(resourceNode); + dirModified = true; + + // Track per-tree update + treeStats.get(tree).updated.push(upsert.fullPath); + + if (!updatedResources.includes(upsert.fullPath)) { + updatedResources.push(upsert.fullPath); + } + } else { + // Track per-tree unchanged + treeStats.get(tree).unchanged.push(upsert.fullPath); + + if (!unchangedResources.includes(upsert.fullPath)) { + unchangedResources.push(upsert.fullPath); + } + } + } else { + // First time seeing this node — do full comparison + matchMetadataStrictCalls++; + const currentMetadata = { + integrity: resourceNode.integrity, + lastModified: resourceNode.lastModified, + size: resourceNode.size, + inode: resourceNode.inode + }; + + const isUnchanged = await matchResourceMetadataStrict( + upsert.resource, + currentMetadata, + tree.getIndexTimestamp() + ); + + if (!isUnchanged) { + resourceNode.integrity = await upsert.resource.getIntegrity(); + resourceNode.lastModified = upsert.resource.getLastModified(); + resourceNode.size = await upsert.resource.getSize(); + resourceNode.inode = upsert.resource.getInode(); + resourceNode.tags = upsert.resource.getTags?.() ?? + upsert.resource.tags ?? resourceNode.tags; + modifiedNodes.add(resourceNode); + dirModified = true; + + // Track per-tree update + treeStats.get(tree).updated.push(upsert.fullPath); + + if (!updatedResources.includes(upsert.fullPath)) { + updatedResources.push(upsert.fullPath); + } + } else { + matchMetadataUnchanged++; + unchangedNodes.add(resourceNode); + const currentTags = + upsert.resource.getTags?.() ?? upsert.resource.tags ?? null; + if (!tagsEqual(resourceNode.tags, currentTags)) { + // Tags changed — treat as update + resourceNode.tags = currentTags; + modifiedNodes.add(resourceNode); + unchangedNodes.delete(resourceNode); + dirModified = true; + + // Track per-tree update + treeStats.get(tree).updated.push(upsert.fullPath); + + if (!updatedResources.includes(upsert.fullPath)) { + updatedResources.push(upsert.fullPath); + } + } else { + // Track per-tree unchanged + treeStats.get(tree).unchanged.push(upsert.fullPath); + + if (!unchangedResources.includes(upsert.fullPath)) { + unchangedResources.push(upsert.fullPath); + } + } + } + } + } + } + + if (dirModified && parentNode) { + // Compute hashes for modified/new resources + for (const upsert of upserts) { + const resourceNode = parentNode.children.get(upsert.resourceName); + if (resourceNode && resourceNode.type === "resource" && modifiedNodes.has(resourceNode)) { + tree._computeHash(resourceNode); + } + } + + if (!affectedTrees.has(tree)) { + affectedTrees.set(tree, new Set()); + } + + tree._computeHash(parentNode); + this._markAncestorsAffected( + tree, parentPath.split(path.sep).filter((p) => p.length > 0), affectedTrees); + } + } + } + + // Recompute ancestor hashes for all affected trees + const phase3Start = perfEnabled ? performance.now() : 0; + for (const [tree, affectedPaths] of affectedTrees) { + // Sort paths by depth (deepest first) to recompute bottom-up + const sortedPaths = Array.from(affectedPaths).sort((a, b) => { + const depthA = a ? a.split(path.sep).length : 0; + const depthB = b ? b.split(path.sep).length : 0; + if (depthA !== depthB) return depthB - depthA; // deeper first + return a.localeCompare(b); + }); + + for (const dirPath of sortedPaths) { + const node = tree._findNode(dirPath); + if (node && node.type === "directory") { + tree._computeHash(node); + } + } + if (this.pendingTimestampUpdate) { + tree.setIndexTimestamp(this.pendingTimestampUpdate); + } + } + + // Clear all pending operations + this.pendingUpserts.clear(); + this.pendingRemovals.clear(); + this.pendingTimestampUpdate = null; + + if (perfEnabled) { + const now = performance.now(); + log.perf( + `TreeRegistry.flush completed: ` + + `phase1(removals)=${(phase2Start - phase1Start).toFixed(2)} ms, ` + + `phase2(upserts)=${(phase3Start - phase2Start).toFixed(2)} ms, ` + + `phase3(rehash)=${(now - phase3Start).toFixed(2)} ms | ` + + `matchMetadataStrictCalls=${matchMetadataStrictCalls}, ` + + `matchMetadataUnchanged=${matchMetadataUnchanged}, ` + + `modifiedNodesSkips=${modifiedNodesSkips}`); + } + + return { + added: addedResources, + updated: updatedResources, + unchanged: unchangedResources, + removed: removedResources, + treeStats + }; + } + + /** + * Get all nodes along a path from root to the target. + * + * Returns an array of TreeNode objects representing the full path, + * starting with root at index 0 and ending with the target node. + * + * @param {import('./SharedHashTree.js').default} tree - Tree to traverse + * @param {string[]} pathParts - Path components to follow + * @returns {Array} Array of TreeNode objects along the path + */ + _getPathNodes(tree, pathParts) { + const nodes = [tree.root]; + let current = tree.root; + + for (let i = 0; i < pathParts.length - 1; i++) { + if (!current.children.has(pathParts[i])) { + break; + } + current = current.children.get(pathParts[i]); + nodes.push(current); + } + + return nodes; + } + + /** + * Mark all ancestor directories in a tree as requiring hash recomputation. + * + * When a resource or directory is modified, all ancestor directories up to the root + * need their hashes recomputed to reflect the change. This method tracks those paths + * in the affectedTrees map for later batch processing. + * + * @param {import('./SharedHashTree.js').default} tree - Tree containing the affected path + * @param {string[]} pathParts - Path components of the modified resource/directory + * @param {Map>} affectedTrees + * Map tracking affected paths per tree + */ + _markAncestorsAffected(tree, pathParts, affectedTrees) { + if (!affectedTrees.has(tree)) { + affectedTrees.set(tree, new Set()); + } + + for (let i = 0; i <= pathParts.length; i++) { + affectedTrees.get(tree).add(pathParts.slice(0, i).join(path.sep)); + } + } + + /** + * Ensure a directory path exists in a tree, creating missing directories as needed. + * + * This method walks down the path from root, creating any missing directory nodes. + * It's used during upsert operations to automatically create parent directories + * when inserting resources into paths that don't yet exist. + * + * @param {import('./SharedHashTree.js').default} tree - Tree to create directory path in + * @param {string[]} pathParts - Path components of the directory to ensure exists + * @returns {TreeNode} The directory node at the end of the path + */ + _ensureDirectoryPath(tree, pathParts) { + let current = tree.root; + + for (const part of pathParts) { + if (!current.children.has(part)) { + const dirNode = new TreeNode(part, "directory"); + current.children.set(part, dirNode); + } + current = current.children.get(part); + } + + return current; + } + + /** + * Get the number of HashTree instances currently registered with this registry. + * + * @returns {number} Count of registered trees + */ + getTreeCount() { + return this.trees.size; + } + + /** + * Get the total number of pending operations (upserts + removals) waiting to be applied. + * + * @returns {number} Count of pending upserts and removals combined + */ + getPendingUpdateCount() { + return this.pendingUpserts.size + this.pendingRemovals.size; + } + + /** + * Check if there are any pending operations waiting to be applied. + * + * @returns {boolean} True if there are pending upserts or removals, false otherwise + */ + hasPendingUpdates() { + return this.pendingUpserts.size > 0 || this.pendingRemovals.size > 0; + } +} diff --git a/packages/project/lib/build/cache/utils.js b/packages/project/lib/build/cache/utils.js new file mode 100644 index 00000000000..c13835ea43e --- /dev/null +++ b/packages/project/lib/build/cache/utils.js @@ -0,0 +1,182 @@ +/** + * @typedef {object} ResourceMetadata + * @property {string} integrity Content integrity of the resource + * @property {number} lastModified Last modified timestamp (mtimeMs) + * @property {number} inode Inode number of the resource + * @property {number} size Size of the resource in bytes + */ + +const PERF_TRACKING = !!process.env.UI5_CACHE_PERF; +const perfCounters = { + calls: 0, + shortCircuitTrue: 0, + sizeMismatch: 0, + integrityFallback: 0, +}; +export {perfCounters as matchResourceMetadataStrictCounters}; + +/** + * Compares a resource instance with cached resource metadata + * + * Optimized for quickly rejecting changed files. Performs a series of checks + * starting with the cheapest (timestamp) to more expensive (integrity hash). + * + * @public + * @param {@ui5/fs/Resource} resource Resource instance to compare + * @param {ResourceMetadata} resourceMetadata Resource metadata to compare against + * @param {number} [indexTimestamp] Timestamp of the metadata creation + * @returns {Promise} True if resource is found to match the metadata + * @throws {Error} If resource or metadata is undefined + */ +export async function matchResourceMetadata(resource, resourceMetadata, indexTimestamp) { + if (!resource || !resourceMetadata) { + throw new Error("Cannot compare undefined resources or metadata"); + } + + const currentLastModified = resource.getLastModified(); + if (indexTimestamp && currentLastModified > indexTimestamp) { + // Resource modified after index was created, no need for further checks + return false; + } + if (currentLastModified !== resourceMetadata.lastModified) { + return false; + } + if (await resource.getSize() !== resourceMetadata.size) { + return false; + } + const incomingInode = resource.getInode(); + if (resourceMetadata.inode !== undefined && incomingInode !== undefined && + incomingInode !== resourceMetadata.inode) { + return false; + } + + if (currentLastModified === indexTimestamp) { + // If the source modification time is equal to index creation time, + // it's possible for a race condition to have occurred where the file was modified + // during index creation without changing its size. + // In this case, we need to perform an integrity check to determine if the file has changed. + if (await resource.getIntegrity() !== resourceMetadata.integrity) { + return false; + } + } + return true; +} + +/** + * Determines if a resource has changed compared to cached metadata + * + * Optimized for quickly accepting unchanged files. Resources are assumed to be + * usually unchanged (same lastModified timestamp). Performs checks from cheapest + * to most expensive, falling back to integrity comparison when necessary. + * + * @public + * @param {@ui5/fs/Resource} resource Resource instance to compare + * @param {ResourceMetadata} cachedMetadata Cached metadata from the tree + * @param {number} [indexTimestamp] Timestamp when the tree state was created + * @returns {Promise} True if resource content is unchanged + * @throws {Error} If resource or metadata is undefined + */ +export async function matchResourceMetadataStrict(resource, cachedMetadata, indexTimestamp) { + if (!resource || !cachedMetadata) { + throw new Error("Cannot compare undefined resources or metadata"); + } + if (PERF_TRACKING) perfCounters.calls++; + + // Check 1: Inode mismatch would indicate file replacement (comparison only if inodes are provided) + // const currentInode = resource.getInode(); + // if (cachedMetadata.inode !== undefined && currentInode !== undefined && + // currentInode !== cachedMetadata.inode) { + // return false; + // } + + // Check 2: Modification time unchanged would suggest no update needed + const currentLastModified = resource.getLastModified(); + if (currentLastModified === cachedMetadata.lastModified) { + if (indexTimestamp && currentLastModified !== indexTimestamp) { + // File has not been modified since last indexing. No update needed + if (PERF_TRACKING) perfCounters.shortCircuitTrue++; + return true; + } // else: Edge case. File modified exactly at index time + // Race condition possible - content may have changed during indexing + // Fall through to integrity check + } + + // Check 3: Size mismatch indicates definite content change + const currentSize = await resource.getSize(); + if (currentSize !== cachedMetadata.size) { + if (PERF_TRACKING) perfCounters.sizeMismatch++; + return false; + } + + // Check 4: Compare integrity (expensive) + // lastModified has changed, but the content might be the same. E.g. in case of a metadata-only update + if (PERF_TRACKING) perfCounters.integrityFallback++; + const currentIntegrity = await resource.getIntegrity(); + return currentIntegrity === cachedMetadata.integrity; +} + + +/** + * Creates an index of resource metadata from an array of resources + * + * Processes all resources in parallel, extracting their metadata including + * path, integrity, lastModified timestamp, and size. Optionally includes inode information. + * + * @public + * @param {Array<@ui5/fs/Resource>} resources Array of resources to index + * @param {boolean} [includeInode=false] Whether to include inode information in the metadata + * @returns {Promise>} + * Array of resource metadata objects + */ +export async function createResourceIndex(resources, includeInode = false) { + return await Promise.all(resources.map(async (resource) => { + const resourceMetadata = { + path: resource.getOriginalPath(), + integrity: await resource.getIntegrity(), + lastModified: resource.getLastModified(), + size: await resource.getSize(), + tags: resource.getTags(), + }; + if (includeInode) { + resourceMetadata.inode = resource.getInode(); + } + return resourceMetadata; + })); +} + +/** + * Returns the first truthy value from an array of promises + * + * This function evaluates all promises in parallel and returns immediately + * when the first truthy value is found. If all promises resolve to falsy + * values, null is returned. + * + * @param {Promise[]} promises Array of promises to evaluate + * @returns {Promise<*>} The first truthy resolved value or null if all are falsy + */ +export async function firstTruthy(promises) { + return new Promise((resolve, reject) => { + let completed = 0; + const total = promises.length; + + if (total === 0) { + resolve(null); + return; + } + + promises.forEach((promise) => { + Promise.resolve(promise) + .then((value) => { + if (value) { + resolve(value); + } else { + completed++; + if (completed === total) { + resolve(null); + } + } + }) + .catch(reject); + }); + }); +} diff --git a/packages/project/lib/build/definitions/application.js b/packages/project/lib/build/definitions/application.js index c546ee9d6bf..9b502502836 100644 --- a/packages/project/lib/build/definitions/application.js +++ b/packages/project/lib/build/definitions/application.js @@ -20,6 +20,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceCopyright", { + supportsDifferentialBuilds: true, options: { copyright: project.getCopyright(), pattern: "/**/*.{js,json}" @@ -27,6 +28,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { + supportsDifferentialBuilds: true, options: { version: project.getVersion(), pattern: "/**/*.{js,json}" @@ -42,6 +44,7 @@ export default function({project, taskUtil, getTask}) { } } tasks.set("minify", { + supportsDifferentialBuilds: true, options: { pattern: minificationPattern } diff --git a/packages/project/lib/build/definitions/component.js b/packages/project/lib/build/definitions/component.js index 48684b6df03..ac64531ee98 100644 --- a/packages/project/lib/build/definitions/component.js +++ b/packages/project/lib/build/definitions/component.js @@ -20,6 +20,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceCopyright", { + supportsDifferentialBuilds: true, options: { copyright: project.getCopyright(), pattern: "/**/*.{js,json}" @@ -27,6 +28,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { + supportsDifferentialBuilds: true, options: { version: project.getVersion(), pattern: "/**/*.{js,json}" @@ -41,6 +43,7 @@ export default function({project, taskUtil, getTask}) { } tasks.set("minify", { + supportsDifferentialBuilds: true, options: { pattern: minificationPattern } diff --git a/packages/project/lib/build/definitions/library.js b/packages/project/lib/build/definitions/library.js index 9b92177d1cd..ab7a0cca58e 100644 --- a/packages/project/lib/build/definitions/library.js +++ b/packages/project/lib/build/definitions/library.js @@ -20,6 +20,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceCopyright", { + supportsDifferentialBuilds: true, options: { copyright: project.getCopyright(), pattern: "/**/*.{js,library,css,less,theme,html}" @@ -27,6 +28,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { + supportsDifferentialBuilds: true, options: { version: project.getVersion(), pattern: "/**/*.{js,json,library,css,less,theme,html}" @@ -34,6 +36,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceBuildtime", { + supportsDifferentialBuilds: true, options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" } @@ -82,6 +85,7 @@ export default function({project, taskUtil, getTask}) { } tasks.set("minify", { + supportsDifferentialBuilds: true, options: { pattern: minificationPattern } diff --git a/packages/project/lib/build/definitions/themeLibrary.js b/packages/project/lib/build/definitions/themeLibrary.js index 2acf0392768..00ef7424290 100644 --- a/packages/project/lib/build/definitions/themeLibrary.js +++ b/packages/project/lib/build/definitions/themeLibrary.js @@ -11,6 +11,7 @@ export default function({project, taskUtil, getTask}) { const tasks = new Map(); tasks.set("replaceCopyright", { + supportsDifferentialBuilds: true, options: { copyright: project.getCopyright(), pattern: "/resources/**/*.{less,theme}" @@ -18,6 +19,7 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { + supportsDifferentialBuilds: true, options: { version: project.getVersion(), pattern: "/resources/**/*.{less,theme}" diff --git a/packages/project/lib/build/helpers/BuildContext.js b/packages/project/lib/build/helpers/BuildContext.js index 8d8d1e1a329..c14308fb103 100644 --- a/packages/project/lib/build/helpers/BuildContext.js +++ b/packages/project/lib/build/helpers/BuildContext.js @@ -1,5 +1,10 @@ import ProjectBuildContext from "./ProjectBuildContext.js"; import OutputStyleEnum from "./ProjectBuilderOutputStyle.js"; +import CacheManager from "../cache/CacheManager.js"; +import {getBaseSignature} from "./getBuildSignature.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:helpers:BuildContext"); +import Cache from "../cache/Cache.js"; /** * Context of a build process @@ -8,6 +13,8 @@ import OutputStyleEnum from "./ProjectBuilderOutputStyle.js"; * @memberof @ui5/project/build/helpers */ class BuildContext { + #cacheManager; + constructor(graph, taskRepository, { // buildConfig selfContained = false, cssVariables = false, @@ -15,6 +22,7 @@ class BuildContext { createBuildManifest = false, outputStyle = OutputStyleEnum.Default, includedTasks = [], excludedTasks = [], + cache = Cache.Default, } = {}) { if (!graph) { throw new Error(`Missing parameter 'graph'`); @@ -67,14 +75,18 @@ class BuildContext { outputStyle, includedTasks, excludedTasks, + cache }; + // eslint-disable-next-line no-unused-vars + const {cache: _ignoreMe, ...signatureConfig} = this._buildConfig; // Clones buildConfig omitting the cache mode + this._buildSignatureBase = getBaseSignature(signatureConfig); this._taskRepository = taskRepository; this._options = { cssVariables: cssVariables }; - this._projectBuildContexts = []; + this._projectBuildContexts = new Map(); } getRootProject() { @@ -97,20 +109,113 @@ class BuildContext { return this._graph; } - createProjectContext({project}) { - const projectBuildContext = new ProjectBuildContext({ - buildContext: this, - project - }); - this._projectBuildContexts.push(projectBuildContext); + async getProjectContext(projectName) { + if (this._projectBuildContexts.has(projectName)) { + return this._projectBuildContexts.get(projectName); + } + const project = this._graph.getProject(projectName); + const projectBuildContext = await ProjectBuildContext.create( + this, project, await this.getCacheManager(), this._buildSignatureBase); + this._projectBuildContexts.set(projectName, projectBuildContext); return projectBuildContext; } + async getRequiredProjectContexts(requestedProjects) { + const totalStart = performance.now(); + const projectBuildContexts = new Map(); + const requiredProjects = new Set(requestedProjects); + + // Phase 1: Discover all required projects (sequential — each project's + // dependencies may expand the set). This is fast because getProjectContext + // no longer triggers source index I/O. + for (const projectName of requiredProjects) { + const projectBuildContext = await this.getProjectContext(projectName); + + projectBuildContexts.set(projectName, projectBuildContext); + + // Collect all direct dependencies of the project that are required to build the project + const requiredDependencies = await projectBuildContext.getRequiredDependencies(); + + for (const depName of requiredDependencies) { + // Add dependency to list of required projects + requiredProjects.add(depName); + } + } + + // Phase 2: Initialize all source indices in parallel + const initStart = performance.now(); + await Promise.all( + Array.from(projectBuildContexts.values()).map((ctx) => ctx.initSourceIndex()) + ); + if (log.isLevelEnabled("perf")) { + log.perf( + `Parallel source index initialization completed in ` + + `${(performance.now() - initStart).toFixed(2)} ms ` + + `for ${projectBuildContexts.size} projects`); + } + + if (log.isLevelEnabled("perf")) { + log.perf( + `getRequiredProjectContexts completed in ${(performance.now() - totalStart).toFixed(2)} ms ` + + `for ${projectBuildContexts.size} projects`); + } + return projectBuildContexts; + } + + async getCacheManager() { + if (this.#cacheManager) { + return this.#cacheManager; + } + this.#cacheManager = await CacheManager.create(this._graph.getRoot().getRootPath()); + return this.#cacheManager; + } + + getBuildContext(projectName) { + return this._projectBuildContexts.get(projectName); + } + async executeCleanupTasks(force = false) { - await Promise.all(this._projectBuildContexts.map((ctx) => { + await Promise.all(Array.from(this._projectBuildContexts.values()).map((ctx) => { return ctx.executeCleanupTasks(force); })); } + + /** + * + * @param {Map>} resourceChanges Mapping project name to changed resource paths + * @returns {Set} Names of projects potentially affected by the resource changes + */ + propagateResourceChanges(resourceChanges) { + const affectedProjectNames = new Set(); + const dependencyChanges = new Map(); + for (const [projectName, changedResourcePaths] of resourceChanges) { + affectedProjectNames.add(projectName); + // Propagate changes to dependents of the project + for (const {project: dep} of this._graph.traverseDependents(projectName)) { + const depChanges = dependencyChanges.get(dep.getName()); + if (!depChanges) { + dependencyChanges.set(dep.getName(), new Set(changedResourcePaths)); + } else { + for (const res of changedResourcePaths) { + depChanges.add(res); + } + } + } + const projectBuildContext = this.getBuildContext(projectName); + if (projectBuildContext) { + projectBuildContext.projectSourcesChanged(Array.from(changedResourcePaths)); + } + } + + for (const [projectName, changedResourcePaths] of dependencyChanges) { + affectedProjectNames.add(projectName); + const projectBuildContext = this.getBuildContext(projectName); + if (projectBuildContext) { + projectBuildContext.dependencyResourcesChanged(Array.from(changedResourcePaths)); + } + } + return affectedProjectNames; + } } export default BuildContext; diff --git a/packages/project/lib/build/helpers/ProjectBuildContext.js b/packages/project/lib/build/helpers/ProjectBuildContext.js index 10eb2a67a83..bed574f9e02 100644 --- a/packages/project/lib/build/helpers/ProjectBuildContext.js +++ b/packages/project/lib/build/helpers/ProjectBuildContext.js @@ -1,17 +1,26 @@ -import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; import ProjectBuildLogger from "@ui5/logger/internal/loggers/ProjectBuild"; import TaskUtil from "./TaskUtil.js"; import TaskRunner from "../TaskRunner.js"; +import {getProjectSignature} from "./getBuildSignature.js"; +import ProjectBuildCache from "../cache/ProjectBuildCache.js"; /** * Build context of a single project. Always part of an overall * [Build Context]{@link @ui5/project/build/helpers/BuildContext} - * - * @private + * @memberof @ui5/project/build/helpers */ class ProjectBuildContext { - constructor({buildContext, project}) { + /** + * Creates a new ProjectBuildContext instance + * + * @param {@ui5/project/build/helpers/BuildContext} buildContext Overall build context + * @param {@ui5/project/specifications/Project} project Project instance to build + * @param {string} buildSignature Signature of the build configuration + * @param {@ui5/project/build/cache/ProjectBuildCache} buildCache Build cache instance + * @throws {Error} If 'buildContext' or 'project' is missing + */ + constructor(buildContext, project, buildSignature, buildCache) { if (!buildContext) { throw new Error(`Missing parameter 'buildContext'`); } @@ -25,40 +34,104 @@ class ProjectBuildContext { projectName: project.getName(), projectType: project.getType() }); + this._buildSignature = buildSignature; + this._buildCache = buildCache; this._queues = { cleanup: [] }; + } - this._resourceTagCollection = new ResourceTagCollection({ - allowedTags: ["ui5:OmitFromBuildResult", "ui5:IsBundle"], - allowedNamespaces: ["build"] - }); + /** + * Factory method to create and initialize a ProjectBuildContext instance + * + * This is the recommended way to create a ProjectBuildContext as it ensures + * proper initialization of the build signature and cache. + * + * @param {@ui5/project/build/helpers/BuildContext} buildContext Overall build context + * @param {@ui5/project/specifications/Project} project Project instance to build + * @param {object} cacheManager Cache manager instance + * @param {string} baseSignature Base signature for the build + * @returns {Promise<@ui5/project/build/helpers/ProjectBuildContext>} Initialized context instance + */ + static async create(buildContext, project, cacheManager, baseSignature) { + const buildSignature = getProjectSignature( + baseSignature, project, buildContext.getGraph(), buildContext.getTaskRepository()); + const cacheMode = buildContext.getBuildConfig().cache; + const buildCache = await ProjectBuildCache.create( + project, buildSignature, cacheManager, cacheMode); + return new ProjectBuildContext( + buildContext, + project, + buildSignature, + buildCache + ); + } + + /** + * Initializes the source index for this project's build cache + * + * Must be called after create() and before any cache operations. + * Separated from create() to allow parallel initialization of multiple projects. + * + * @returns {Promise} + */ + async initSourceIndex() { + await this._buildCache.initSourceIndex(); } + /** + * Checks whether this context is for the root project + * + * @returns {boolean} True if this is the root project context + */ isRootProject() { return this._project === this._buildContext.getRootProject(); } + /** + * Retrieves a build configuration option + * + * @param {string} key Option key to retrieve + * @returns {*} Option value + */ getOption(key) { return this._buildContext.getOption(key); } + /** + * Registers a cleanup task to be executed after the build + * + * Cleanup tasks are called after all regular tasks have completed, + * allowing resources to be freed or temporary data to be cleaned up. + * + * @param {Function} callback Cleanup callback function that accepts a force parameter + */ registerCleanupTask(callback) { this._queues.cleanup.push(callback); } + /** + * Executes all registered cleanup tasks + * + * Calls all cleanup callbacks in parallel and clears the cleanup queue. + * + * @param {boolean} force Whether to force cleanup even if conditions aren't met + * @returns {Promise} + */ async executeCleanupTasks(force) { await Promise.all(this._queues.cleanup.map((callback) => { return callback(force); })); + this._queues.cleanup = []; } /** - * Retrieve a single project from the dependency graph + * Retrieves a single project from the dependency graph * - * @param {string} [projectName] Name of the project to retrieve. Defaults to the project currently being built + * @param {string} [projectName] Name of the project to retrieve. + * Defaults to the project currently being built * @returns {@ui5/project/specifications/Project|undefined} - * project instance or undefined if the project is unknown to the graph + * Project instance or undefined if the project is unknown to the graph */ getProject(projectName) { if (projectName) { @@ -68,9 +141,10 @@ class ProjectBuildContext { } /** - * Retrieve a list of direct dependencies of a given project from the dependency graph + * Retrieves a list of direct dependencies of a given project from the dependency graph * - * @param {string} [projectName] Name of the project to retrieve. Defaults to the project currently being built + * @param {string} [projectName] Name of the project to retrieve. + * Defaults to the project currently being built * @returns {string[]} Names of all direct dependencies * @throws {Error} If the requested project is unknown to the graph */ @@ -78,24 +152,51 @@ class ProjectBuildContext { return this._buildContext.getGraph().getDependencies(projectName || this._project.getName()); } + /** + * Gets the list of required dependencies for the current project + * + * Determines which dependencies are actually needed based on the tasks that will be executed. + * Results are cached after the first call. + * + * @returns {Promise} Array of required dependency names + */ + async getRequiredDependencies() { + if (this._requiredDependencies) { + return this._requiredDependencies; + } + const taskRunner = this.getTaskRunner(); + this._requiredDependencies = await taskRunner.getRequiredDependencies(); + return this._requiredDependencies; + } + + /** + * Gets the appropriate resource tag collection for a resource and tag + * + * Determines which tag collection (project-specific or build-level) should be used + * for the given resource and tag combination. Associates the resource with the current + * project if not already associated. + * + * @param {@ui5/fs/Resource} resource Resource to get tag collection for + * @param {string} tag Tag to check acceptance for + * @returns {@ui5/fs/internal/ResourceTagCollection} Appropriate tag collection + * @throws {Error} If no collection accepts the given tag + */ getResourceTagCollection(resource, tag) { if (!resource.hasProject()) { this._log.silly(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`); resource.setProject(this._project); - // throw new Error( - // `Unable to get tag collection for resource ${resource.getPath()}: ` + - // `Resource must be associated to a project`); - } - const projectCollection = resource.getProject().getResourceTagCollection(); - if (projectCollection.acceptsTag(tag)) { - return projectCollection; } - if (this._resourceTagCollection.acceptsTag(tag)) { - return this._resourceTagCollection; - } - throw new Error(`Could not find collection for resource ${resource.getPath()} and tag ${tag}`); + return resource.getProject().getResourceTagCollection(resource, tag); } + /** + * Gets the task utility instance for this build context + * + * Creates a TaskUtil instance on first access and caches it for subsequent calls. + * The TaskUtil provides helper functions for tasks during execution. + * + * @returns {@ui5/project/build/helpers/TaskUtil} Task utility instance + */ getTaskUtil() { if (!this._taskUtil) { this._taskUtil = new TaskUtil({ @@ -106,11 +207,20 @@ class ProjectBuildContext { return this._taskUtil; } + /** + * Gets the task runner instance for this build context + * + * Creates a TaskRunner instance on first access and caches it for subsequent calls. + * The TaskRunner is responsible for executing all build tasks for the project. + * + * @returns {@ui5/project/build/TaskRunner} Task runner instance + */ getTaskRunner() { if (!this._taskRunner) { this._taskRunner = new TaskRunner({ project: this._project, log: this._log, + buildCache: this._buildCache, taskUtil: this.getTaskUtil(), graph: this._buildContext.getGraph(), taskRepository: this._buildContext.getTaskRepository(), @@ -121,28 +231,181 @@ class ProjectBuildContext { } /** - * Determine whether the project has to be built or is already built - * (typically indicated by the presence of a build manifest) + * Early check whether a project build is possibly required. + * + * In some cases, the cache state cannot be determined until all dependencies have been processed and + * the cache has been updated with that information. This happens during prepareProjectBuildAndValidateCache(). + * + * This method allows for an early check whether a project build can be skipped. + * + * @returns {boolean} True if a build might required, false otherwise + */ + possiblyRequiresBuild() { + if (this.#getBuildManifest()) { + // Build manifest present -> No build required + return false; + } + // Without build manifest, check cache state + return !this.getBuildCache().isFresh(); + } + + /** + * Prepares the project build by updating and validating the build cache + * + * Creates a dependency reader and validates the cache state against current resources. + * Must be called before buildProject(). + * + * @returns {Promise} + * True if a valid cache was found and is being used. False otherwise (indicating a build is required). + */ + async prepareProjectBuildAndValidateCache() { + const readerStart = performance.now(); + const depReader = await this.getTaskRunner().getDependenciesReader( + await this.getTaskRunner().getRequiredDependencies(), + true, // Force creation of new reader since project readers might have changed during their (re-)build + ); + if (this._log.isLevelEnabled("perf")) { + this._log.perf( + `getDependenciesReader completed in ${(performance.now() - readerStart).toFixed(2)} ms`); + } + const cacheStart = performance.now(); + const boolOrChangedPaths = await this.getBuildCache().prepareProjectBuildAndValidateCache(depReader); + if (this._log.isLevelEnabled("perf")) { + this._log.perf( + `ProjectBuildCache.prepareProjectBuildAndValidateCache completed in ` + + `${(performance.now() - cacheStart).toFixed(2)} ms`); + } + if (Array.isArray(boolOrChangedPaths)) { + // Cache can be used, but some resources have changed + // Propagate changed paths to dependents + this.propagateResourceChanges(boolOrChangedPaths); + } + return !!boolOrChangedPaths; + } + + /** + * Builds the project by running all required tasks + * + * Executes all configured build tasks for the project using the task runner. + * Must be called after prepareProjectBuildAndValidateCache(). + * + * @param {AbortSignal} [signal] Abort signal + */ + async buildProject(signal) { + const changedPaths = await this.getTaskRunner().runTasks(signal); + // Propagate changed paths to dependents + this.propagateResourceChanges(changedPaths); + } + + buildFinished() { + this.getBuildCache().buildFinished(); + } + + /** + * Informs the build cache about changed project source resources + * + * Notifies the cache that source files have changed so it can invalidate + * affected cache entries and mark the cache as stale. + * + * @param {string[]} changedPaths Changed project source file paths + */ + projectSourcesChanged(changedPaths) { + return this._buildCache.projectSourcesChanged(changedPaths); + } + + /** + * Informs the build cache about changed dependency resources + * + * Notifies the cache that dependency resources have changed so it can invalidate + * affected cache entries and mark the cache as stale. + * + * @param {string[]} changedPaths Changed dependency resource paths + */ + dependencyResourcesChanged(changedPaths) { + return this._buildCache.dependencyResourcesChanged(changedPaths); + } + + propagateResourceChanges(changedPaths) { + if (!changedPaths.length) { + return; + } + for (const {project: dep} of this._buildContext.getGraph().traverseDependents(this._project.getName())) { + const projectBuildContext = this._buildContext.getBuildContext(dep.getName()); + if (projectBuildContext) { + projectBuildContext.dependencyResourcesChanged(changedPaths); + } + } + } + + /** + * Gets the build manifest if available and compatible + * + * Retrieves the project's build manifest and validates its version. + * Only manifest versions 0.1 and 0.2 are currently supported. * - * @returns {boolean} True if the project needs to be built + * @returns {object|undefined} Build manifest object or undefined if unavailable or incompatible */ - requiresBuild() { - return !this._project.getBuildManifest(); + #getBuildManifest() { + const manifest = this._project.getBuildManifest(); + if (!manifest) { + return; + } + // Check whether the manifest can be used for this build + if (manifest.manifestVersion === "0.1" || manifest.manifestVersion === "0.2") { + // Manifest version 0.1 and 0.2 are always used without further checks for legacy reasons + return manifest; + } + // Unknown manifest version can't be used + return; } + /** + * Gets metadata about the previous build from the build manifest + * + * Extracts timestamp and age information from the build manifest if available. + * + * @returns {object|null} Build metadata with timestamp and age, or null if no manifest exists + * @returns {string} return.timestamp ISO timestamp of the previous build + * @returns {string} return.age Human-readable age of the previous build + */ getBuildMetadata() { - const buildManifest = this._project.getBuildManifest(); + const buildManifest = this.#getBuildManifest(); if (!buildManifest) { return null; } const timeDiff = (new Date().getTime() - new Date(buildManifest.timestamp).getTime()); - // TODO: Format age properly via a new @ui5/logger util module + // TODO: Format age properly return { timestamp: buildManifest.timestamp, age: timeDiff / 1000 + " seconds" }; } + + /** + * Gets the project build cache instance + * + * @returns {@ui5/project/build/cache/ProjectBuildCache} Build cache instance + */ + getBuildCache() { + return this._buildCache; + } + + async writeBuildCache() { + await this._buildCache.writeCache(); + } + + /** + * Gets the build signature for this project + * + * The build signature uniquely identifies the build configuration and dependencies, + * used for cache validation and invalidation. + * + * @returns {string} Build signature string + */ + getBuildSignature() { + return this._buildSignature; + } } export default ProjectBuildContext; diff --git a/packages/project/lib/build/helpers/TaskUtil.js b/packages/project/lib/build/helpers/TaskUtil.js index b3a4fb97437..3a3f8e21d81 100644 --- a/packages/project/lib/build/helpers/TaskUtil.js +++ b/packages/project/lib/build/helpers/TaskUtil.js @@ -35,10 +35,10 @@ class TaskUtil { * This tag identifies resources that contain (i.e. bundle) multiple other resources * @property {string} IsDebugVariant * This tag identifies resources that are a debug variant (typically named with a "-dbg" suffix) - * of another resource. This tag is part of the build manifest. + * of another resource. This tag is visible to other projects * @property {string} HasDebugVariant * This tag identifies resources for which a debug variant has been created. - * This tag is part of the build manifest. + * This tag is visible to other projects */ /** diff --git a/packages/project/lib/build/helpers/WatchHandler.js b/packages/project/lib/build/helpers/WatchHandler.js new file mode 100644 index 00000000000..08d5d6d2ffd --- /dev/null +++ b/packages/project/lib/build/helpers/WatchHandler.js @@ -0,0 +1,77 @@ +import EventEmitter from "node:events"; +import chokidar from "chokidar"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:helpers:WatchHandler"); + +/** + * Context of a build process + * + * @private + * @memberof @ui5/project/build/helpers + */ +class WatchHandler extends EventEmitter { + #closeCallbacks = []; + + constructor() { + super(); + } + + async watch(projects) { + const readyPromises = []; + for (const project of projects) { + readyPromises.push(this._watchProject(project)); + } + await Promise.all(readyPromises); + } + + async _watchProject(project) { + let ready = false; + const paths = project.getSourcePaths(); + log.verbose(`Watching source paths: ${paths.join(", ")}`); + + const watcher = chokidar.watch(paths, { + ignoreInitial: true, + }); + this.#closeCallbacks.push(async () => { + await watcher.close(); + }); + watcher.on("all", (event, filePath) => { + if (!ready) { + // Ignore events before ready + return; + } + if (event === "addDir") { + // Ignore directory creation events + return; + } + this.#handleWatchEvents(event, filePath, project).catch((err) => { + this.emit("error", err); + }); + }); + const {promise, resolve} = Promise.withResolvers(); + + watcher.on("ready", () => { + ready = true; + resolve(); + }); + watcher.on("error", (err) => { + this.emit("error", err); + }); + + return promise; + } + + async destroy() { + for (const cb of this.#closeCallbacks) { + await cb(); + } + } + + async #handleWatchEvents(eventType, filePath, project) { + const resourcePath = project.getVirtualPath(filePath); + log.verbose(`File changed: ${eventType} ${filePath} (as ${resourcePath} in project '${project.getName()}')`); + this.emit("change", eventType, resourcePath, project); + } +} + +export default WatchHandler; diff --git a/packages/project/lib/build/helpers/composeProjectList.js b/packages/project/lib/build/helpers/composeProjectList.js index d98ad17929e..0f4c5d6d01f 100644 --- a/packages/project/lib/build/helpers/composeProjectList.js +++ b/packages/project/lib/build/helpers/composeProjectList.js @@ -6,17 +6,17 @@ const log = getLogger("build:helpers:composeProjectList"); * its value is an array of all of its transitive dependencies. * * @param {@ui5/project/graph/ProjectGraph} graph - * @returns {Promise>} A promise resolving to an object with dependency names as + * @returns {Object} A promise resolving to an object with dependency names as * key and each with an array of its transitive dependencies as value */ -async function getFlattenedDependencyTree(graph) { +function getFlattenedDependencyTree(graph) { const dependencyMap = Object.create(null); const rootName = graph.getRoot().getName(); - await graph.traverseDepthFirst(({project, dependencies}) => { + for (const {project, dependencies} of graph.traverseDependenciesDepthFirst()) { if (project.getName() === rootName) { // Skip root project - return; + continue; } const projectDeps = []; dependencies.forEach((depName) => { @@ -26,7 +26,7 @@ async function getFlattenedDependencyTree(graph) { } }); dependencyMap[project.getName()] = projectDeps; - }); + } return dependencyMap; } @@ -41,7 +41,7 @@ async function getFlattenedDependencyTree(graph) { * @returns {{includedDependencies:string[],excludedDependencies:string[]}} An object containing the * 'includedDependencies' and 'excludedDependencies' */ -async function createDependencyLists(graph, { +function createDependencyLists(graph, { includeAllDependencies = false, includeDependency = [], includeDependencyRegExp = [], includeDependencyTree = [], excludeDependency = [], excludeDependencyRegExp = [], excludeDependencyTree = [], @@ -57,7 +57,7 @@ async function createDependencyLists(graph, { return {includedDependencies: [], excludedDependencies: []}; } - const flattenedDependencyTree = await getFlattenedDependencyTree(graph); + const flattenedDependencyTree = getFlattenedDependencyTree(graph); function isExcluded(excludeList, depName) { return excludeList && excludeList.has(depName); diff --git a/packages/project/lib/build/helpers/createBuildManifest.js b/packages/project/lib/build/helpers/createBuildManifest.js index 998935b3c05..1fe44ca7f40 100644 --- a/packages/project/lib/build/helpers/createBuildManifest.js +++ b/packages/project/lib/build/helpers/createBuildManifest.js @@ -8,7 +8,7 @@ async function getVersion(pkg) { } function getSortedTags(project) { - const tags = project.getResourceTagCollection().getAllTags(); + const tags = project.getProjectResourceTagCollection().getAllTags(); const entities = Object.entries(tags); entities.sort(([keyA], [keyB]) => { return keyA.localeCompare(keyB); @@ -16,7 +16,7 @@ function getSortedTags(project) { return Object.fromEntries(entities); } -export default async function(project, buildConfig, taskRepository) { +export default async function(project, buildConfig, taskRepository, signature) { if (!project) { throw new Error(`Missing parameter 'project'`); } @@ -26,6 +26,10 @@ export default async function(project, buildConfig, taskRepository) { if (!taskRepository) { throw new Error(`Missing parameter 'taskRepository'`); } + if (!signature) { + throw new Error(`Missing parameter 'signature'`); + } + const projectName = project.getName(); const type = project.getType(); @@ -45,7 +49,6 @@ export default async function(project, buildConfig, taskRepository) { `Project type ${type} is currently not supported`); } - const {builderVersion, fsVersion: builderFsVersion} = await taskRepository.getVersions(); const metadata = { project: { specVersion: project.getSpecVersion().toString(), @@ -59,27 +62,35 @@ export default async function(project, buildConfig, taskRepository) { } } }, - buildManifest: { - manifestVersion: "0.2", - timestamp: new Date().toISOString(), - versions: { - builderVersion: builderVersion, - projectVersion: await getVersion("@ui5/project"), - fsVersion: await getVersion("@ui5/fs"), - }, - buildConfig, - version: project.getVersion(), - namespace: project.getNamespace(), - tags: getSortedTags(project) - } + buildManifest: await createBuildManifest(project, buildConfig, taskRepository, signature), }; - if (metadata.buildManifest.versions.fsVersion !== builderFsVersion) { + return metadata; +} + +async function createBuildManifest(project, buildConfig, taskRepository, signature) { + // Use legacy manifest version for framework libraries to ensure compatibility + const {builderVersion, fsVersion: builderFsVersion} = await taskRepository.getVersions(); + const buildManifest = { + manifestVersion: "1.0", + timestamp: new Date().toISOString(), + signature, + versions: { + builderVersion: builderVersion, + projectVersion: await getVersion("@ui5/project"), + fsVersion: await getVersion("@ui5/fs"), + }, + buildConfig, + version: project.getVersion(), + namespace: project.getNamespace(), + tags: getSortedTags(project) + }; + + if (buildManifest.versions.fsVersion !== builderFsVersion) { // Added in manifestVersion 0.2: // @ui5/project and @ui5/builder use different versions of @ui5/fs. // This should be mentioned in the build manifest: - metadata.buildManifest.versions.builderFsVersion = builderFsVersion; + buildManifest.versions.builderFsVersion = builderFsVersion; } - - return metadata; + return buildManifest; } diff --git a/packages/project/lib/build/helpers/getBuildSignature.js b/packages/project/lib/build/helpers/getBuildSignature.js new file mode 100644 index 00000000000..f3dc3bc440c --- /dev/null +++ b/packages/project/lib/build/helpers/getBuildSignature.js @@ -0,0 +1,29 @@ +import crypto from "node:crypto"; + +const BUILD_SIG_VERSION = "0"; + +export function getBaseSignature(buildConfig) { + const key = BUILD_SIG_VERSION + JSON.stringify(buildConfig); + return crypto.createHash("sha256").update(key).digest("hex"); +} + +/** + * The build signature is calculated based on the **build configuration and environment** of a project. + * + * The hash is represented as a hexadecimal string to allow safe usage in file names. + * + * @private + * @param {string} baseSignature + * @param {@ui5/project/lib/Project} project The project to create the cache integrity for + * @param {@ui5/project/lib/graph/ProjectGraph} graph The project graph + * @param {@ui5/builder/tasks/taskRepository} taskRepository The task repository (used to determine the effective + * versions of ui5-builder and ui5-fs) + */ +export function getProjectSignature(baseSignature, project, graph, taskRepository) { + const key = baseSignature + project.getId() + JSON.stringify(project.getConfig()); + // TODO: Add signatures of relevant custom tasks + + // Create a hash for all metadata + const hash = crypto.createHash("sha256").update(key).digest("hex"); + return hash; +} diff --git a/packages/project/lib/graph/ProjectGraph.js b/packages/project/lib/graph/ProjectGraph.js index ba6967154e6..b0d8f1cfaf0 100644 --- a/packages/project/lib/graph/ProjectGraph.js +++ b/packages/project/lib/graph/ProjectGraph.js @@ -1,6 +1,7 @@ import OutputStyleEnum from "../build/helpers/ProjectBuilderOutputStyle.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("graph:ProjectGraph"); +import Cache from "../../../project/lib/build/cache/Cache.js"; /** @@ -284,6 +285,40 @@ class ProjectGraph { processDependency(projectName); return Array.from(dependencies); } + + getDependents(projectName) { + if (!this._projects.has(projectName)) { + throw new Error( + `Failed to get dependents for project ${projectName}: ` + + `Unable to find project in project graph`); + } + const dependents = []; + for (const [fromProjectName, adjacencies] of this._adjList) { + if (adjacencies.has(projectName)) { + dependents.push(fromProjectName); + } + } + return dependents; + } + + getTransitiveDependents(projectName) { + const dependents = new Set(); + if (!this._projects.has(projectName)) { + throw new Error( + `Failed to get transitive dependents for project ${projectName}: ` + + `Unable to find project in project graph`); + } + const addDependents = (projectName) => { + const projectDependents = this.getDependents(projectName); + projectDependents.forEach((dependent) => { + dependents.add(dependent); + addDependents(dependent); + }); + }; + addDependents(projectName); + return Array.from(dependents); + } + /** * Checks whether a dependency is optional or not. * Currently only used in tests. @@ -475,6 +510,135 @@ class ProjectGraph { })(); } + /** + * Generator function that traverses all dependencies of the given start project depth-first. + * Each dependency project is visited exactly once, and dependencies are fully explored + * before the dependent project is yielded (post-order traversal). + * In case a cycle is detected, an error is thrown. + * + * @public + * @generator + * @param {string|boolean} [startName] Name of the project to start the traversal at, + * or a boolean to set includeStartModule while using the root project as start. + * Defaults to the graph's root project. + * @param {boolean} [includeStartModule=false] Whether to include the start project itself in the results + * @yields {object} Object containing the project and its direct dependencies + * @yields {module:@ui5/project/specifications/Project} return.project The dependency project + * @yields {string[]} return.dependencies Array of direct dependency names for this project + * @throws {Error} If the start project cannot be found or if a cycle is detected + */ + * traverseDependenciesDepthFirst(startName, includeStartModule = false) { + if (typeof startName === "boolean") { + includeStartModule = startName; + startName = undefined; + } + if (!startName) { + startName = this._rootProjectName; + } else if (!this.getProject(startName)) { + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); + } + + const visited = Object.create(null); + const processing = Object.create(null); + + const traverse = function* (projectName, ancestors) { + this._checkCycle(ancestors, projectName); + + if (visited[projectName]) { + return; + } + + if (processing[projectName]) { + return; + } + + processing[projectName] = true; + const newAncestors = [...ancestors, projectName]; + const dependencies = this.getDependencies(projectName); + + for (const depName of dependencies) { + yield* traverse.call(this, depName, newAncestors); + } + + visited[projectName] = true; + processing[projectName] = false; + + if (includeStartModule || projectName !== startName) { + yield { + project: this.getProject(projectName), + dependencies + }; + } + }.bind(this); + + yield* traverse(startName, []); + } + + /** + * Generator function that traverses all projects that depend on the given start project. + * Traversal is breadth-first, visiting each dependent project exactly once. + * Projects are yielded in the order they are discovered as dependents. + * In case a cycle is detected, an error is thrown. + * + * @public + * @generator + * @param {string|boolean} [startName] Name of the project to start the traversal at, + * or a boolean to set includeStartModule while using the root project as start. + * Defaults to the graph's root project. + * @param {boolean} [includeStartModule=false] Whether to include the start project itself in the results + * @yields {object} Object containing the dependent project and its dependents + * @yields {module:@ui5/project/specifications/Project} return.project The dependent project + * @yields {string[]} return.dependents Array of project names that depend on this project + * @throws {Error} If the start project cannot be found or if a cycle is detected + */ + * traverseDependents(startName, includeStartModule = false) { + if (typeof startName === "boolean") { + includeStartModule = startName; + startName = undefined; + } + if (!startName) { + startName = this._rootProjectName; + } else if (!this.getProject(startName)) { + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); + } + + const queue = [{ + projectNames: [startName], + ancestors: [] + }]; + + const visited = Object.create(null); + + while (queue.length) { + const {projectNames, ancestors} = queue.shift(); // Get and remove first entry from queue + + for (const projectName of projectNames) { + this._checkCycle(ancestors, projectName); + if (visited[projectName]) { + continue; + } + + visited[projectName] = true; + + const newAncestors = [...ancestors, projectName]; + const dependents = this.getDependents(projectName); + + queue.push({ + projectNames: dependents, + ancestors: newAncestors + }); + + if (includeStartModule || projectName !== startName) { + // Do not yield the start module itself + yield { + project: this.getProject(projectName), + dependents + }; + } + } + } + } + /** * Join another project graph into this one. * Projects and extensions which already exist in this graph will cause an error to be thrown @@ -550,6 +714,8 @@ class ProjectGraph { * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * @param {module:@ui5/project/build/ProjectBuilderOutputStyle} [parameters.outputStyle=Default] * Processes build results into a specific directory structure. + * @param {module:@ui5/project/build/cache/Cache} [parameters.cache=Default] + * Cache mode to use for building UI5 projects * @returns {Promise} Promise resolving to undefined once build has finished */ async build({ @@ -558,15 +724,16 @@ class ProjectGraph { dependencyIncludes, selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, includedTasks = [], excludedTasks = [], - outputStyle = OutputStyleEnum.Default + outputStyle = OutputStyleEnum.Default, + cache = Cache.Default, }) { this.seal(); // Do not allow further changes to the graph - if (this._built) { + if (this._builtOrServed) { throw new Error( - `Project graph with root node ${this._rootProjectName} has already been built. ` + - `Each graph can only be built once`); + `Project graph with root node ${this._rootProjectName} has already been built or served. ` + + `Each graph can only be built or served once`); } - this._built = true; + this._builtOrServed = true; const { default: ProjectBuilder } = await import("../build/ProjectBuilder.js"); @@ -577,15 +744,51 @@ class ProjectGraph { selfContained, cssVariables, jsdoc, createBuildManifest, includedTasks, excludedTasks, outputStyle, + cache } }); - await builder.build({ + return await builder.buildToTarget({ destPath, cleanDest, includedDependencies, excludedDependencies, dependencyIncludes, }); } + async serve({ + initialBuildRootProject = false, + initialBuildIncludedDependencies = [], initialBuildExcludedDependencies = [], + selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, + includedTasks = [], excludedTasks = [], + cache = Cache.Default, + }) { + this.seal(); // Do not allow further changes to the graph + if (this._builtOrServed) { + throw new Error( + `Project graph with root node ${this._rootProjectName} has already been built or served. ` + + `Each graph can only be built or served once`); + } + this._builtOrServed = true; + const { + default: ProjectBuilder + } = await import("../build/ProjectBuilder.js"); + const builder = new ProjectBuilder({ + graph: this, + taskRepository: await this._getTaskRepository(), + buildConfig: { + selfContained, cssVariables, jsdoc, + createBuildManifest, + includedTasks, excludedTasks, + outputStyle: OutputStyleEnum.Default, + cache + } + }); + const { + default: BuildServer + } = await import("../build/BuildServer.js"); + return new BuildServer(this, builder, + initialBuildRootProject, initialBuildIncludedDependencies, initialBuildExcludedDependencies); + } + /** * Seal the project graph so that no further changes can be made to it * diff --git a/packages/project/lib/graph/graph.js b/packages/project/lib/graph/graph.js index 0885265447f..bf1bd5c3583 100644 --- a/packages/project/lib/graph/graph.js +++ b/packages/project/lib/graph/graph.js @@ -31,8 +31,8 @@ const log = getLogger("generateProjectGraph"); * Whether framework dependencies should be added to the graph * @param {string|null} [options.workspaceName=default] * Name of the workspace configuration that should be used. "default" if not provided. - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {string} [options.workspaceConfigPath=ui5-workspace.yaml] * Workspace configuration file to use if no object has been provided * @param {@ui5/project/graph/Workspace~Configuration} [options.workspaceConfiguration] @@ -42,7 +42,7 @@ const log = getLogger("generateProjectGraph"); */ export async function graphFromPackageDependencies({ cwd, rootConfiguration, rootConfigPath, - versionOverride, cacheMode, resolveFrameworkDependencies = true, + versionOverride, snapshotCache, resolveFrameworkDependencies = true, workspaceName="default", workspaceConfiguration, workspaceConfigPath = "ui5-workspace.yaml" }) { @@ -73,7 +73,7 @@ export async function graphFromPackageDependencies({ const projectGraph = await projectGraphBuilder(provider, workspace); if (resolveFrameworkDependencies) { - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode, workspace}); + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, snapshotCache, workspace}); } return projectGraph; @@ -98,8 +98,8 @@ export async function graphFromPackageDependencies({ * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to * cwd or an absolute path. In both case, platform-specific path segment separators must be used. * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {string} [options.resolveFrameworkDependencies=true] * Whether framework dependencies should be added to the graph * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance @@ -107,7 +107,7 @@ export async function graphFromPackageDependencies({ export async function graphFromStaticFile({ filePath = "projectDependencies.yaml", cwd, rootConfiguration, rootConfigPath, - versionOverride, cacheMode, resolveFrameworkDependencies = true + versionOverride, snapshotCache, resolveFrameworkDependencies = true }) { log.verbose(`Creating project graph using static file...`); const { @@ -128,7 +128,7 @@ export async function graphFromStaticFile({ const projectGraph = await projectGraphBuilder(provider); if (resolveFrameworkDependencies) { - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode}); + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, snapshotCache}); } return projectGraph; @@ -150,8 +150,8 @@ export async function graphFromStaticFile({ * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to * cwd or an absolute path. In both case, platform-specific path segment separators must be used. * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {string} [options.resolveFrameworkDependencies=true] * Whether framework dependencies should be added to the graph * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance @@ -159,7 +159,7 @@ export async function graphFromStaticFile({ export async function graphFromObject({ dependencyTree, cwd, rootConfiguration, rootConfigPath, - versionOverride, cacheMode, resolveFrameworkDependencies = true + versionOverride, snapshotCache, resolveFrameworkDependencies = true }) { log.verbose(`Creating project graph using object...`); const { @@ -178,7 +178,7 @@ export async function graphFromObject({ const projectGraph = await projectGraphBuilder(dependencyTreeProvider); if (resolveFrameworkDependencies) { - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode}); + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, snapshotCache}); } return projectGraph; diff --git a/packages/project/lib/graph/helpers/ui5Framework.js b/packages/project/lib/graph/helpers/ui5Framework.js index 19737a4bd7b..660cc78427e 100644 --- a/packages/project/lib/graph/helpers/ui5Framework.js +++ b/packages/project/lib/graph/helpers/ui5Framework.js @@ -282,15 +282,15 @@ export default { * @param {object} [options] * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework * version - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {@ui5/project/graph/Workspace} [options.workspace] * Optional workspace instance to use for overriding node resolutions * @returns {Promise<@ui5/project/graph/ProjectGraph>} * Promise resolving with the given graph instance to allow method chaining */ enrichProjectGraph: async function(projectGraph, options = {}) { - const {workspace, cacheMode} = options; + const {workspace, snapshotCache} = options; const rootProject = projectGraph.getRoot(); const frameworkName = rootProject.getFrameworkName(); const frameworkVersion = rootProject.getFrameworkVersion(); @@ -386,7 +386,7 @@ export default { cwd, version, providedLibraryMetadata, - cacheMode, + snapshotCache, ui5DataDir }); diff --git a/packages/project/lib/resources/ProjectResources.js b/packages/project/lib/resources/ProjectResources.js new file mode 100644 index 00000000000..790bf23b250 --- /dev/null +++ b/packages/project/lib/resources/ProjectResources.js @@ -0,0 +1,527 @@ +import {createWorkspace, createReaderCollectionPrioritized} from "@ui5/fs/resourceFactory"; +import MonitoredResourceTagCollection from "@ui5/fs/internal/MonitoredResourceTagCollection"; +import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; +import Stage from "./Stage.js"; + +const INITIAL_STAGE_ID = "initial"; +const RESULT_STAGE_ID = "result"; + +/** + * Manages resource access and stages for a project. + * + * @public + * @class + * @alias @ui5/project/resources/ProjectResources + */ +class ProjectResources { + #stages = []; // Stages in order of creation + + // State + #currentStage; + #currentStageReadIndex; + #lastTagCacheImportIndex; + #currentStageId; + + // Cache + #currentStageWorkspace; + #currentStageReaders; // Map to store the various reader styles + + // CAS-backed frozen source reader (set after build or restore from cache) + #frozenSourceReader = null; + + // Callbacks (interface object) + #getName; + #getStyledReader; + #createWriter; + #addReadersForWriter; + + // Project tag collection resets at the beginning of every build + #projectResourceTagCollection; + // Build tag collection resets at the end of every build + // (so that those tags are not accessible to dependent projects) + #buildResourceTagCollection; + + // Individual monitors per stage + #monitoredProjectResourceTagCollection; + #monitoredBuildResourceTagCollection; + + #buildManifest; + + /** + * @param {object} options Configuration options + * @param {Function} options.getName Returns the project name (for error messages and reader names) + * @param {Function} options.getStyledReader Gets the source reader for a given style + * @param {Function} options.createWriter Creates a writer for a stage + * @param {Function} options.addReadersForWriter Adds readers for a writer to a readers array + * @param {object} options.buildManifest + */ + constructor({getName, getStyledReader, createWriter, addReadersForWriter, buildManifest}) { + this.#getName = getName; + this.#getStyledReader = getStyledReader; + this.#createWriter = createWriter; + this.#addReadersForWriter = addReadersForWriter; + this.#buildManifest = buildManifest; + + this.#initStageMetadata(); + } + + /** + * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the + * project in the specified "style": + * + *
    + *
  • buildtime: Resource paths are always prefixed with /resources/ + * or /test-resources/ followed by the project's namespace. + * Any configured build-excludes are applied
  • + *
  • dist: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * Any configured build-excludes are applied
  • + *
  • runtime: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
  • + *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that + * project types like "theme-library", which can have multiple namespaces, can't omit them. + * Any configured build-excludes are applied
  • + *
+ * + * If project resources have been changed through the means of a workspace, those changes + * are reflected in the provided reader too. + * + * Resource readers always use POSIX-style paths. + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. + * Can be "buildtime", "dist", "runtime" or "flat" + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getReader({style = "buildtime"} = {}) { + let reader = this.#currentStageReaders.get(style); + if (reader) { + // Use cached reader + return reader; + } + + const readers = []; + if (this.#currentStage) { + // Add current writer as highest priority reader + const currentWriter = this.#currentStage.getWriter(); + if (currentWriter) { + this.#addReadersForWriter(readers, currentWriter, style); + } else { + const currentReader = this.#currentStage.getCachedWriter(); + if (currentReader) { + this.#addReadersForWriter(readers, currentReader, style); + } + } + } + // Add readers for previous stages and source + readers.push(...this.#getReaders(style)); + + reader = createReaderCollectionPrioritized({ + name: `Reader collection for stage '${this.#currentStageId}' of project ${this.#getName()}`, + readers + }); + + this.#currentStageReaders.set(style, reader); + return reader; + } + + #getReaders(style = "buildtime") { + const readers = []; + + // Add writers for previous stages as readers + const stageReadIdx = this.#currentStageReadIndex; + + // Collect writers from all relevant stages + for (let i = stageReadIdx; i >= 0; i--) { + this.#addReaderForStage(this.#stages[i], readers, style); + } + + // Add CAS-backed frozen source reader (if available) + if (this.#frozenSourceReader) { + readers.push(this.#frozenSourceReader); + } + + // Finally add the project's source reader + readers.push(this.#getStyledReader(style)); + + return readers; + } + + /** + * Get the source reader for the project. + * + * @public + * @param {string} [style=buildtime] Path style to access resources + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getSourceReader(style = "buildtime") { + return this.#getStyledReader(style); + } + + /** + * Sets a CAS-backed frozen source reader that provides immutable snapshots + * of untransformed source files. This reader is inserted into the reader chain + * between stage readers and the filesystem source reader, so that downstream + * dependency consumers read from CAS instead of the live filesystem. + * + * @public + * @param {@ui5/fs/AbstractReader} reader CAS-backed reader for frozen source files + */ + setFrozenSourceReader(reader) { + this.#frozenSourceReader = reader; + // Invalidate cached readers since the reader chain changed + this.#currentStageReaders = new Map(); + } + + /** + * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a + * project's resources. This is always of style buildtime. + * + * Once a project has finished building, this method will throw to prevent further modifications + * since those would have no effect. Use the getReader method to access the project's (modified) resources + * + * @public + * @returns {@ui5/fs/DuplexCollection} DuplexCollection + */ + getWorkspace() { + if (this.#currentStageId === RESULT_STAGE_ID) { + throw new Error( + `Workspace of project ${this.#getName()} is currently not available. ` + + `This might indicate that the project has already finished building ` + + `and its content can not be modified further. ` + + `Use method 'getReader' for read-only access`); + } + if (this.#currentStageWorkspace) { + return this.#currentStageWorkspace; + } + const reader = createReaderCollectionPrioritized({ + name: `Reader collection for stage '${this.#currentStageId}' of project ${this.#getName()}`, + readers: this.#getReaders(), + }); + const writer = this.#currentStage.getWriter(); + const workspace = createWorkspace({ + reader, + writer + }); + this.#currentStageWorkspace = workspace; + return workspace; + } + + /** + * Seal the workspace of the project, preventing further modifications. + * This is typically called once the project has finished building. Resources from all stages will be used. + * + * A project can be unsealed by calling useStage() again. + * + * @public + */ + useResultStage() { + this.#currentStage = null; + this.#currentStageId = RESULT_STAGE_ID; + this.#currentStageReadIndex = this.#stages.length - 1; // Read from all stages + + // Unset "current" reader/writer. They will be recreated on demand + this.#currentStageReaders = new Map(); + this.#currentStageWorkspace = null; + + this.#monitoredProjectResourceTagCollection = null; + this.#monitoredBuildResourceTagCollection = null; + } + + #initStageMetadata() { + this.#stages = []; + // Initialize with an empty stage for use without stages (i.e. without build cache) + this.#currentStage = new Stage(INITIAL_STAGE_ID, this.#createWriter(INITIAL_STAGE_ID)); + this.#currentStageId = INITIAL_STAGE_ID; + this.#currentStageReadIndex = -1; + this.#lastTagCacheImportIndex = -1; + this.#currentStageReaders = new Map(); + this.#currentStageWorkspace = null; + this.#frozenSourceReader = null; + this.#projectResourceTagCollection = null; + } + + #addReaderForStage(stage, readers, style = "buildtime") { + const writer = stage.getWriter(); + if (writer) { + this.#addReadersForWriter(readers, writer, style); + } else { + const reader = stage.getCachedWriter(); + if (reader) { + this.#addReadersForWriter(readers, reader, style); + } + } + } + + /** + * Initialize stages for the build process. + * + * @public + * @param {string[]} stageIds Array of stage IDs to initialize + */ + initStages(stageIds) { + this.#initStageMetadata(); + for (let i = 0; i < stageIds.length; i++) { + const stageId = stageIds[i]; + const newStage = new Stage(stageId, this.#createWriter(stageId)); + this.#stages.push(newStage); + } + } + + /** + * Get the current stage. + * + * @public + * @returns {Stage|null} The current stage or null if in result stage + */ + getStage() { + return this.#currentStage; + } + + /** + * Switch to a specific stage. + * + * @public + * @param {string} stageId The ID of the stage to use + * @throws {Error} If the stage does not exist + */ + useStage(stageId) { + if (stageId === this.#currentStage?.getId()) { + // Already using requested stage + return; + } + + const stageIdx = this.#stages.findIndex((s) => s.getId() === stageId); + + if (stageIdx === -1) { + throw new Error(`Stage '${stageId}' does not exist in project ${this.#getName()}`); + } + + const stage = this.#stages[stageIdx]; + this.#currentStage = stage; + this.#currentStageId = stageId; + this.#currentStageReadIndex = stageIdx - 1; // Read from all previous stages + + // Unset "current" reader/writer caches. They will be recreated on demand + this.#currentStageReaders = new Map(); + this.#currentStageWorkspace = null; + + this.#monitoredProjectResourceTagCollection = null; + this.#monitoredBuildResourceTagCollection = null; + } + + /** + * Set or replace a stage. + * + * @public + * @param {string} stageId The ID of the stage to set + * @param {Stage|object} stageOrCachedWriter A Stage instance or a cached writer/reader + * @param {Map>} projectTagOperations + * @param {Map>} buildTagOperations + * @returns {boolean} True if the stored stage has changed, false otherwise + * @throws {Error} If the stage does not exist or invalid parameters are provided + */ + setStage(stageId, stageOrCachedWriter, projectTagOperations, buildTagOperations) { + const stageIdx = this.#stages.findIndex((s) => s.getId() === stageId); + if (stageIdx === -1) { + throw new Error(`Stage '${stageId}' does not exist in project ${this.#getName()}`); + } + if (!stageOrCachedWriter) { + throw new Error( + `Invalid stage or cache reader provided for stage '${stageId}' in project ${this.#getName()}`); + } + const oldStage = this.#stages[stageIdx]; + if (oldStage.getId() !== stageId) { + throw new Error( + `Stage ID mismatch for stage '${stageId}' in project ${this.#getName()}`); + } + let newStage; + if (stageOrCachedWriter instanceof Stage) { + newStage = stageOrCachedWriter; + if (oldStage === newStage) { + // Same stage as before, nothing to do + return false; // Stored stage has not changed + } + } else { + newStage = new Stage(stageId, undefined, stageOrCachedWriter, + projectTagOperations, buildTagOperations); + } + this.#stages[stageIdx] = newStage; + + // If we are updating the current stage, make sure to update and reset all relevant references + if (oldStage === this.#currentStage) { + this.#currentStage = newStage; + // Unset "current" reader/writer. They might be outdated + this.#currentStageReaders = new Map(); + this.#currentStageWorkspace = null; + } + return true; // Indicate that the stored stage has changed + } + + buildFinished() { + // Clear build resource tag collections. They must not be provided to dependent projects + this.#buildResourceTagCollection = null; + } + + /** + * Gets the appropriate resource tag collection for a resource and tag + * + * Determines which tag collection (project-specific or build-level) should be used + * for the given resource and tag combination. Associates the resource with the current + * project if not already associated. + * + * @param {@ui5/fs/Resource} resource Resource to get tag collection for + * @param {string} tag Tag to check acceptance for + * @returns {@ui5/fs/internal/ResourceTagCollection} Appropriate tag collection + * @throws {Error} If no collection accepts the given tag + */ + getResourceTagCollection(resource, tag) { + this.#applyCachedResourceTags(); + const projectCollection = this.#getProjectResourceTagCollection(); + if (!tag || projectCollection.acceptsTag(tag)) { + if (!this.#monitoredProjectResourceTagCollection) { + this.#monitoredProjectResourceTagCollection = new MonitoredResourceTagCollection(projectCollection); + } + return this.#monitoredProjectResourceTagCollection; + } + const buildCollection = this.#getBuildResourceTagCollection(); + if (buildCollection.acceptsTag(tag)) { + if (!this.#monitoredBuildResourceTagCollection) { + this.#monitoredBuildResourceTagCollection = new MonitoredResourceTagCollection(buildCollection); + } + return this.#monitoredBuildResourceTagCollection; + } + throw new Error(`Could not find collection for resource ${resource.getPath()} and tag ${tag}`); + } + + getResourceTagOperations() { + return { + projectTagOperations: new Map([ + ...this.#currentStage.getCachedProjectTagOperations() ?? [], + ...this.#monitoredProjectResourceTagCollection?.getTagOperations() ?? [], + ]), + buildTagOperations: new Map([ + ...this.#currentStage.getCachedBuildTagOperations() ?? [], + ...this.#monitoredBuildResourceTagCollection?.getTagOperations() ?? [], + ]), + }; + } + + /** + * Imports tag operations into the underlying tag collections. + * + * This is used during delta builds to apply tag operations from a previous stage cache + * that would otherwise be lost because the delta execution only records its own tag operations. + * + * @param {Map>} [projectTagOperations] + * @param {Map>} [buildTagOperations] + */ + importTagOperations(projectTagOperations, buildTagOperations) { + if (projectTagOperations?.size) { + const projectTagCollection = this.#getProjectResourceTagCollection(); + for (const [resourcePath, tags] of projectTagOperations.entries()) { + for (const [tag, value] of tags.entries()) { + projectTagCollection.setTag(resourcePath, tag, value); + } + } + } + if (buildTagOperations?.size) { + const buildTagCollection = this.#getBuildResourceTagCollection(); + for (const [resourcePath, tags] of buildTagOperations.entries()) { + for (const [tag, value] of tags.entries()) { + buildTagCollection.setTag(resourcePath, tag, value); + } + } + } + } + + /** + * Returns the project-level resource tag collection. + * + * This provides direct access to the collection holding project-level tags + * (e.g. ui5:IsDebugVariant, ui5:HasDebugVariant), which is needed for + * build manifest creation and reading. + * + * @returns {@ui5/fs/internal/ResourceTagCollection} The project-level resource tag collection + */ + getProjectResourceTagCollection() { + return this.#getProjectResourceTagCollection(); + } + + #getProjectResourceTagCollection() { + if (!this.#projectResourceTagCollection) { + this.#projectResourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], + allowedNamespaces: ["project"], + tags: this.#buildManifest?.tags + }); + } + return this.#projectResourceTagCollection; + } + + #getBuildResourceTagCollection() { + if (!this.#buildResourceTagCollection) { + this.#buildResourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:OmitFromBuildResult", "ui5:IsBundle"], + allowedNamespaces: ["build"], + tags: this.#buildManifest?.tags + }); + } + return this.#buildResourceTagCollection; + } + + #applyCachedResourceTags() { + // Collect tag ops from all relevant stages + const cachedProjectTagOps = []; + const cachedBuildTagOps = []; + + for (let i = this.#lastTagCacheImportIndex + 1; i <= this.#currentStageReadIndex; i++) { + const projectTagOps = this.#stages[i].getCachedProjectTagOperations(); + if (projectTagOps) { + cachedProjectTagOps.push(projectTagOps); + } + const buildTagOps = this.#stages[i].getCachedBuildTagOperations(); + if (buildTagOps) { + cachedBuildTagOps.push(buildTagOps); + } + } + + this.#lastTagCacheImportIndex = this.#currentStageReadIndex; + + const projectTagOps = mergeMaps(...cachedProjectTagOps); + const buildTagOps = mergeMaps(...cachedBuildTagOps); + + if (projectTagOps.size) { + const projectTagCollection = this.#getProjectResourceTagCollection(); + for (const [resourcePath, tags] of projectTagOps.entries()) { + for (const [tag, value] of tags.entries()) { + projectTagCollection.setTag(resourcePath, tag, value); + } + } + } + if (buildTagOps.size) { + const buildTagCollection = this.#getBuildResourceTagCollection(); + for (const [resourcePath, tags] of buildTagOps.entries()) { + for (const [tag, value] of tags.entries()) { + buildTagCollection.setTag(resourcePath, tag, value); + } + } + } + } +} + +const mergeMaps = (...maps) => { + const result = new Map(); + for (const map of maps) { + for (const [key, value] of map) { + result.set(key, value); + } + } + return result; +}; + +export default ProjectResources; diff --git a/packages/project/lib/resources/Stage.js b/packages/project/lib/resources/Stage.js new file mode 100644 index 00000000000..e0e6d4276ed --- /dev/null +++ b/packages/project/lib/resources/Stage.js @@ -0,0 +1,45 @@ +/** + * A stage has either a writer or a reader, never both. + * Consumers need to be able to differentiate between the two + */ +class Stage { + #id; + #writer; + #cachedWriter; + #cachedProjectTagOperations; + #cachedBuildTagOperations; + + constructor(id, writer, cachedWriter, cachedProjectTagOperations, cachedBuildTagOperations) { + if (writer && cachedWriter) { + throw new Error( + `Stage '${id}' cannot have both a writer and a cache reader`); + } + this.#id = id; + this.#writer = writer; + this.#cachedWriter = cachedWriter; + this.#cachedProjectTagOperations = cachedProjectTagOperations; + this.#cachedBuildTagOperations = cachedBuildTagOperations; + } + + getId() { + return this.#id; + } + + getWriter() { + return this.#writer; + } + + getCachedWriter() { + return this.#cachedWriter; + } + + getCachedProjectTagOperations() { + return this.#cachedProjectTagOperations; + } + + getCachedBuildTagOperations() { + return this.#cachedBuildTagOperations; + } +} + +export default Stage; diff --git a/packages/project/lib/specifications/ComponentProject.js b/packages/project/lib/specifications/ComponentProject.js index 337b3652e29..654c49c1b08 100644 --- a/packages/project/lib/specifications/ComponentProject.js +++ b/packages/project/lib/specifications/ComponentProject.js @@ -91,39 +91,7 @@ class ComponentProject extends Project { /* === Resource Access === */ - /** - * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the - * project in the specified "style": - * - *
    - *
  • buildtime: Resource paths are always prefixed with /resources/ - * or /test-resources/ followed by the project's namespace. - * Any configured build-excludes are applied
  • - *
  • dist: Resource paths always match with what the UI5 runtime expects. - * This means that paths generally depend on the project type. Applications for example use a "flat"-like - * structure, while libraries use a "buildtime"-like structure. - * Any configured build-excludes are applied
  • - *
  • runtime: Resource paths always match with what the UI5 runtime expects. - * This means that paths generally depend on the project type. Applications for example use a "flat"-like - * structure, while libraries use a "buildtime"-like structure. - * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
  • - *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that - * project types like "theme-library", which can have multiple namespaces, can't omit them. - * Any configured build-excludes are applied
  • - *
- * - * If project resources have been changed through the means of a workspace, those changes - * are reflected in the provided reader too. - * - * Resource readers always use POSIX-style paths. - * - * @public - * @param {object} [options] - * @param {string} [options.style=buildtime] Path style to access resources. - * Can be "buildtime", "dist", "runtime" or "flat" - * @returns {@ui5/fs/ReaderCollection} A reader collection instance - */ - getReader({style = "buildtime"} = {}) { + _getStyledReader(style) { // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? // Apply builder excludes to all styles but "runtime" @@ -161,7 +129,6 @@ class ComponentProject extends Project { throw new Error(`Unknown path mapping style ${style}`); } - reader = this._addWriter(reader, style); return reader; } @@ -183,52 +150,30 @@ class ComponentProject extends Project { throw new Error(`_getTestReader must be implemented by subclass ${this.constructor.name}`); } - /** - * Get a resource reader/writer for accessing and modifying a project's resources - * - * @public - * @returns {@ui5/fs/ReaderCollection} A reader collection instance - */ - getWorkspace() { - // Workspace is always of style "buildtime" - // Therefore builder resource-excludes are always to be applied - const excludes = this.getBuilderResourcesExcludes(); - return resourceFactory.createWorkspace({ - name: `Workspace for project ${this.getName()}`, - reader: this._getReader(excludes), - writer: this._getWriter().collection + _createWriter(stageId) { + // writer is always of style "buildtime" + const namespaceWriter = resourceFactory.createAdapter({ + name: `Namespace writer for project ${this.getName()}, stage ${stageId}`, + virBasePath: "/", + project: this }); - } - - _getWriter() { - if (!this._writers) { - // writer is always of style "buildtime" - const namespaceWriter = resourceFactory.createAdapter({ - virBasePath: "/", - project: this - }); - const generalWriter = resourceFactory.createAdapter({ - virBasePath: "/", - project: this - }); + const generalWriter = resourceFactory.createAdapter({ + name: `General writer for project ${this.getName()}, stage ${stageId}`, + virBasePath: "/", + project: this + }); - const collection = resourceFactory.createWriterCollection({ - name: `Writers for project ${this.getName()}`, - writerMapping: { - [`/resources/${this._namespace}/`]: namespaceWriter, - [`/test-resources/${this._namespace}/`]: namespaceWriter, - [`/`]: generalWriter - } - }); + const collection = resourceFactory.createWriterCollection({ + name: `Writers for project ${this.getName()}, stage ${stageId}`, + writerMapping: { + [`/resources/${this._namespace}/`]: namespaceWriter, + [`/test-resources/${this._namespace}/`]: namespaceWriter, + [`/`]: generalWriter + } + }); - this._writers = { - namespaceWriter, - generalWriter, - collection - }; - } - return this._writers; + return collection; } _getReader(excludes) { @@ -243,15 +188,31 @@ class ComponentProject extends Project { return reader; } - _addWriter(reader, style) { - const {namespaceWriter, generalWriter} = this._getWriter(); - + _addReadersForWriter(readers, writer, style) { if ((style === "runtime" || style === "dist") && this._isRuntimeNamespaced) { // If the project's type requires a namespace at runtime, the // dist- and runtime-style paths are identical to buildtime-style paths style = "buildtime"; } - const readers = []; + + let generalWriter; + let namespaceWriter; + if (writer.getMapping) { + const mapping = writer.getMapping(); + generalWriter = mapping[`/`]; + for (const writer of Object.values(mapping)) { + if (writer === generalWriter) { + continue; + } + if (namespaceWriter && writer !== namespaceWriter) { + throw new Error(`Cannot determine unique namespace writer for project ${this.getName()}`); + } + namespaceWriter = writer; + } + } else { + throw new Error(`Cannot determine writers for project ${this.getName()}`); + } + switch (style) { case "buildtime": // Writer already uses buildtime style @@ -265,8 +226,10 @@ class ComponentProject extends Project { reader: namespaceWriter, namespace: this._namespace })); - // Add general writer as is - readers.push(generalWriter); + // Add general writer only if it differs to prevent duplicate entries (with and without namespace) + if (namespaceWriter !== generalWriter) { + readers.push(generalWriter); + } break; case "flat": // Rewrite paths from "flat" to "buildtime" @@ -279,12 +242,6 @@ class ComponentProject extends Project { default: throw new Error(`Unknown path mapping style ${style}`); } - readers.push(reader); - - return resourceFactory.createReaderCollectionPrioritized({ - name: `Reader/Writer collection for project ${this.getName()}`, - readers - }); } /* === Internals === */ diff --git a/packages/project/lib/specifications/Project.js b/packages/project/lib/specifications/Project.js index 98cbf29da1d..97c6703231a 100644 --- a/packages/project/lib/specifications/Project.js +++ b/packages/project/lib/specifications/Project.js @@ -1,5 +1,5 @@ import Specification from "./Specification.js"; -import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; +import ProjectResources from "../resources/ProjectResources.js"; /** * Project @@ -12,6 +12,8 @@ import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; * @hideconstructor */ class Project extends Specification { + #projectResources; + constructor(parameters) { super(parameters); if (new.target === Project) { @@ -37,6 +39,15 @@ class Project extends Specification { await this._configureAndValidatePaths(this._config); await this._parseConfiguration(this._config, this._buildManifest); + // Initialize ProjectResources with interface callbacks + this.#projectResources = new ProjectResources({ + getName: () => this.getName(), + getStyledReader: (style) => this._getStyledReader(style), + createWriter: (stageId) => this._createWriter(stageId), + addReadersForWriter: (readers, writer, style) => this._addReadersForWriter(readers, writer, style), + buildManifest: this._buildManifest + }); + return this; } @@ -87,6 +98,14 @@ class Project extends Specification { throw new Error(`getSourcePath must be implemented by subclass ${this.constructor.name}`); } + getSourcePaths() { + throw new Error(`getSourcePaths must be implemented by subclass ${this.constructor.name}`); + } + + getVirtualPath() { + throw new Error(`getVirtualPath must be implemented by subclass ${this.constructor.name}`); + } + /** * Get the project's framework name configuration * @@ -220,6 +239,17 @@ class Project extends Specification { } /* === Resource Access === */ + + /** + * Get the ProjectResources instance for this project. + * + * @public + * @returns {@ui5/project/resources/ProjectResources} The ProjectResources instance + */ + getProjectResources() { + return this.#projectResources; + } + /** * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the * project in the specified "style": @@ -241,38 +271,74 @@ class Project extends Specification { * Any configured build-excludes are applied * * + * If project resources have been changed through the means of a workspace, those changes + * are reflected in the provided reader too. + * * Resource readers always use POSIX-style paths. * * @public * @param {object} [options] * @param {string} [options.style=buildtime] Path style to access resources. * Can be "buildtime", "dist", "runtime" or "flat" - * @returns {@ui5/fs/ReaderCollection} Reader collection allowing access to all resources of the project + * @returns {@ui5/fs/ReaderCollection} A reader collection instance */ getReader(options) { - throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`); + return this.#projectResources.getReader(options); } - getResourceTagCollection() { - if (!this._resourceTagCollection) { - this._resourceTagCollection = new ResourceTagCollection({ - allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], - allowedNamespaces: ["project"], - tags: this.getBuildManifest()?.tags - }); - } - return this._resourceTagCollection; + /** + * Get the source reader for the project. + * + * @public + * @param {string} [style=buildtime] Path style to access resources + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getSourceReader(style = "buildtime") { + return this.#projectResources.getSourceReader(style); } /** * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a * project's resources. This is always of style buildtime. * + * Once a project has finished building, this method will throw to prevent further modifications + * since those would have no effect. Use the getReader method to access the project's (modified) resources + * * @public * @returns {@ui5/fs/DuplexCollection} DuplexCollection */ getWorkspace() { - throw new Error(`getWorkspace must be implemented by subclass ${this.constructor.name}`); + return this.#projectResources.getWorkspace(); + } + + /* Overwritten in ComponentProject subclass */ + _addReadersForWriter(readers, writer, style) { + readers.unshift(writer); + } + + /** + * Gets the appropriate resource tag collection for a resource and tag + * + * Determines which tag collection (project-specific or build-level) should be used + * for the given resource and tag combination. Associates the resource with the current + * project if not already associated. + * + * @param {@ui5/fs/Resource} resource Resource to get tag collection for + * @param {string} tag Tag to check acceptance for + * @returns {@ui5/fs/internal/ResourceTagCollection} Appropriate tag collection + * @throws {Error} If no collection accepts the given tag + */ + getResourceTagCollection(resource, tag) { + return this.#projectResources.getResourceTagCollection(resource, tag); + } + + /** + * Returns the project-level resource tag collection + * + * @returns {@ui5/fs/internal/ResourceTagCollection} The project-level resource tag collection + */ + getProjectResourceTagCollection() { + return this.#projectResources.getProjectResourceTagCollection(); } /* === Internals === */ diff --git a/packages/project/lib/specifications/Specification.js b/packages/project/lib/specifications/Specification.js index 02bdc58036e..212aa37ecdd 100644 --- a/packages/project/lib/specifications/Specification.js +++ b/packages/project/lib/specifications/Specification.js @@ -161,6 +161,10 @@ class Specification { } /* === Attributes === */ + getConfig() { + return this._config; + } + /** * Gets the ID of this specification. * diff --git a/packages/project/lib/specifications/extensions/Task.js b/packages/project/lib/specifications/extensions/Task.js index c5aee60b7a0..9964dbe43f3 100644 --- a/packages/project/lib/specifications/extensions/Task.js +++ b/packages/project/lib/specifications/extensions/Task.js @@ -31,6 +31,27 @@ class Task extends Extension { return (await this._getImplementation()).determineRequiredDependencies; } + /** + * @public + */ + async getBuildSignatureCallback() { + return (await this._getImplementation()).determineBuildSignature; + } + + /** + * @public + */ + async getSupportsDifferentialBuildsCallback() { + return (await this._getImplementation()).supportsDifferentialBuilds; + } + + /** + * @public + */ + async getExpectedOutputCallback() { + return (await this._getImplementation()).determineExpectedOutput; + } + /* === Internals === */ /** * @private diff --git a/packages/project/lib/specifications/types/Application.js b/packages/project/lib/specifications/types/Application.js index 1dc17b4bc1c..44f39b4ef6d 100644 --- a/packages/project/lib/specifications/types/Application.js +++ b/packages/project/lib/specifications/types/Application.js @@ -45,6 +45,21 @@ class Application extends ComponentProject { return fsPath.join(this.getRootPath(), this._webappPath); } + getSourcePaths() { + return [this.getSourcePath()]; + } + + getVirtualPath(sourceFilePath) { + const sourcePath = this.getSourcePath(); + if (sourceFilePath.startsWith(sourcePath)) { + const relSourceFilePath = fsPath.relative(sourcePath, sourceFilePath); + return `/resources/${this._namespace}/${relSourceFilePath}`; + } + + throw new Error( + `Unable to convert source path ${sourceFilePath} to virtual path for project ${this.getName()}`); + } + /* === Resource Access === */ /** * Get a resource reader for the sources of the project (excluding any test resources) @@ -107,13 +122,13 @@ class Application extends ComponentProject { /** * @private * @param {object} config Configuration object - * @param {object} buildDescription Cache metadata object + * @param {object} buildManifest Cache metadata object */ - async _parseConfiguration(config, buildDescription) { - await super._parseConfiguration(config, buildDescription); + async _parseConfiguration(config, buildManifest) { + await super._parseConfiguration(config, buildManifest); - if (buildDescription) { - this._namespace = buildDescription.namespace; + if (buildManifest) { + this._namespace = buildManifest.namespace; return; } this._namespace = await this._getNamespace(); diff --git a/packages/project/lib/specifications/types/Component.js b/packages/project/lib/specifications/types/Component.js index 8ca5b94df26..54a19df7f51 100644 --- a/packages/project/lib/specifications/types/Component.js +++ b/packages/project/lib/specifications/types/Component.js @@ -165,13 +165,13 @@ class Component extends ComponentProject { /** * @private * @param {object} config Configuration object - * @param {object} buildDescription Cache metadata object + * @param {object} buildManifest Cache metadata object */ - async _parseConfiguration(config, buildDescription) { - await super._parseConfiguration(config, buildDescription); + async _parseConfiguration(config, buildManifest) { + await super._parseConfiguration(config, buildManifest); - if (buildDescription) { - this._namespace = buildDescription.namespace; + if (buildManifest) { + this._namespace = buildManifest.namespace; return; } this._namespace = await this._getNamespace(); diff --git a/packages/project/lib/specifications/types/Library.js b/packages/project/lib/specifications/types/Library.js index d3d2059a055..e118f39e6b6 100644 --- a/packages/project/lib/specifications/types/Library.js +++ b/packages/project/lib/specifications/types/Library.js @@ -56,6 +56,39 @@ class Library extends ComponentProject { return fsPath.join(this.getRootPath(), this._srcPath); } + getSourcePaths() { + const paths = [this.getSourcePath()]; + if (this._testPathExists) { + paths.push(fsPath.join(this.getRootPath(), this._testPath)); + } + return paths; + } + + getVirtualPath(sourceFilePath) { + const sourcePath = this.getSourcePath(); + if (sourceFilePath.startsWith(sourcePath)) { + const relSourceFilePath = fsPath.relative(sourcePath, sourceFilePath); + let virBasePath = "/resources/"; + if (!this._isSourceNamespaced) { + virBasePath += `${this._namespace}/`; + } + return posixPath.join(virBasePath, relSourceFilePath); + } + + const testPath = fsPath.join(this.getRootPath(), this._testPath); + if (sourceFilePath.startsWith(testPath)) { + const relSourceFilePath = fsPath.relative(testPath, sourceFilePath); + let virBasePath = "/test-resources/"; + if (!this._isSourceNamespaced) { + virBasePath += `${this._namespace}/`; + } + return posixPath.join(virBasePath, relSourceFilePath); + } + + throw new Error( + `Unable to convert source path ${sourceFilePath} to virtual path for project ${this.getName()}`); + } + /* === Resource Access === */ /** * Get a resource reader for the sources of the project (excluding any test resources) @@ -156,13 +189,13 @@ class Library extends ComponentProject { /** * @private * @param {object} config Configuration object - * @param {object} buildDescription Cache metadata object + * @param {object} buildManifest Cache metadata object */ - async _parseConfiguration(config, buildDescription) { - await super._parseConfiguration(config, buildDescription); + async _parseConfiguration(config, buildManifest) { + await super._parseConfiguration(config, buildManifest); - if (buildDescription) { - this._namespace = buildDescription.namespace; + if (buildManifest) { + this._namespace = buildManifest.namespace; return; } diff --git a/packages/project/lib/specifications/types/Module.js b/packages/project/lib/specifications/types/Module.js index 69c5987c9d8..201dccfe130 100644 --- a/packages/project/lib/specifications/types/Module.js +++ b/packages/project/lib/specifications/types/Module.js @@ -16,7 +16,6 @@ class Module extends Project { super(parameters); this._paths = null; - this._writer = null; } /* === Attributes === */ @@ -31,44 +30,39 @@ class Module extends Project { throw new Error(`Projects of type module have more than one source path`); } + getSourcePaths() { + return this._paths.map(({fsBasePath}) => { + return fsBasePath; + }); + } + /* === Resource Access === */ - /** - * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the - * project in the specified "style": - * - *
    - *
  • buildtime: Resource paths are always prefixed with /resources/ - * or /test-resources/ followed by the project's namespace. - * Any configured build-excludes are applied
  • - *
  • dist: Resource paths always match with what the UI5 runtime expects. - * This means that paths generally depend on the project type. Applications for example use a "flat"-like - * structure, while libraries use a "buildtime"-like structure. - * Any configured build-excludes are applied
  • - *
  • runtime: Resource paths always match with what the UI5 runtime expects. - * This means that paths generally depend on the project type. Applications for example use a "flat"-like - * structure, while libraries use a "buildtime"-like structure. - * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
  • - *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that - * project types like "theme-library", which can have multiple namespaces, can't omit them. - * Any configured build-excludes are applied
  • - *
- * - * If project resources have been changed through the means of a workspace, those changes - * are reflected in the provided reader too. - * - * Resource readers always use POSIX-style paths. - * - * @public - * @param {object} [options] - * @param {string} [options.style=buildtime] Path style to access resources. - * Can be "buildtime", "dist", "runtime" or "flat" - * @returns {@ui5/fs/ReaderCollection} A reader collection instance - */ - getReader({style = "buildtime"} = {}) { + _getStyledReader(style) { // Apply builder excludes to all styles but "runtime" const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes(); + return this._getReader(excludes); + } + + // /** + // * Get a resource reader/writer for accessing and modifying a project's resources + // * + // * @public + // * @returns {@ui5/fs/ReaderCollection} A reader collection instance + // */ + // getWorkspace() { + // const excludes = this.getBuilderResourcesExcludes(); + // const reader = this._getReader(excludes); + + // const writer = this._createWriter(); + // return resourceFactory.createWorkspace({ + // reader, + // writer + // }); + // } + + _getReader(excludes) { const readers = this._paths.map(({name, virBasePath, fsBasePath}) => { return resourceFactory.createReader({ name, @@ -81,42 +75,18 @@ class Module extends Project { if (readers.length === 1) { return readers[0]; } - const readerCollection = resourceFactory.createReaderCollection({ + return resourceFactory.createReaderCollection({ name: `Reader collection for module project ${this.getName()}`, readers }); - return resourceFactory.createReaderCollectionPrioritized({ - name: `Reader/Writer collection for project ${this.getName()}`, - readers: [this._getWriter(), readerCollection] - }); } - /** - * Get a resource reader/writer for accessing and modifying a project's resources - * - * @public - * @returns {@ui5/fs/ReaderCollection} A reader collection instance - */ - getWorkspace() { - const reader = this.getReader(); - - const writer = this._getWriter(); - return resourceFactory.createWorkspace({ - reader, - writer + _createWriter() { + return resourceFactory.createAdapter({ + virBasePath: "/" }); } - _getWriter() { - if (!this._writer) { - this._writer = resourceFactory.createAdapter({ - virBasePath: "/" - }); - } - - return this._writer; - } - /* === Internals === */ /** * @private diff --git a/packages/project/lib/specifications/types/ThemeLibrary.js b/packages/project/lib/specifications/types/ThemeLibrary.js index 398e570cdfb..c9c00dc6cea 100644 --- a/packages/project/lib/specifications/types/ThemeLibrary.js +++ b/packages/project/lib/specifications/types/ThemeLibrary.js @@ -18,7 +18,6 @@ class ThemeLibrary extends Project { this._srcPath = "src"; this._testPath = "test"; this._testPathExists = false; - this._writer = null; } /* === Attributes === */ @@ -39,43 +38,52 @@ class ThemeLibrary extends Project { return fsPath.join(this.getRootPath(), this._srcPath); } + getSourcePaths() { + return [this.getSourcePath()]; + } + + getVirtualPath(sourceFilePath) { + const sourcePath = this.getSourcePath(); + if (sourceFilePath.startsWith(sourcePath)) { + const relSourceFilePath = fsPath.relative(sourcePath, sourceFilePath); + return `/resources/${relSourceFilePath}`; + } + + throw new Error( + `Unable to convert source path ${sourceFilePath} to virtual path for project ${this.getName()}`); + } + /* === Resource Access === */ - /** - * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the - * project in the specified "style": - * - *
    - *
  • buildtime: Resource paths are always prefixed with /resources/ - * or /test-resources/ followed by the project's namespace. - * Any configured build-excludes are applied
  • - *
  • dist: Resource paths always match with what the UI5 runtime expects. - * This means that paths generally depend on the project type. Applications for example use a "flat"-like - * structure, while libraries use a "buildtime"-like structure. - * Any configured build-excludes are applied
  • - *
  • runtime: Resource paths always match with what the UI5 runtime expects. - * This means that paths generally depend on the project type. Applications for example use a "flat"-like - * structure, while libraries use a "buildtime"-like structure. - * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
  • - *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that - * project types like "theme-library", which can have multiple namespaces, can't omit them. - * Any configured build-excludes are applied
  • - *
- * - * If project resources have been changed through the means of a workspace, those changes - * are reflected in the provided reader too. - * - * Resource readers always use POSIX-style paths. - * - * @public - * @param {object} [options] - * @param {string} [options.style=buildtime] Path style to access resources. - * Can be "buildtime", "dist", "runtime" or "flat" - * @returns {@ui5/fs/ReaderCollection} A reader collection instance - */ - getReader({style = "buildtime"} = {}) { + + _getStyledReader(style) { // Apply builder excludes to all styles but "runtime" const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes(); + return this._getReader(excludes); + } + + // /** + // * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a + // * project's resources. + // * + // * This is always of style buildtime, which for theme libraries is identical to style + // * runtime. + // * + // * @public + // * @returns {@ui5/fs/DuplexCollection} DuplexCollection + // */ + // getWorkspace() { + // const excludes = this.getBuilderResourcesExcludes(); + // const reader = this._getReader(excludes); + + // const writer = this._createWriter(); + // return resourceFactory.createWorkspace({ + // reader, + // writer + // }); + // } + + _getReader(excludes) { let reader = resourceFactory.createReader({ fsBasePath: this.getSourcePath(), virBasePath: "/resources/", @@ -96,45 +104,16 @@ class ThemeLibrary extends Project { readers: [reader, testReader] }); } - const writer = this._getWriter(); - - return resourceFactory.createReaderCollectionPrioritized({ - name: `Reader/Writer collection for project ${this.getName()}`, - readers: [writer, reader] - }); + return reader; } - /** - * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a - * project's resources. - * - * This is always of style buildtime, wich for theme libraries is identical to style - * runtime. - * - * @public - * @returns {@ui5/fs/DuplexCollection} DuplexCollection - */ - getWorkspace() { - const reader = this.getReader(); - - const writer = this._getWriter(); - return resourceFactory.createWorkspace({ - reader, - writer + _createWriter() { + return resourceFactory.createAdapter({ + virBasePath: "/", + project: this }); } - _getWriter() { - if (!this._writer) { - this._writer = resourceFactory.createAdapter({ - virBasePath: "/", - project: this - }); - } - - return this._writer; - } - /* === Internals === */ /** * @private diff --git a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js index 7002bddbd27..01c4b843541 100644 --- a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js +++ b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js @@ -34,21 +34,21 @@ class Sapui5MavenSnapshotResolver extends AbstractResolver { * @param {string} [options.cwd=process.cwd()] Current working directory * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, * metadata and configuration used by the resolvers. Relative to `process.cwd()` - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode=Default] - * Cache mode to use + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache=Default] + * Snapshot cache mode to use */ constructor(options) { super(options); const { - cacheMode, + snapshotCache, } = options; this._installer = new Installer({ ui5DataDir: this._ui5DataDir, snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(options.snapshotEndpointUrl), - cacheMode, + snapshotCache, }); this._loadDistMetadata = null; diff --git a/packages/project/lib/ui5Framework/maven/Installer.js b/packages/project/lib/ui5Framework/maven/Installer.js index 4dd6d1bc8cd..2c8e45fb7f6 100644 --- a/packages/project/lib/ui5Framework/maven/Installer.js +++ b/packages/project/lib/ui5Framework/maven/Installer.js @@ -6,7 +6,7 @@ const StreamZip = _StreamZip.async; import {promisify} from "node:util"; import Registry from "./Registry.js"; import AbstractInstaller from "../AbstractInstaller.js"; -import CacheMode from "./CacheMode.js"; +import SnapshotCache from "./SnapshotCache.js"; import {rmrf} from "../../utils/fs.js"; const stat = promisify(fs.stat); const readFile = promisify(fs.readFile); @@ -27,9 +27,10 @@ class Installer extends AbstractInstaller { * @param {Function} parameters.snapshotEndpointUrlCb Callback that returns a Promise , * resolving to the Maven repository URL. * Example: https://registry.corp/vendor/build-snapshots/ - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [parameters.cacheMode=Default] Cache mode to use + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [parameters.snapshotCache=Default] + * Snapshot cache mode to use */ - constructor({ui5DataDir, snapshotEndpointUrlCb, cacheMode = CacheMode.Default}) { + constructor({ui5DataDir, snapshotEndpointUrlCb, snapshotCache = SnapshotCache.Default}) { super(ui5DataDir); this._artifactsDir = path.join(ui5DataDir, "framework", "artifacts"); @@ -37,20 +38,20 @@ class Installer extends AbstractInstaller { this._metadataDir = path.join(ui5DataDir, "framework", "metadata"); this._stagingDir = path.join(ui5DataDir, "framework", "staging"); - this._cacheMode = cacheMode; + this._snapshotCache = snapshotCache; this._snapshotEndpointUrlCb = snapshotEndpointUrlCb; if (!this._snapshotEndpointUrlCb) { throw new Error(`Installer: Missing Snapshot-Endpoint URL callback parameter`); } - if (!Object.values(CacheMode).includes(cacheMode)) { - throw new Error(`Installer: Invalid value '${cacheMode}' for cacheMode parameter. ` + - `Must be one of ${Object.values(CacheMode).join(", ")}`); + if (!Object.values(SnapshotCache).includes(snapshotCache)) { + throw new Error(`Installer: Invalid value '${snapshotCache}' for snapshotCache parameter. ` + + `Must be one of ${Object.values(SnapshotCache).join(", ")}`); } log.verbose(`Installing Maven artifacts to: ${this._artifactsDir}`); log.verbose(`Installing Packages to: ${this._packagesDir}`); - log.verbose(`Caching mode: ${this._cacheMode}`); + log.verbose(`Snapshot cache mode: ${this._snapshotCache}`); } async getRegistry() { @@ -122,7 +123,7 @@ class Installer extends AbstractInstaller { return this._synchronize("metadata-" + fsId, async () => { const localMetadata = await this._getLocalArtifactMetadata(fsId); - if (this._cacheMode === CacheMode.Force && !localMetadata.revision) { + if (this._snapshotCache === SnapshotCache.Force && !localMetadata.revision) { throw new Error(`Could not find artifact ` + `${logId} in local cache`); } @@ -130,8 +131,8 @@ class Installer extends AbstractInstaller { const now = new Date().getTime(); const timeSinceLastCheck = now - localMetadata.lastCheck; - if (this._cacheMode !== CacheMode.Force && - (timeSinceLastCheck > CACHE_TIME || this._cacheMode === CacheMode.Off)) { + if (this._snapshotCache !== SnapshotCache.Force && + (timeSinceLastCheck > CACHE_TIME || this._snapshotCache === SnapshotCache.Off)) { // No cached metadata (-> timeSinceLastCheck equals time since 1970) or // too old metadata or disabled cache // => Retrieve metadata from repository diff --git a/packages/project/lib/ui5Framework/maven/Registry.js b/packages/project/lib/ui5Framework/maven/Registry.js index ec5b2293e75..7003c49b230 100644 --- a/packages/project/lib/ui5Framework/maven/Registry.js +++ b/packages/project/lib/ui5Framework/maven/Registry.js @@ -65,8 +65,8 @@ class Registry { `You can change the configured URL using the following command: ` + `'ui5 config set mavenSnapshotEndpointUrl '`); - // TODO: Allow cacheMode to be set from outside - // `You may be able to continue working offline. For this, set --cache-mode to "force"`); + // TODO: Allow snapshotCache to be set from outside + // `You may be able to continue working offline. For this, set --snapshot-cache to "force"`); // ` or use the --offline flag`); // TODO: Implement --offline flag } throw new Error( @@ -108,8 +108,8 @@ class Registry { `You can change the configured URL using the following command: ` + `'ui5 config set mavenSnapshotEndpointUrl '`); - // TODO: Allow cacheMode to be set from outside - // `You may be able to continue working offline. For this, set --cache-mode to "force"`); + // TODO: Allow snapshotCache to be set from outside + // `You may be able to continue working offline. For this, set --snapshot-cache to "force"`); // ` or use the --offline flag`); // TODO: Implement --offline flag } throw new Error(`Failed to retrieve artifact ` + diff --git a/packages/project/lib/ui5Framework/maven/CacheMode.js b/packages/project/lib/ui5Framework/maven/SnapshotCache.js similarity index 77% rename from packages/project/lib/ui5Framework/maven/CacheMode.js rename to packages/project/lib/ui5Framework/maven/SnapshotCache.js index d1b5af0d422..5e7927090f6 100644 --- a/packages/project/lib/ui5Framework/maven/CacheMode.js +++ b/packages/project/lib/ui5Framework/maven/SnapshotCache.js @@ -1,7 +1,7 @@ /** - * Cache modes for maven consumption + * Snapshot cache modes for Maven consumption * * @public * @readonly @@ -9,7 +9,7 @@ * @property {string} Default Cache everything, invalidate after 9 hours * @property {string} Force Use cache only. Do not send any requests to the repository * @property {string} Off Invalidate the cache and update from the repository - * @module @ui5/project/ui5Framework/maven/CacheMode + * @module @ui5/project/ui5Framework/maven/SnapshotCache */ export default { Default: "Default", diff --git a/packages/project/lib/utils/sanitizeFileName.js b/packages/project/lib/utils/sanitizeFileName.js new file mode 100644 index 00000000000..8950705de2c --- /dev/null +++ b/packages/project/lib/utils/sanitizeFileName.js @@ -0,0 +1,44 @@ +import path from "node:path"; + +const forbiddenCharsRegex = /[^0-9a-zA-Z\-._]/g; +const windowsReservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i; + +/** + * Sanitize a file name by replacing any characters not matching the allowed set with a dash. + * Additionally validate that the file name to make sure it is safe to use on various file systems. + * + * @param {string} fileName The file name to validate + * @returns {string} The sanitized file name + * @throws {Error} If the file name is empty, starts with a dot, contains a reserved value, or is too long + */ +export default function sanitizeFileName(fileName) { + if (!fileName) { + throw new Error("Illegal empty file name"); + } + if (fileName.startsWith(".")) { + throw new Error(`Illegal file name starting with a dot: ${fileName}`); + } + fileName = fileName.replaceAll(forbiddenCharsRegex, "-"); + + if (fileName.length > 255) { + throw new Error(`Illegal file name exceeding maximum length of 255 characters: ${fileName}`); + } + + if (windowsReservedNames.test(fileName)) { + throw new Error(`Illegal file name reserved on Windows systems: ${fileName}`); + } + + return fileName; +} + +export function getPathFromPackageName(pkgName) { + // If pkgName starts with a scope, that becomes a folder + if (pkgName.startsWith("@") && pkgName.includes("/")) { + // Split at first slash to get the scope and sanitize it without the "@" + const scope = sanitizeFileName(pkgName.substring(1, pkgName.indexOf("/"))); + // Get the rest of the package name + const pkg = pkgName.substring(pkgName.indexOf("/") + 1); + return path.join(`@${sanitizeFileName(scope)}`, sanitizeFileName(pkg)); + } + return sanitizeFileName(pkgName); +} diff --git a/packages/project/package.json b/packages/project/package.json index a4c6a9f61cc..22f9a8ce37f 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -19,12 +19,13 @@ "type": "module", "exports": { "./config/Configuration": "./lib/config/Configuration.js", + "./build/cache/Cache": "./lib/build/cache/Cache.js", "./specifications/Specification": "./lib/specifications/Specification.js", "./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js", "./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js", "./ui5Framework/Openui5Resolver": "./lib/ui5Framework/Openui5Resolver.js", "./ui5Framework/Sapui5Resolver": "./lib/ui5Framework/Sapui5Resolver.js", - "./ui5Framework/maven/CacheMode": "./lib/ui5Framework/maven/CacheMode.js", + "./ui5Framework/maven/SnapshotCache": "./lib/ui5Framework/maven/SnapshotCache.js", "./validation/validator": "./lib/validation/validator.js", "./validation/ValidationError": "./lib/validation/ValidationError.js", "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js", @@ -62,12 +63,14 @@ "ajv": "^8.18.0", "ajv-errors": "^3.0.0", "chalk": "^5.6.2", + "chokidar": "^3.6.0", "escape-string-regexp": "^5.0.0", "globby": "^14.1.0", "graceful-fs": "^4.2.11", "js-yaml": "^4.1.1", "lockfile": "^1.0.4", "make-fetch-happen": "^15.0.5", + "micromatch": "^4.0.8", "node-stream-zip": "^1.15.0", "pacote": "^21.0.4", "pretty-hrtime": "^1.0.3", @@ -75,6 +78,7 @@ "read-pkg": "^10.0.0", "resolve": "^1.22.10", "semver": "^7.7.2", + "ssri": "^13.0.1", "xml2js": "^0.6.2", "yesno": "^0.4.0" }, diff --git a/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js new file mode 100644 index 00000000000..91ef3b68232 --- /dev/null +++ b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js @@ -0,0 +1,37 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:depTagReader"); + +let buildRanOnce; +module.exports = async function ({taskUtil, dependencies}) { + log.verbose("dep-tag-reader executed"); + + // const libraryDProject = taskUtil.getProject("library.d"); + const resources = await dependencies.byGlob("**/some.js"); + if (resources.length === 0) { + throw new Error("dep-tag-reader: some.js not found in library.d"); + } + const someJs = resources[0]; + + if (buildRanOnce !== true) { + log.verbose("First build: Verifying ui5:IsDebugVariant is set on some.js"); + buildRanOnce = true; + const tag = taskUtil.getTag(someJs, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (!tag) { + throw new Error( + "dep-tag-reader: Expected ui5:IsDebugVariant tag to be set on some.js in first build" + ); + } + } else { + log.verbose("Subsequent build: Verifying ui5:HasDebugVariant is set on some.js"); + const tag = taskUtil.getTag(someJs, taskUtil.STANDARD_TAGS.HasDebugVariant); + if (!tag) { + throw new Error( + "dep-tag-reader: Expected ui5:HasDebugVariant tag to be set on some.js in subsequent build" + ); + } + } +}; + +module.exports.determineRequiredDependencies = function ({availableDependencies}) { + return availableDependencies; +} diff --git a/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js new file mode 100644 index 00000000000..8d5b986dbd4 --- /dev/null +++ b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js @@ -0,0 +1,22 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:depTagSetter"); + +let buildRanOnce; +module.exports = async function ({workspace, taskUtil}) { + log.verbose("dep-tag-setter executed"); + + const resources = await workspace.byGlob("**/some.js"); + if (resources.length === 0) { + throw new Error("dep-tag-setter: some.js not found in workspace"); + } + const someJs = resources[0]; + + if (buildRanOnce !== true) { + log.verbose("First build: Setting ui5:IsDebugVariant on some.js"); + buildRanOnce = true; + taskUtil.setTag(someJs, taskUtil.STANDARD_TAGS.IsDebugVariant); + } else { + log.verbose("Subsequent build: Setting ui5:HasDebugVariant on some.js"); + taskUtil.setTag(someJs, taskUtil.STANDARD_TAGS.HasDebugVariant); + } +}; diff --git a/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js new file mode 100644 index 00000000000..78495298e3a --- /dev/null +++ b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js @@ -0,0 +1,22 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:customTask0"); + +let buildRanOnce; +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + log.verbose("Custom task 0 executed"); + + // Read a file to trigger execution of this task: + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + + if (buildRanOnce != true) { + log.verbose("Flag NOT set -> We are in #1 Build still"); + buildRanOnce = true; + taskUtil.setTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + } else { + log.verbose("Flag set -> We are in #2 Build"); + taskUtil.setTag(testJS, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } +}; diff --git a/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js new file mode 100644 index 00000000000..98f8b94c848 --- /dev/null +++ b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js @@ -0,0 +1,32 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:customTask1"); + +let buildRanOnce; +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + log.verbose("Custom task 1 executed"); + + // Read a file to trigger execution of this task: + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + + if (buildRanOnce != true) { + log.verbose("Flag NOT set -> We are in #1 Build still"); + buildRanOnce = true; + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (!tag) { + throw new Error("Tag set during #1 Build is not readable, which is UNEXPECTED."); + } + } else { + const previousTag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (previousTag) { + throw new Error("Tag set during #1 Build is still readable, which is UNEXPECTED."); + } + log.verbose("Flag set -> We are in #2 Build"); + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + if (!tag) { + throw new Error("Tag set during #2 Build is not readable, which is UNEXPECTED."); + } + } +}; diff --git a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js new file mode 100644 index 00000000000..c19f110113c --- /dev/null +++ b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js @@ -0,0 +1,22 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:customTask0"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + log.verbose("Custom task 0 executed"); + + // Read a file which is an input of custom-task-1 (which sets a tag on it): + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + // For #1 & #3 build: + if (tag) { + throw new Error("Tag set by custom-task-1 is present in custom-task-0, which is UNEXPECTED."); + } + + // For #3 build: Read a different file which is not an input of custom-task-1 + // (ensures that this task is executed): + const test2JS = await workspace.byPath(`/resources/${projectNamespace}/test2.js`); +}; diff --git a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js new file mode 100644 index 00000000000..58030f3ea06 --- /dev/null +++ b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js @@ -0,0 +1,15 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:customTask1"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + log.verbose("Custom task 1 executed"); + + // Set a tag on a specific resource: + const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + if (resource) { + taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + }; +}; diff --git a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js new file mode 100644 index 00000000000..de5a39068a2 --- /dev/null +++ b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js @@ -0,0 +1,22 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:customTask2"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + log.verbose("Custom task 2 executed"); + + // Read a file which is an input of custom-task-1 (which sets a tag on it): + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + // For #1 & #3 build: + if (!tag) { + throw new Error("Tag set by custom-task-1 is NOT present in custom-task-2, which is UNEXPECTED."); + } + + // For #3 build: Read a different file which is not an input of custom-task-1 + // (ensures that this task is executed): + const test2JS = await workspace.byPath(`/resources/${projectNamespace}/test2.js`); +}; diff --git a/packages/project/test/fixtures/application.a/dependency-race-condition-task.js b/packages/project/test/fixtures/application.a/dependency-race-condition-task.js new file mode 100644 index 00000000000..9fac2ae248d --- /dev/null +++ b/packages/project/test/fixtures/application.a/dependency-race-condition-task.js @@ -0,0 +1,61 @@ +const {readFile, writeFile} = require("fs/promises"); +const path = require("path"); + +/** + * Custom task that verifies the frozen CAS reader protects against + * cross-project dependency source race conditions. + * + * Uses data.json — an untransformed source file (no placeholders, not processed by + * minify/replaceCopyright/replaceVersion). This is critical because transformed files + * are written to stage writers which have higher priority than both the frozen reader + * and the filesystem reader, making disk modifications invisible regardless. + * + * Flow: + * 1. Read library.d's data.json via the dependency reader (CAS-backed) + * 2. Overwrite the file on disk with different content + * 3. Re-read via the dependency reader + * 4. Assert the content is still the original (CAS-served) + * 5. Restore the original file on disk + */ +module.exports = async function ({taskUtil}) { + const libDProject = taskUtil.getProject("library.d"); + const libDReader = libDProject.getReader(); + + // Step 1: Read the original content via the dependency reader (CAS-backed) + // data.json is untransformed (no placeholders, not a .js file subject to minification) + // so it is only served by the frozen CAS reader or the filesystem reader + const resourcePath = "/resources/library/d/data.json"; + const originalResource = await libDReader.byPath(resourcePath); + if (!originalResource) { + throw new Error(`Resource ${resourcePath} not found via dependency reader`); + } + const originalContent = await originalResource.getString(); + + // Step 2: Overwrite the file on disk + const sourcePath = libDProject.getSourcePath(); + const diskFilePath = path.join(sourcePath, "library", "d", "data.json"); + const diskOriginalContent = await readFile(diskFilePath, {encoding: "utf8"}); + await writeFile(diskFilePath, JSON.stringify({key: "modified-by-race-condition"})); + + try { + // Step 3: Re-read via the dependency reader — should still return CAS-frozen content + // Without the frozen reader, this would read the modified disk content + const reReadResource = await libDReader.byPath(resourcePath); + if (!reReadResource) { + throw new Error(`Resource ${resourcePath} not found on re-read via dependency reader`); + } + const reReadContent = await reReadResource.getString(); + + // Step 4: Assert the content is still the original (not modified disk content) + if (reReadContent !== originalContent) { + throw new Error( + "Frozen source reader protection failed: dependency reader returned modified disk content " + + "instead of the original CAS-frozen content. " + + `Expected: ${JSON.stringify(originalContent)}, Got: ${JSON.stringify(reReadContent)}` + ); + } + } finally { + // Step 5: Always restore the original file on disk + await writeFile(diskFilePath, diskOriginalContent); + } +}; diff --git a/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/data.json b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/data.json new file mode 100644 index 00000000000..9658000784b --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/data.json @@ -0,0 +1 @@ +{"key": "original-value"} diff --git a/packages/project/test/fixtures/application.a/race-condition-add-file-task.js b/packages/project/test/fixtures/application.a/race-condition-add-file-task.js new file mode 100644 index 00000000000..c4572b0aae1 --- /dev/null +++ b/packages/project/test/fixtures/application.a/race-condition-add-file-task.js @@ -0,0 +1,11 @@ +const {writeFile} = require("fs/promises"); +const path = require("path"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + const webappPath = taskUtil.getProject().getSourcePath(); + const addedFilePath = path.join(webappPath, "added-during-build.js"); + await writeFile(addedFilePath, `console.log("RACE CONDITION ADDED FILE");\n`); +}; diff --git a/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js b/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js new file mode 100644 index 00000000000..bad0440f7c4 --- /dev/null +++ b/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js @@ -0,0 +1,11 @@ +const {rm} = require("fs/promises"); +const path = require("path"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + const webappPath = taskUtil.getProject().getSourcePath(); + const deletedFilePath = path.join(webappPath, "test.js"); + await rm(deletedFilePath, {force: true}); +}; diff --git a/packages/project/test/fixtures/application.a/race-condition-task.js b/packages/project/test/fixtures/application.a/race-condition-task.js new file mode 100644 index 00000000000..e70ed51ce7f --- /dev/null +++ b/packages/project/test/fixtures/application.a/race-condition-task.js @@ -0,0 +1,13 @@ +const {readFile, writeFile} = require("fs/promises"); +const path = require("path"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + const webappPath = taskUtil.getProject().getSourcePath(); + // Modify source file during build + const testFilePath = path.join(webappPath, "test.js"); + const originalContent = await readFile(testFilePath, {encoding: "utf8"}); + await writeFile(testFilePath, originalContent + `\nconsole.log("RACE CONDITION MODIFICATION");\n`); +}; diff --git a/packages/project/test/fixtures/application.a/task.dependency-change.js b/packages/project/test/fixtures/application.a/task.dependency-change.js new file mode 100644 index 00000000000..118e74b1cb1 --- /dev/null +++ b/packages/project/test/fixtures/application.a/task.dependency-change.js @@ -0,0 +1,36 @@ +// This is a modified version of the compileLicenseSummary example of the UI5 CLI documentation. +// (https://github.com/UI5/cli/blob/b72919469d856508dd757ecf325a5fb45f15e56d/internal/documentation/docs/pages/extensibility/CustomTasks.md#example-libtaskscompilelicensesummaryjs) + +module.exports = async function ({log, taskUtil, workspace}) { + const {createResource} = taskUtil.resourceFactory; + const projectsVisited = new Set(); + + async function processProject() { + return Promise.all(taskUtil.getDependencies().map(async (projectName) => { + if (projectName !== "library.d") { + return; + } + if (projectsVisited.has(projectName)) { + return; + } + projectsVisited.add(projectName); + const project = taskUtil.getProject(projectName); + const newLibraryFile = await project.getReader().byGlob("**/newLibraryFile.js"); + if (newLibraryFile.length > 0) { + log.verbose('New Library file found. We are in #4 build.'); + // Change content of application.a: + const applicationResource = await workspace.byPath("/resources/id1/test.js"); + const content = (await applicationResource.getString()) + "\n console.log('something new');"; + await workspace.write(createResource({ + path: "/test.js", + string: content + })); + } else { + log.verbose(`New Library file not found. We are still in an earlier build.`); + } + return processProject(project); + })); + } + // Start processing dependencies of the root project + await processProject(taskUtil.getProject()); +}; diff --git a/packages/project/test/fixtures/application.a/task.example.js b/packages/project/test/fixtures/application.a/task.example.js new file mode 100644 index 00000000000..669c19c3fbf --- /dev/null +++ b/packages/project/test/fixtures/application.a/task.example.js @@ -0,0 +1,15 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:exampleTask"); + +module.exports = async function ({ + workspace, taskUtil, + options: {projectNamespace} +}) { + log.verbose("Example task executed"); + + // Omit a specific resource from the build result + const omittedResource = await workspace.byPath(`/resources/${projectNamespace}/fileToBeOmitted.js`); + if (omittedResource) { + taskUtil.setTag(omittedResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + }; +}; diff --git a/packages/project/test/fixtures/application.a/ui5-crossProject-tagChange.yaml b/packages/project/test/fixtures/application.a/ui5-crossProject-tagChange.yaml new file mode 100644 index 00000000000..595add44793 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-crossProject-tagChange.yaml @@ -0,0 +1,25 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: dep-tag-reader + afterTask: minify +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: dep-tag-setter +task: + path: cross-project-tag-tasks/dep-tag-setter.js +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: dep-tag-reader +task: + path: cross-project-tag-tasks/dep-tag-reader.js diff --git a/packages/project/test/fixtures/application.a/ui5-custom-bundling.yaml b/packages/project/test/fixtures/application.a/ui5-custom-bundling.yaml new file mode 100644 index 00000000000..f3c8a243a2b --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-custom-bundling.yaml @@ -0,0 +1,19 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + bundles: + - bundleDefinition: + name: "custom-bundle.js" + defaultFileTypes: + - ".js" + - ".json" + sections: + - mode: preload + name: "customBundle" + filters: + - "id1/Component.js" + - "id1/newFile.js" + resolve: false diff --git a/packages/project/test/fixtures/application.a/ui5-custom-preload-config.yaml b/packages/project/test/fixtures/application.a/ui5-custom-preload-config.yaml new file mode 100644 index 00000000000..41a5a497fe4 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-custom-preload-config.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + componentPreload: + namespaces: + - "id1" + excludes: + - "id1/thirdparty/scriptWithSourceMap.js" \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/ui5-customTask-dependency-change.yaml b/packages/project/test/fixtures/application.a/ui5-customTask-dependency-change.yaml new file mode 100644 index 00000000000..fa7743f34bf --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-customTask-dependency-change.yaml @@ -0,0 +1,18 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: dependency-change + afterTask: minify +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: dependency-change +task: + path: task.dependency-change.js + diff --git a/packages/project/test/fixtures/application.a/ui5-customTask.yaml b/packages/project/test/fixtures/application.a/ui5-customTask.yaml new file mode 100644 index 00000000000..3c44bbf65c7 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-customTask.yaml @@ -0,0 +1,17 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: example-task + afterTask: minify +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: example-task +task: + path: task.example.js diff --git a/packages/project/test/fixtures/application.a/ui5-dependency-race-condition.yaml b/packages/project/test/fixtures/application.a/ui5-dependency-race-condition.yaml new file mode 100644 index 00000000000..02ed3896358 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-dependency-race-condition.yaml @@ -0,0 +1,17 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: dependency-race-condition-task + afterTask: escapeNonAsciiCharacters +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: dependency-race-condition-task +task: + path: dependency-race-condition-task.js diff --git a/packages/project/test/fixtures/application.a/ui5-multiple-customTasks-2.yaml b/packages/project/test/fixtures/application.a/ui5-multiple-customTasks-2.yaml new file mode 100644 index 00000000000..0e8f71305b2 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-multiple-customTasks-2.yaml @@ -0,0 +1,27 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: custom-task-0 + afterTask: minify + - name: custom-task-1 + afterTask: custom-task-0 +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: custom-task-0 +task: + path: custom-tasks-2/custom-task-0.js +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: custom-task-1 +task: + path: custom-tasks-2/custom-task-1.js \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/ui5-multiple-customTasks.yaml b/packages/project/test/fixtures/application.a/ui5-multiple-customTasks.yaml new file mode 100644 index 00000000000..bb99285b85c --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-multiple-customTasks.yaml @@ -0,0 +1,37 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: custom-task-0 + afterTask: minify + - name: custom-task-1 + afterTask: custom-task-0 + - name: custom-task-2 + afterTask: custom-task-1 +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: custom-task-0 +task: + path: custom-tasks/custom-task-0.js +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: custom-task-1 +task: + path: custom-tasks/custom-task-1.js +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: custom-task-2 +task: + path: custom-tasks/custom-task-2.js \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/ui5-race-condition-add-file.yaml b/packages/project/test/fixtures/application.a/ui5-race-condition-add-file.yaml new file mode 100644 index 00000000000..a222c8d80fd --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-race-condition-add-file.yaml @@ -0,0 +1,17 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: race-condition-add-file-task + afterTask: escapeNonAsciiCharacters +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: race-condition-add-file-task +task: + path: race-condition-add-file-task.js diff --git a/packages/project/test/fixtures/application.a/ui5-race-condition-delete-file.yaml b/packages/project/test/fixtures/application.a/ui5-race-condition-delete-file.yaml new file mode 100644 index 00000000000..18b27971769 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-race-condition-delete-file.yaml @@ -0,0 +1,17 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: race-condition-delete-file-task + afterTask: escapeNonAsciiCharacters +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: race-condition-delete-file-task +task: + path: race-condition-delete-file-task.js diff --git a/packages/project/test/fixtures/application.a/ui5-race-condition.yaml b/packages/project/test/fixtures/application.a/ui5-race-condition.yaml new file mode 100644 index 00000000000..aa6d48ec208 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-race-condition.yaml @@ -0,0 +1,17 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: race-condition-task + afterTask: escapeNonAsciiCharacters +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: race-condition-task +task: + path: race-condition-task.js diff --git a/packages/project/test/fixtures/application.a/webapp/thirdparty/scriptWithSourceMap.js b/packages/project/test/fixtures/application.a/webapp/thirdparty/scriptWithSourceMap.js new file mode 100644 index 00000000000..5c633c08703 --- /dev/null +++ b/packages/project/test/fixtures/application.a/webapp/thirdparty/scriptWithSourceMap.js @@ -0,0 +1,2 @@ +console.log("This is a script with a source map."); +//# sourceMappingURL=scriptWithSourceMap.js.map diff --git a/packages/project/test/fixtures/application.a/webapp/thirdparty/scriptWithSourceMap.js.map b/packages/project/test/fixtures/application.a/webapp/thirdparty/scriptWithSourceMap.js.map new file mode 100644 index 00000000000..5e28dc9849d --- /dev/null +++ b/packages/project/test/fixtures/application.a/webapp/thirdparty/scriptWithSourceMap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scriptWithSourceMap.js","names":["console","log"],"sources":["scriptWithSourceMap.ts"],"sourcesContent":["console.log(\"This is a script with a source map.\");\n"],"mappings":"AAAAA,QAAQC,IAAI","ignoreList":[]} diff --git a/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/data.json b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/data.json new file mode 100644 index 00000000000..9658000784b --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/data.json @@ -0,0 +1 @@ +{"key": "original-value"} diff --git a/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.i.copy/node_modules/library.d/package.json b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.i.copy/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.i.copy/package.json b/packages/project/test/fixtures/application.i.copy/package.json new file mode 100644 index 00000000000..23a233d70c8 --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/package.json @@ -0,0 +1,12 @@ +{ + "name": "application.i.copy", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.i.copy/ui5.yaml b/packages/project/test/fixtures/application.i.copy/ui5.yaml new file mode 100644 index 00000000000..8da9f4504d4 --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.i.copy diff --git a/packages/project/test/fixtures/application.i.copy/webapp/index.html b/packages/project/test/fixtures/application.i.copy/webapp/index.html new file mode 100644 index 00000000000..1b8755901bf --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + diff --git a/packages/project/test/fixtures/application.i.copy/webapp/manifest.json b/packages/project/test/fixtures/application.i.copy/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.i.copy/webapp/test.js b/packages/project/test/fixtures/application.i.copy/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/application.i.copy/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/data.json b/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/data.json new file mode 100644 index 00000000000..9658000784b --- /dev/null +++ b/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/data.json @@ -0,0 +1 @@ +{"key": "original-value"} diff --git a/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.i/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.i/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.i/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.i/node_modules/library.d/package.json b/packages/project/test/fixtures/application.i/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.i/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.i/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.i/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.i/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.i/package.json b/packages/project/test/fixtures/application.i/package.json new file mode 100644 index 00000000000..12d3de627b3 --- /dev/null +++ b/packages/project/test/fixtures/application.i/package.json @@ -0,0 +1,12 @@ +{ + "name": "application.i", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.i/ui5.yaml b/packages/project/test/fixtures/application.i/ui5.yaml new file mode 100644 index 00000000000..33a7ccb97f7 --- /dev/null +++ b/packages/project/test/fixtures/application.i/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.i diff --git a/packages/project/test/fixtures/application.i/webapp/index.html b/packages/project/test/fixtures/application.i/webapp/index.html new file mode 100644 index 00000000000..1b8755901bf --- /dev/null +++ b/packages/project/test/fixtures/application.i/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + diff --git a/packages/project/test/fixtures/application.i/webapp/manifest.json b/packages/project/test/fixtures/application.i/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.i/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.i/webapp/test.js b/packages/project/test/fixtures/application.i/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/application.i/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json new file mode 100644 index 00000000000..2179673d41d --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json @@ -0,0 +1,17 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "ui5": { + "name": "library.a", + "type": "library", + "settings": { + "src": "src", + "test": "test" + } + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library new file mode 100644 index 00000000000..25c8603f31a --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + ${copyright} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library new file mode 100644 index 00000000000..36052acebdc --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + ${copyright} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/package.json new file mode 100644 index 00000000000..81b948438bd --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/package.json @@ -0,0 +1,18 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "collection": { + "modules": { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c" + } + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml new file mode 100644 index 00000000000..e47048de6a7 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml @@ -0,0 +1,12 @@ +specVersion: "2.1" +metadata: + name: application.a.collection.dependency.shim +kind: extension +type: project-shim +shims: + collections: + collection: + modules: + "library.a": "./library.a" + "library.b": "./library.b" + "library.c": "./library.c" \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/package.json b/packages/project/test/fixtures/component.a/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/component.a/ui5-custom-preload-config.yaml b/packages/project/test/fixtures/component.a/ui5-custom-preload-config.yaml new file mode 100644 index 00000000000..9d1ef25beca --- /dev/null +++ b/packages/project/test/fixtures/component.a/ui5-custom-preload-config.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "5.0" +type: component +metadata: + name: component.a +builder: + componentPreload: + namespaces: + - "id1" + excludes: + - "id1/test.js" \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d/main/src/library/d/.library b/packages/project/test/fixtures/library.d/main/src/library/d/.library index 53c2d14c9d6..21251d1bbba 100644 --- a/packages/project/test/fixtures/library.d/main/src/library/d/.library +++ b/packages/project/test/fixtures/library.d/main/src/library/d/.library @@ -3,7 +3,7 @@ library.d SAP SE - Some fancy copyright + ${copyright} ${version} Library D diff --git a/packages/project/test/fixtures/library.d/main/src/library/d/data.json b/packages/project/test/fixtures/library.d/main/src/library/d/data.json new file mode 100644 index 00000000000..9658000784b --- /dev/null +++ b/packages/project/test/fixtures/library.d/main/src/library/d/data.json @@ -0,0 +1 @@ +{"key": "original-value"} diff --git a/packages/project/test/fixtures/library.d/ui5-custom-preload-config.yaml b/packages/project/test/fixtures/library.d/ui5-custom-preload-config.yaml new file mode 100644 index 00000000000..6187b116068 --- /dev/null +++ b/packages/project/test/fixtures/library.d/ui5-custom-preload-config.yaml @@ -0,0 +1,15 @@ +--- +specVersion: "5.0" +type: library +metadata: + name: library.d + copyright: Some fancy copyright +resources: + configuration: + paths: + src: main/src + test: main/test +builder: + libraryPreload: + excludes: + - "library/d/some.js" \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d/ui5.yaml b/packages/project/test/fixtures/library.d/ui5.yaml index a47c1f64c3d..9d1317fba3f 100644 --- a/packages/project/test/fixtures/library.d/ui5.yaml +++ b/packages/project/test/fixtures/library.d/ui5.yaml @@ -3,6 +3,7 @@ specVersion: "2.3" type: library metadata: name: library.d + copyright: Some fancy copyright resources: configuration: paths: diff --git a/packages/project/test/fixtures/module.b/dev/devTools.js b/packages/project/test/fixtures/module.b/dev/devTools.js new file mode 100644 index 00000000000..e035bfaeab6 --- /dev/null +++ b/packages/project/test/fixtures/module.b/dev/devTools.js @@ -0,0 +1 @@ +console.log("dev dev dev"); diff --git a/packages/project/test/fixtures/module.b/node_modules/.package-lock.json b/packages/project/test/fixtures/module.b/node_modules/.package-lock.json new file mode 100644 index 00000000000..ba2e1378c35 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/.package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "module.b", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "../library.d": { + "version": "1.0.0", + "extraneous": true + }, + "node_modules/collection": { + "version": "1.0.0", + "resolved": "file:../collection", + "workspaces": [ + "library.a", + "library.b", + "library.c" + ], + "dependencies": { + "library.d": "file:../library.d" + } + }, + "node_modules/library.d": { + "version": "1.0.0", + "resolved": "file:../library.d" + } + } +} diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/package.json new file mode 100644 index 00000000000..aec498f7283 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/src/library/a/.library new file mode 100644 index 00000000000..ef0ea1065bc --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + Some fancy copyright ${currentYear} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/src/library/b/.library new file mode 100644 index 00000000000..7128151f3f4 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + Some fancy copyright ${currentYear} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/package.json b/packages/project/test/fixtures/module.b/node_modules/collection/package.json new file mode 100644 index 00000000000..24849dbe4a8 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/package.json @@ -0,0 +1,16 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "workspaces": [ + "library.a", + "library.b", + "library.c" + ] +} diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/test.js b/packages/project/test/fixtures/module.b/node_modules/collection/test.js new file mode 100644 index 00000000000..d063db1e726 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/test.js @@ -0,0 +1,4 @@ +import {globby} from 'globby'; + +const paths = await globby(["library.a"]); +console.log("paths") diff --git a/packages/project/test/fixtures/module.b/node_modules/collection/ui5.yaml b/packages/project/test/fixtures/module.b/node_modules/collection/ui5.yaml new file mode 100644 index 00000000000..e47048de6a7 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/collection/ui5.yaml @@ -0,0 +1,12 @@ +specVersion: "2.1" +metadata: + name: application.a.collection.dependency.shim +kind: extension +type: project-shim +shims: + collections: + collection: + modules: + "library.a": "./library.a" + "library.b": "./library.b" + "library.c": "./library.c" \ No newline at end of file diff --git a/packages/project/test/fixtures/module.b/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/module.b/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/module.b/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/module.b/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/module.b/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/module.b/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/module.b/node_modules/library.d/package.json b/packages/project/test/fixtures/module.b/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/module.b/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/module.b/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..9d1317fba3f --- /dev/null +++ b/packages/project/test/fixtures/module.b/node_modules/library.d/ui5.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d + copyright: Some fancy copyright +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/module.b/package-lock.json b/packages/project/test/fixtures/module.b/package-lock.json new file mode 100644 index 00000000000..fcbbe63defc --- /dev/null +++ b/packages/project/test/fixtures/module.b/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "module.b", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "module.b", + "version": "1.0.0", + "dependencies": { + "collection": "file:../collection", + "library.d": "file:../library.d" + } + }, + "../library.d": { + "version": "1.0.0", + "extraneous": true + }, + "node_modules/collection": { + "version": "1.0.0", + "resolved": "file:../collection", + "workspaces": [ + "library.a", + "library.b", + "library.c" + ], + "dependencies": { + "library.d": "file:../library.d" + } + }, + "node_modules/library.d": { + "version": "1.0.0", + "resolved": "file:../library.d" + } + } +} diff --git a/packages/project/test/fixtures/module.b/package.json b/packages/project/test/fixtures/module.b/package.json new file mode 100644 index 00000000000..384989cb3da --- /dev/null +++ b/packages/project/test/fixtures/module.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "module.b", + "version": "1.0.0", + "description": "Custom UI5 module", + "dependencies": { + "library.d": "file:../library.d", + "collection": "file:../collection" + } +} diff --git a/packages/project/test/fixtures/module.b/ui5.yaml b/packages/project/test/fixtures/module.b/ui5.yaml new file mode 100644 index 00000000000..f5365cf1f0b --- /dev/null +++ b/packages/project/test/fixtures/module.b/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "5.0" +type: module +metadata: + name: module.b +resources: + configuration: + paths: + /resources/b/module/dev/: dev \ No newline at end of file diff --git a/packages/project/test/fixtures/theme.library.e/package.json b/packages/project/test/fixtures/theme.library.e/package.json new file mode 100644 index 00000000000..2315226524d --- /dev/null +++ b/packages/project/test/fixtures/theme.library.e/package.json @@ -0,0 +1,4 @@ +{ + "name": "theme.library.e", + "version": "1.0.0" +} diff --git a/packages/project/test/lib/build/BuildServer.integration.js b/packages/project/test/lib/build/BuildServer.integration.js new file mode 100644 index 00000000000..4e999796b1f --- /dev/null +++ b/packages/project/test/lib/build/BuildServer.integration.js @@ -0,0 +1,733 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import {fileURLToPath} from "node:url"; +import {setTimeout} from "node:timers/promises"; +import fs from "node:fs/promises"; +import {graphFromPackageDependencies} from "../../../lib/graph/graph.js"; +import {setLogLevel} from "@ui5/logger"; +import Cache from "../../../lib/build/cache/Cache.js"; + +// Ensures that all logging code paths are tested +setLogLevel("silly"); + +test.beforeEach((t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.logEventStub = sinon.stub(); + t.context.buildMetadataEventStub = sinon.stub(); + t.context.projectBuildMetadataEventStub = sinon.stub(); + t.context.buildStatusEventStub = sinon.stub(); + t.context.projectBuildStatusEventStub = sinon.stub(); + + process.on("ui5.log", t.context.logEventStub); + process.on("ui5.build-metadata", t.context.buildMetadataEventStub); + process.on("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub); + process.on("ui5.build-status", t.context.buildStatusEventStub); + process.on("ui5.project-build-status", t.context.projectBuildStatusEventStub); +}); + +test.afterEach.always(async (t) => { + await t.context.fixtureTester.teardown(); + t.context.sinon.restore(); + delete process.env.UI5_DATA_DIR; + + process.off("ui5.log", t.context.logEventStub); + process.off("ui5.build-metadata", t.context.buildMetadataEventStub); + process.off("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub); + process.off("ui5.build-status", t.context.buildStatusEventStub); + process.off("ui5.project-build-status", t.context.projectBuildStatusEventStub); +}); + +// Note: This test should be the first test to run, as it covers initial build scenarios, which are not reproducible +// once the BuildServer has been started and built a project at least once. +// This is independent of caching on file-system level, which is isolated per test via tmp folders. +test.serial("Serve application.a, initial file changes", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + await fixtureTester.serveProject(); + + // Directly change a source file in application.a before requesting it + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("initial change");\n`); + + // Request the changed resource immediately + const resourceRequestPromise = fixtureTester.requestResource({ + resource: "/test.js" + }); + + await setTimeout(500); + + // Directly change the source file again, which should abort the current build and trigger a new one + await fs.appendFile(changedFilePath, `\ntest("second change");\n`); + await fs.appendFile(changedFilePath, `\ntest("third change");\n`); + + // Wait for the resource to be served + await resourceRequestPromise; + await setTimeout(500); + + const resource2 = await fixtureTester.requestResource({ + resource: "/test.js" + }); + + // Check whether the change is reflected + const servedFileContent = await resource2.getString(); + t.true(servedFileContent.includes(`test("initial change");`), "Resource contains initial changed file content"); + t.true(servedFileContent.includes(`test("second change");`), "Resource contains second changed file content"); + t.true(servedFileContent.includes(`test("third change");`), "Resource contains third changed file content"); +}); + +test.serial("Serve application.a, request application resource", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1 request with empty cache + await fixtureTester.serveProject(); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 request with cache + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the change + + // #3 request with cache and changes + const res = await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + // Note: replaceCopyright is skipped because no copyright is configured in the project + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Check whether the changed file is in the destPath + const servedFileContent = await res.getString(); + t.true(servedFileContent.includes(`test("line added");`), "Resource contains changed file content"); +}); + +test.serial("Serve application.a, request library resource", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1 request with empty cache + await fixtureTester.serveProject(); + await fixtureTester.requestResource({ + resource: "/resources/library/a/.library", + assertions: { + projects: { + "library.a": {} + } + } + }); + + // #2 request with cache + await fixtureTester.requestResource({ + resource: "/resources/library/a/.library", + assertions: { + projects: {} + } + }); + + // Change a source file in library.a + const changedFilePath = `${fixtureTester.fixturePath}/node_modules/collection/library.a/src/library/a/.library`; + await fs.writeFile( + changedFilePath, + (await fs.readFile(changedFilePath, {encoding: "utf8"})).replace( + `Library A`, + `Library A (updated #1)` + ) + ); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the change + + // #3 request with cache and changes + const dotLibraryResource = await fixtureTester.requestResource({ + resource: "/resources/library/a/.library", + assertions: { + projects: { + "library.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "minify", + "replaceBuildtime", + ] + } + } + } + }); + + // Check whether the changed file is served + const servedFileContent = await dotLibraryResource.getString(); + t.true( + servedFileContent.includes(`Library A (updated #1)`), + "Resource contains changed file content" + ); + + // #4 request with cache (no changes) + const manifestResource = await fixtureTester.requestResource({ + resource: "/resources/library/a/manifest.json", + assertions: { + projects: {} + } + }); + + // Check whether the manifest is served correctly with changed .library content reflected + const manifestContent = JSON.parse(await manifestResource.getString()); + t.is( + manifestContent["sap.app"]["description"], "Library A (updated #1)", + "Manifest reflects changed .library content" + ); +}); + +test.serial("Serve library", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "library.d"); + + // #1 request with empty cache + await fixtureTester.serveProject({ + config: { + excludedTasks: ["minify"], + } + }); + await fixtureTester.requestResource({ + resource: "/resources/library/d/some.js", + assertions: { + projects: { + "library.d": {} + } + } + }); + + // #2 request with cache + await fixtureTester.requestResource({ + resource: "/resources/library/d/some.js", + assertions: { + projects: {} + } + }); + + // Change a source file in library.d + const changedFilePath = `${fixtureTester.fixturePath}/main/src/library/d/some.js`; + const originalContent = await fs.readFile(changedFilePath, {encoding: "utf8"}); + await fs.writeFile( + changedFilePath, + originalContent.replace( + ` */`, + ` */\n// Test 1` + ) + ); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the change + + // #3 request with cache and changes + const resourceContent1 = await fixtureTester.requestResource({ + resource: "/resources/library/d/some.js", + assertions: { + projects: { + "library.d": { + skippedTasks: [ + "buildThemes", + "enhanceManifest", + "escapeNonAsciiCharacters", + "replaceBuildtime", + ] + } + } + } + }); + + // Check whether the changed file is served + const servedFileContent1 = await resourceContent1.getString(); + t.true( + servedFileContent1.includes(`Test 1`), + "Resource contains changed file content" + ); + + // Restore original file content + + await fs.writeFile(changedFilePath, originalContent); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the change + + // #4 request with cache (no changes) + const resourceContent2 = await fixtureTester.requestResource({ + resource: "/resources/library/d/some.js", + assertions: { + projects: {} + } + }); + + const servedFileContent2 = await resourceContent2.getString(); + t.false( + servedFileContent2.includes(`Test 1`), + "Resource does not contain changed file content" + ); +}); + +test.serial("Serve application.a, request application resource AND library resource", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1 request with empty cache + await fixtureTester.serveProject(); + await fixtureTester.requestResources({ + resources: ["/test.js", "/resources/library/a/.library"], + assertions: { + projects: { + "library.a": {}, + "application.a": {} + } + } + }); + + // #2 request with cache + await fixtureTester.requestResources({ + resources: ["/test.js", "/resources/library/a/.library"], + assertions: { + projects: {} + } + }); + + // Change a source file in application.a and library.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added");\n`); + const changedFilePath2 = `${fixtureTester.fixturePath}/node_modules/collection/library.a/src/library/a/.library`; + await fs.writeFile( + changedFilePath2, + (await fs.readFile(changedFilePath2, {encoding: "utf8"})).replace( + `Library A`, + `Library A (updated #1)` + ) + ); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3 request with cache and changes + const [resource1, resource2] = await fixtureTester.requestResources({ + resources: ["/test.js", "/resources/library/a/.library"], + assertions: { + projects: { + "library.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "minify", + "replaceBuildtime", + ] + }, + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + // Note: replaceCopyright is skipped because no copyright is configured in the project + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Check whether the changed files contain the correct contents + const resource1FileContent = await resource1.getString(); + const resource2FileContent = await resource2.getString(); + t.true(resource1FileContent.includes(`test("line added");`), "Resource contains changed file content"); + t.true( + resource2FileContent.includes(`Library A (updated #1)`), + "Resource contains changed file content" + ); +}); + +test.serial("Serve application.a with --cache=Default", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with empty cache --> all tasks execute + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2: Request with valid cache, no changes --> nothing rebuilds (all cached) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for cache test");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3: Request with valid cache, source changes --> only affected tasks rebuild + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is served + const resource = await fixtureTester.requestResource({resource: "/test.js"}); + const servedFileContent = await resource.getString(); + t.true(servedFileContent.includes(`test("line added for cache test");`), + "Served resource contains changed file content"); +}); + +test.serial("Serve application.a with --cache=Off", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with cache=Off --> all tasks execute, cache not written + await fixtureTester.serveProject({config: {cache: Cache.Off}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2: Request with cache=Off (again) --> all tasks execute again (no cache reuse) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Restart server with cache=Default + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + + // #3: Request with cache=Default --> all tasks execute (no cache from previous Off mode) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #4: Request with cache=Default (again) --> nothing rebuilds (cache now exists) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Restart server with cache=Off + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Off}}); + + // #5: Request with cache=Off --> all tasks execute (ignores existing cache) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); +}); + +test.serial("Serve application.a with --cache=ReadOnly", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with cache=Default --> all tasks execute, cache written + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Restart server with ReadOnly mode + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.ReadOnly}}); + + // #2: Request with cache=ReadOnly, no changes --> nothing rebuilds (cache used) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for ReadOnly test");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3: Request with cache=ReadOnly --> affected tasks rebuild, BUT cache not updated + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is served + const resource = await fixtureTester.requestResource({resource: "/test.js"}); + const servedFileContent = await resource.getString(); + t.true(servedFileContent.includes(`test("line added for ReadOnly test");`), + "Served resource contains changed file content"); + + // Restart server with Default mode + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + + // #4: Request with cache=Default, no new changes --> rebuilds again (cache from #3 missing) + // This validates that ReadOnly didn't write the cache in step #3 + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); +}); + +test.serial("Serve application.a with --cache=Force (1)", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with cache=Default --> all tasks execute, cache written + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Restart server with Force mode + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Force}, expectBuildErrors: true}); + + // #2: Request with cache=Force, no changes --> nothing rebuilds (cache used) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for Force test");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3: Request with cache=Force --> ERROR (cache invalid due to source changes) + const error = await t.throwsAsync(async () => { + await fixtureTester.requestResource({ + resource: "/test.js", + }); + }); + + t.truthy(error, "Request with Force mode should throw error when cache is stale"); + t.true(error.message.includes(`Cache is in "Force" mode but cache is stale for project application.a`)); + + // Wait for async error handling to complete + await setTimeout(50); +}); + +test.serial("Serve application.a with --cache=Force (2)", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve with cache=Force on empty cache --> ERROR when requesting resource + await fixtureTester.serveProject({config: {cache: Cache.Force}, expectBuildErrors: true}); + + const error = await t.throwsAsync(async () => { + await fixtureTester.requestResource({ + resource: "/test.js", + }); + }); + + t.truthy(error, "Request with Force mode should throw error when cache is empty"); + t.true(error.message.includes(`Cache is in "Force" mode but no cache found for project application.a`)); + + // Wait for async error handling to complete + await setTimeout(50); +}); + +function getFixturePath(fixtureName) { + return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url)); +} + +function getTmpPath(folderName) { + return fileURLToPath(new URL(`../../tmp/BuildServer/${folderName}`, import.meta.url)); +} + +async function rmrf(dirPath) { + return fs.rm(dirPath, {recursive: true, force: true}); +} + +class FixtureTester { + constructor(t, fixtureName) { + this._t = t; + this._sinon = t.context.sinon; + this._fixtureName = fixtureName; + this._initialized = false; + + // Public + this.fixturePath = getTmpPath(fixtureName); + this.buildServer = null; + this.graph = null; + } + + async _initialize() { + if (this._initialized) { + return; + } + process.env.UI5_DATA_DIR = getTmpPath(`${this._fixtureName}/.ui5`); + await rmrf(this.fixturePath); // Clean up any previous test runs + await fs.cp(getFixturePath(this._fixtureName), this.fixturePath, {recursive: true}); + this._initialized = true; + } + + async teardown() { + if (this.buildServer) { + try { + await this.buildServer.destroy(); + } catch { + // Ignore errors during teardown (e.g., failed Force mode builds) + } + } + } + + async serveProject({graphConfig = {}, config = {}, expectBuildErrors = false} = {}) { + await this._initialize(); + + const graph = this.graph = await graphFromPackageDependencies({ + ...graphConfig, + cwd: this.fixturePath, + }); + + // Execute the build + this.buildServer = await graph.serve(config); + this.buildServer.on("error", (err) => { + if (!expectBuildErrors) { + this._t.fail(`Build server error: ${err.message}`); + } + }); + this._reader = this.buildServer.getReader(); + } + + async requestResource({resource, assertions}) { + this._sinon.resetHistory(); + const res = await this._reader.byPath(resource); + // Apply assertions if provided + if (assertions) { + this._assertBuild(assertions); + } + return res; + } + + async requestResources({resources, assertions}) { + this._sinon.resetHistory(); + const returnedResources = await Promise.all(resources.map((resource) => this._reader.byPath(resource))); + // Apply assertions if provided + if (assertions) { + this._assertBuild(assertions); + } + return returnedResources; + } + + _assertBuild(assertions) { + const {projects = {}} = assertions; + const eventArgs = this._t.context.projectBuildStatusEventStub.args.map((args) => args[0]); + + const projectsInOrder = []; + const seenProjects = new Set(); + const tasksByProject = {}; + + for (const event of eventArgs) { + if (!seenProjects.has(event.projectName)) { + projectsInOrder.push(event.projectName); + seenProjects.add(event.projectName); + } + if (!tasksByProject[event.projectName]) { + tasksByProject[event.projectName] = {executed: [], skipped: []}; + } + if (event.status === "task-skip") { + tasksByProject[event.projectName].skipped.push(event.taskName); + } else if (event.status === "task-start") { + tasksByProject[event.projectName].executed.push(event.taskName); + } + } + + // Assert projects built in order + const expectedProjects = Object.keys(projects); + this._t.deepEqual(projectsInOrder, expectedProjects); + + // Assert skipped tasks per project + for (const [projectName, expectedSkipped] of Object.entries(projects)) { + const skippedTasks = expectedSkipped.skippedTasks || []; + const actualSkipped = (tasksByProject[projectName]?.skipped || []).sort(); + const expectedArray = skippedTasks.sort(); + this._t.deepEqual(actualSkipped, expectedArray); + } + } +} diff --git a/packages/project/test/lib/build/ProjectBuilder.integration.js b/packages/project/test/lib/build/ProjectBuilder.integration.js new file mode 100644 index 00000000000..d178875a666 --- /dev/null +++ b/packages/project/test/lib/build/ProjectBuilder.integration.js @@ -0,0 +1,3067 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import {fileURLToPath} from "node:url"; +import fs from "node:fs/promises"; +import {graphFromPackageDependencies} from "../../../lib/graph/graph.js"; +import {setLogLevel} from "@ui5/logger"; +import Cache from "../../../lib/build/cache/Cache.js"; + +// Ensures that all logging code paths are tested +setLogLevel("silly"); + +test.beforeEach((t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.logEventStub = sinon.stub(); + t.context.buildMetadataEventStub = sinon.stub(); + t.context.projectBuildMetadataEventStub = sinon.stub(); + t.context.buildStatusEventStub = sinon.stub(); + t.context.projectBuildStatusEventStub = sinon.stub(); + + process.on("ui5.log", t.context.logEventStub); + process.on("ui5.build-metadata", t.context.buildMetadataEventStub); + process.on("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub); + process.on("ui5.build-status", t.context.buildStatusEventStub); + process.on("ui5.project-build-status", t.context.projectBuildStatusEventStub); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + delete process.env.UI5_DATA_DIR; + + process.off("ui5.log", t.context.logEventStub); + process.off("ui5.build-metadata", t.context.buildMetadataEventStub); + process.off("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub); + process.off("ui5.build-status", t.context.buildStatusEventStub); + process.off("ui5.project-build-status", t.context.projectBuildStatusEventStub); +}); + +test.serial("Build application.a project multiple times", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added");\n`); + + // #3 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + // Note: replaceCopyright is skipped because no copyright is configured in the project + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Check whether the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content"); + + + // #4 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + } + } + }); + + + // #5 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // #6 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // #7 build (with cache, no changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + + // #8 build (with cache, no changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // #9 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Change a source file with existing source map in application.a + const fileWithSourceMapPath = + `${fixtureTester.fixturePath}/webapp/thirdparty/scriptWithSourceMap.js`; + const fileWithSourceMapContent = await fs.readFile(fileWithSourceMapPath, {encoding: "utf8"}); + await fs.writeFile( + fileWithSourceMapPath, + fileWithSourceMapContent.replace( + `This is a script with a source map.`, + `This is a CHANGED script with a source map.` + ) + ); + const sourceMapPath = `${fixtureTester.fixturePath}/webapp/thirdparty/scriptWithSourceMap.js.map`; + const sourceMapContent = await fs.readFile(sourceMapPath, {encoding: "utf8"}); + await fs.writeFile( + sourceMapPath, + sourceMapContent.replace( + `This is a script with a source map.`, + `This is a CHANGED script with a source map.` + ) + ); + + // #10 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright" + ] + } + } + } + }); + + + // Add a new file to application.a + await fs.writeFile(`${fixtureTester.fixturePath}/webapp/someNew.js`, + `console.log("SOME NEW CONTENT");\n` + ); + + // #11 build (with cache, with changes - someNew.js added) + // Tasks that don't depend on someNew.js can reuse their caches from build #10. + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + }} + } + }); + + await fs.rm(`${fixtureTester.fixturePath}/webapp/someNew.js`); + + // #12 build (with cache, with changes - someNew.js removed) + // Source state matches build #10's cached result -> cache reused, everything skipped + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {}, + } + }); +}); + +test.serial("Build application.a (with various dependencies)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + + // #2 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Add a "component" dependency to application.a: + await fixtureTester.addComponentDependency(`${fixtureTester.fixturePath}/webapp`); + + // #3 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "component.z": {}, + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // Add a "library" dependency to application.a: + await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/webapp`); + + // #4 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.z": {}, + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // Add a "themelib" dependency to application.a: + await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/webapp`); + + // #5 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "themelib.z": {}, + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // Add a "module" dependency to application.a: + await fixtureTester.addModuleDependency(`${fixtureTester.fixturePath}/webapp`); + + // #6 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "module.z": {}, + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); +}); + +test.serial("Build application.a (including only some dependencies)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // In this test, we're testing the "dependencyIncludes" build option + // which allows to include only a subset of the dependencies of a project in the build. + // "application.a" has 4 dependencies defined: library.a, library.b, library.c and library.d. + + // #1 build + // Only include library.a and library.b as dependencies, but not library.c and library.d: + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, + dependencyIncludes: {includeDependency: ["library.a", "library.b"]}}, + assertions: { + projects: { + "library.a": {}, + "library.b": {}, + "application.a": {} + } + } + }); + + // Check that only the included dependencies are in the destPath: + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`, + {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`, + {encoding: "utf8"})); + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`, + {encoding: "utf8"})); + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`, + {encoding: "utf8"})); + + + // #2 build + // Exclude library.d as dependency, but include all other dependencies + // (builds of library.a and library.b can be reused from cache): + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, + dependencyIncludes: {includeAllDependencies: true, excludeDependency: ["library.d"]}}, + assertions: { + projects: { + "library.c": {}, + } + } + }); + + // Check that only the included dependencies are in the destPath: + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`, + {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`, + {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`, + {encoding: "utf8"})); + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`, + {encoding: "utf8"})); + + + // #3 build + // Include all dependencies (only library.d is built) + // (builds of library.a, library.b, and library.c can be reused from cache): + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + } + } + }); + + // Check that all dependencies are in the destPath: + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`, + {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`, + {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`, + {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`, + {encoding: "utf8"})); + + + // Delete a dependency ("library.d") from application.a: + await fs.rm(`${fixtureTester.fixturePath}/node_modules/library.d`, {recursive: true, force: true}); + const packageJsonContent = JSON.parse( + await fs.readFile(`${fixtureTester.fixturePath}/package.json`, {encoding: "utf8"})); + delete packageJsonContent.dependencies["library.d"]; + await fs.writeFile(`${fixtureTester.fixturePath}/package.json`, JSON.stringify(packageJsonContent, null, 2)); + + // #4 build + // Build application.a again with "includeAllDependencies" + // and check with assertion "allProjects" that "library.d" isn't even seen: + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + allProjects: ["library.a", "library.b", "library.c", "application.a"], + projects: {}, // no project should be rebuilt + } + }); +}); + +test.serial("Build application.a (custom task and tag handling)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 build (no cache, no changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + + // Create new file which should get tagged as "OmitFromBuildResult" by a custom task + await fs.writeFile(`${fixtureTester.fixturePath}/webapp/fileToBeOmitted.js`, + `console.log("this file should be omitted in the build result")`); + + // #2 build (with cache, with changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + // Note: replaceCopyright is skipped because no copyright is configured in the project + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Check that fileToBeOmitted.js is not in dist + await t.throwsAsync(fs.readFile(`${destPath}/fileToBeOmitted.js`, {encoding: "utf8"})); + + + // #3 build (with cache, no changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + // Check that fileToBeOmitted.js is not in dist again + await t.throwsAsync(fs.readFile(`${destPath}/fileToBeOmitted.js`, {encoding: "utf8"})); + + + // Delete the file again + await fs.rm(`${fixtureTester.fixturePath}/webapp/fileToBeOmitted.js`); + + // #4 build (with cache, with changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} // everything should be skipped (already done in very first build) + } + }); +}); + +test.serial("Build application.a (multiple custom tasks)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // This test should cover a scenario with multiple custom tasks. + // Specifically, a tag is set in custom-task-1 on a resource which is read in custom-task-0 and custom-task-2. + // The expected behavior is that the tag is not present in custom-task-0 (which runs before custom-task-1), + // but is present in custom-task-2 (which runs after custom-task-1). + // (for testing purposes, the custom tasks already check for this tag by themselves and handle errors accordingly) + + // #1 build (no cache, no changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 build (with cache, no changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Create a new file to allow a new build: + // Logic of custom-task-1 will NOT handle this file, while custom-task-0 and 2 WILL DO it, + // resulting in custom-task-1 getting skipped (cache reuse). + // The test should then verify that the tag is still only readable for custom-task-2. + // This ensures that the build result is exactly the same with or without using the cache. + // (as in #1 build, the custom tasks already check for this tag by themselves and handle errors accordingly) + await fs.cp(`${fixtureTester.fixturePath}/webapp/test.js`, + `${fixtureTester.fixturePath}/webapp/test2.js`); + + // #3 build (with cache, with changes, with custom tasks) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "custom-task-1", // SHOULD BE SKIPPED + // remaining skipped tasks don't matter here: + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); +}); + +test.serial("Build application.a (multiple custom tasks 2)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // This test should cover a scenario with multiple custom tasks. + + // #1 build (no cache, no changes, with custom tasks) + // During this build, "custom-task-0" sets the tag "isDebugVariant" to test.js. + // "custom-task-1" checks if it's able to read this tag. + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + + // Modify file to trigger a new build + // (this is related to the custom tasks): + await fs.appendFile(`${fixtureTester.fixturePath}/webapp/test.js`, `console.log("CHANGED FILE");`); + + // #2 build (with cache, with changes, with custom tasks) + // During this build, "custom-task-0" sets a different tag to test.js (namely "OmitFromBuildResult"). + // "custom-task-1" again checks if it's able to read this different tag. + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Check that test.js is omitted from build output: + await t.throwsAsync(fs.readFile(`${destPath}/test.js`, {encoding: "utf8"})); + + + // Add new file to trigger another build + // (this is unrelated to the custom tasks): + await fs.writeFile(`${fixtureTester.fixturePath}/webapp/newFile.js`, `console.log("NEW FILE");`); + + // #4 build (with cache, with changes, with custom tasks) + // During this build, both custom tasks are expected to get skipped. + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "custom-task-0", + "custom-task-1", + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // #5 build (with cache, no changes, with custom tasks) + // During this build, everything should get skipped. + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); +}); + +test.serial.skip("Build application.a (dependency content changes)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // This test should cover a scenario with an application depending on a library. + // Specifically, we're directly modifying the contents of the library + // which should have effects on the application because a custom task will detect it + // and modify the application's resources. The application is expected to get rebuilt. + + // #1 build (no cache, no changes, no dependencies) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + + // #2 build (with cache, no changes, no dependencies) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Change content of library.d (this will not affect application.a): + const someJsOfLibrary = `${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`; + await fs.appendFile(someJsOfLibrary, `\ntest("line added");\n`); + + // #3 build (with cache, with changes, with dependencies) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"}, + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + } + } + }); + + // Check if library contains correct changed content: + const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content"); + + + // Change content of library.d again (this time it affects application.a): + await fs.writeFile(`${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/newLibraryFile.js`, + `console.log("SOME NEW CONTENT");`); + + // #4 build (no cache, with changes, with dependencies) + // This build should execute the custom task "task.dependency-change.js" again which now detects "newLibraryFile.js" + // and modifies a resource of application.a (namely "test.js"). + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"}, + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "application.a": { // FIXME: currently failing (getting skipped entirely) + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + }, + } + } + }); + + // Check that application.a contains correct changed content (test.js): + const builtFileContent2 = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true(builtFileContent2.includes(`console.log('something new');`), "Build dest contains changed file content"); +}); + +test.serial("Build application.a (cross-project tag change)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + await fixtureTester._initialize(); + + // Modify library.d's ui5.yaml at runtime to add the dep-tag-setter custom task + const libraryDYamlPath = `${fixtureTester.fixturePath}/node_modules/library.d/ui5.yaml`; + await fs.writeFile(libraryDYamlPath, + `--- +specVersion: "2.3" +type: library +metadata: + name: library.d + copyright: Some fancy copyright +resources: + configuration: + paths: + src: main/src + test: main/test +builder: + customTasks: + - name: dep-tag-setter + afterTask: minify +`); + + // #1 build (no cache, with all dependencies) + // dep-tag-setter sets project:FirstBuild on some.js + // dep-tag-reader verifies project:FirstBuild is present + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"}, + config: { + destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}, + excludedTasks: ["minify"], + }, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // #2 build (cache, no changes) → all skipped + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"}, + config: { + destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}, + excludedTasks: ["minify"], + }, + assertions: { + projects: {} + } + }); + + // Change source in library.d to trigger rebuild + const someJsPath = `${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`; + await fs.appendFile(someJsPath, `\nconsole.log("tag change trigger");\n`); + + // #3 build (cache, library.d source changed) + // library.d rebuilt → dep-tag-setter now sets project:SubsequentBuild (different tag than #1) + // library.d's index signature changes due to the tag change + // application.a rebuilt because its dependency index changed + // dep-tag-reader verifies project:SubsequentBuild is present + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"}, + config: { + destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}, + excludedTasks: ["minify"], + }, + assertions: { + projects: { + "library.d": { + // FIXME: skippedTasks need empirical determination once runtime bugs are fixed + skippedTasks: [ + "buildThemes", + "enhanceManifest", + "escapeNonAsciiCharacters", + "replaceBuildtime", + ] + }, + "application.a": { + // FIXME: skippedTasks need empirical determination once runtime bugs are fixed + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateComponentPreload", + "generateFlexChangesBundle", + "replaceCopyright", + "replaceVersion", + ] + }, + } + } + }); + + // #4 build (cache, no changes) → all skipped + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"}, + config: { + destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}, + excludedTasks: ["minify"], + }, + assertions: { + projects: {} + } + }); +}); + +test.serial("Build application.a (JSDoc build)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // This test should cover a scenario with an application depending on a library. + // We're executing a JSDoc build including dependencies (as with "ui5 build jsdoc --all") + // and testing if the output contains the expected JSDoc contents. + // Then, we're adding some additional JSDoc annotations to the library + // and testing the same again. + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, jsdoc: "jsdoc", dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // Check that JSDoc build ran successfully: + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"})); + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"})); + const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"}); + // Check that output contains correct file content: + t.false(builtFileContent.includes(`//# sourceMappingURL=some.js.map`), + "Build dest does not contain source map reference"); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, jsdoc: "jsdoc", dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Add additional JSDoc annotations to library.d: + const jsdocContent = `/*! +* ` + "${copyright}" + ` +*/ + +/** +* Example JSDoc annotation +* +* @public +* @static +* @param {object} param +* @returns {string} output +*/ +function functionWithJSDoc(param) {return "test"}`; + + await fs.writeFile(`${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`, + jsdocContent); + + // #3 build (no cache, with changes) + // application.a should get skipped: + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, jsdoc: "jsdoc", dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": { + skippedTasks: [ + "buildThemes", + "enhanceManifest", + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + ] + } + } + } + }); + + // Check that JSDoc build ran successfully: + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"})); + await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"})); + const builtFileContent2 = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"}); + t.true(builtFileContent2.includes(`Example JSDoc annotation`), "Build dest contains new JSDoc content"); + // Check that output contains new file content: + t.false(builtFileContent2.includes(`//# sourceMappingURL=some.js.map`), + "Build dest does not contain source map reference"); + + + // #4 build (no cache, no changes) + // Normal build again (non-JSDoc build); should not execute task "generateJsdoc": + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // Check that normal build ran successfully: + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"})); + await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"})); + const builtFileContent3 = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"}); + t.false(builtFileContent3.includes(`Example JSDoc annotation`), "Build dest doesn't contain JSDoc content anymore"); + // Check that output contains content generated by the normal build: + t.true(builtFileContent3.includes(`//# sourceMappingURL=some.js.map`), + "Build dest does contain source map reference"); +}); + +test.serial("Build application.a (Custom Component preload configuration)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // In this test, we're testing the behavior of a custom preload configuration + // which is defined in "ui5-custom-preload-config.yaml". + // This custom preload configuration generates a Component-preload.js similar to a default one. + // However, it will omit a resource ("scriptWithSourceMap.js") from the bundle. + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Check that generated preload bundle doesn't contain the omitted file: + t.false((await fs.readFile(`${destPath}/Component-preload.js`, {encoding: "utf8"})) + .includes("id1/thirdparty/scriptWithSourceMap.js")); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); +}); + +test.serial("Build application.a (self-contained build)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // We're executing a self-contained build including dependencies (as with "ui5 build self-contained --all") + // and testing if the output contains the expected self-contained bundle. + // Then, we're changing the content only of application.a + // and testing if the self-contained build output changes accordingly. + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, selfContained: "self-contained", + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // Check that output contains the correct content: + const builtFileContent = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`"id1/test.js":'function test(t){var o=t;console.log(o)}test();\\n'`)); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, selfContained: "self-contained", + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Remove the file "test.js" from application.a: + await fs.rm(`${fixtureTester.fixturePath}/webapp/test.js`); + + // #3 build (with cache, with changes) + // Dependencies should get skipped, application.a should get rebuilt: + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, selfContained: "self-contained", + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + "transformBootstrapHtml", + ] + } + } + } + }); + + // Check that output contains the correct content (test.js should be missing): + const builtFileContent2 = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"}); + t.false(builtFileContent2.includes(`"id1/test.js":`)); + + + // #4 build (with cache, no changes) + // Run a self-contained build but with a different config which defines a custom preload. + // The build should run and the output should still contain the expected self-contained bundle + // (tasks "generateComponentPreload" and "generateLibraryPreload" should not get executed): + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true, selfContained: "self-contained", + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Check that output contains still the correct content: + const builtFileContent3 = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"}); + t.false(builtFileContent3.includes(`"id1/test.js":`)); + + + // #5 build (with cache, no changes) + // Run a self-contained build but without dependencies: + // (everything should get skipped) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5.yaml"}, + config: {destPath, cleanDest: true, selfContained: "self-contained"}, + assertions: { + projects: {} + } + }); +}); + +test.serial("Build application.a (Custom bundling)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // In this test, we're testing the behavior of a custom bundling configuration + // which is defined in "ui5-custom-bundling.yaml". + // This config generates a custom bundle in various modes. + // The bundle includes resources by a filter ("Component.js" & "newFile.js") which are added at #3 and #4 build. + + // #1 build with custom bundle configuration (with empty cache) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: false}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // Verify that the custom bundle was created and contains the expected content: + const customBundlePath = `${destPath}/resources/custom-bundle.js`; + const customBundleContent = await fs.readFile(customBundlePath, {encoding: "utf8"}); + t.false(customBundleContent.includes("sap.ui.predefine("), + "preload Mode: Custom bundle should not contain sap.ui.predefine() at this stage" + ); + t.false(customBundleContent.includes("sap.ui.require.preload("), + "preload Mode: Custom bundle should not contain sap.ui.require.preload() at this stage" + ); + // Verify that source map was created: + const sourceMapPath = `${destPath}/resources/custom-bundle.js.map`; + const sourceMapContent = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"})); + t.true(sourceMapContent.sections.length === 0, "Source map file should not have content at this stage"); + + + // #2 build with custom bundle (with cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Add a source file which is matched by the filter (UI5 Module): + const newComponentFilepath = `${fixtureTester.fixturePath}/webapp/Component.js`; + await fs.appendFile(newComponentFilepath, + `sap.ui.define(["sap/ui/core/UIComponent", "sap/ui/core/ComponentSupport"], (UIComponent) => { + "use strict"; + return UIComponent.extend("id1.Component", { + metadata: { + manifest: "json", + interfaces: ["sap.ui.core.IAsyncContentCreation"], + } + }); +});`); + + // #3 build with custom bundle (with cache, with changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + // Verify that the updated custom bundle contains the change: + // (the bundle should now contain sap.ui.predefine() due to the added UI5 module "Component.js") + const customBundleContent2 = await fs.readFile(customBundlePath, {encoding: "utf8"}); + t.true(customBundleContent2.includes("sap.ui.predefine("), + "preload Mode: Custom bundle should contain sap.ui.predefine() now" + ); + t.false(customBundleContent2.includes("sap.ui.require.preload("), + "preload Mode: Custom bundle should not contain sap.ui.require.preload() at this stage" + ); + // Verify that source map was created and contains the change: + const sourceMapContent2 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"})); + t.true(sourceMapContent2.sections.length > 0, "Source map file should have content now"); + + + // Add another source file which is matched by the filter (non-UI5 module): + const newTestFilepath = `${fixtureTester.fixturePath}/webapp/newFile.js`; + await fs.appendFile(newTestFilepath, `console.log("another source file");`); + + // #4 build with custom bundle (with cache, with changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + // Verify that the custom bundle was created and contains the expected content: + // (the bundle should now contain sap.ui.require.preload() due to the added non-UI5 module "newFile.js") + const customBundleContent3 = await fs.readFile(customBundlePath, {encoding: "utf8"}); + t.true(customBundleContent3.includes("sap.ui.predefine("), + "preload Mode: Custom bundle should contain sap.ui.predefine() still" + ); + t.true(customBundleContent3.includes("sap.ui.require.preload("), + "preload Mode: Custom bundle should contain sap.ui.require.preload() now" + ); + // Verify that source map was created and contains the change: + const sourceMapContent3 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"})); + t.true(sourceMapContent3.sections.length > 0, "Source map file should have content still"); + + + // ---------------------------------------------------------------------------------- + // ---------------------------- Test other bundle modes: ---------------------------- + // ---------------------------------------------------------------------------------- + // Switch to "raw" mode: + const ui5YamlContent = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`); + await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`, + ui5YamlContent.toString().replace(`- mode: preload`, `- mode: raw`)); + + // #5 build with custom bundle configuration (with empty cache) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Verify that the custom bundle was created and contains the expected content: + // (the bundle should contain sap.ui.define() now) + const customBundleContent4 = await fs.readFile(customBundlePath, {encoding: "utf8"}); + t.false(customBundleContent4.includes("sap.ui.require.preload("), + "raw Mode: Custom bundle should not contain sap.ui.require.preload() anymore" + ); + t.false(customBundleContent4.includes("sap.ui.predefine("), + "raw Mode: Custom bundle should not contain sap.ui.predefine() anymore" + ); + t.true(customBundleContent4.includes("sap.ui.define("), + "raw Mode: Custom bundle should contain sap.ui.define() now" + ); + t.true(customBundleContent4.includes(`console.log("another source file");`)); + t.true(customBundleContent4.includes(`id1.Component`)); + // Verify that source map was created and contains the change: + const sourceMapContent4 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"})); + t.true(sourceMapContent4.sections.length > 0, "Source map file should have content still"); + + + // Switch to "require" mode: + const ui5YamlContent2 = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`); + await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`, + ui5YamlContent2.toString().replace(`- mode: raw`, `- mode: require`)); + + // #6 build with custom bundle configuration (with empty cache) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Verify that the custom bundle was created and contains the expected content: + // (the bundle should contain sap.ui.require() now) + const customBundleContent5 = await fs.readFile(customBundlePath, {encoding: "utf8"}); + t.false(customBundleContent5.includes("sap.ui.require.preload("), + "require Mode: Custom bundle should not contain sap.ui.require.preload() anymore" + ); + t.false(customBundleContent5.includes("sap.ui.predefine("), + "require Mode: Custom bundle should not contain sap.ui.predefine() anymore" + ); + t.false(customBundleContent5.includes("sap.ui.define("), + "require Mode: Custom bundle should not contain sap.ui.define() anymore" + ); + t.true(customBundleContent5.includes("sap.ui.require("), + "require Mode: Custom bundle should contain sap.ui.require() now" + ); + t.true(customBundleContent5.includes(`id1/newFile`)); + t.false(customBundleContent5.includes(`console.log("another source file");`)); + t.true(customBundleContent5.includes(`id1/Component`)); + // Verify that source map was created and contains the change: + const sourceMapContent5 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"})); + t.true(sourceMapContent5.sections.length > 0, "Source map file should have content still"); + + + // Switch to "bundleInfo" mode: + const ui5YamlContent3 = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`); + await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`, + ui5YamlContent3.toString().replace(`- mode: require`, `- mode: bundleInfo`)); + + // #7 build with custom bundle configuration (with empty cache) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Verify that the custom bundle was created and contains the expected content: + // (the bundle should contain sap.ui.loader.config() now) + const customBundleContent6 = await fs.readFile(customBundlePath, {encoding: "utf8"}); + t.false(customBundleContent6.includes("sap.ui.require.preload("), + "bundleInfo Mode: Custom bundle should not contain sap.ui.require.preload() anymore" + ); + t.false(customBundleContent6.includes("sap.ui.predefine("), + "bundleInfo Mode: Custom bundle should not contain sap.ui.predefine() anymore" + ); + t.false(customBundleContent6.includes("sap.ui.define("), + "bundleInfo Mode: Custom bundle should not contain sap.ui.define() anymore" + ); + t.false(customBundleContent6.includes("sap.ui.require("), + "bundleInfo Mode: Custom bundle should not contain sap.ui.require() anymore" + ); + t.true(customBundleContent6.includes("sap.ui.loader.config({bundlesUI5:{"), + "bundleInfo Mode: Custom bundle should contain sap.ui.loader.config() now" + ); + t.true(customBundleContent6.includes(`id1/newFile`)); + t.false(customBundleContent6.includes(`console.log("another source file");`)); + t.true(customBundleContent6.includes(`id1/Component`)); + // Verify that source map was created and contains the change: + const sourceMapContent6 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"})); + t.true(sourceMapContent6.sections.length > 0, "Source map file should have content still"); +}); + +test.serial("Build library.d project multiple times", async (t) => { + const fixtureTester = new FixtureTester(t, "library.d"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false}, + assertions: { + projects: {"library.d": {}} + } + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Change a source file in library.d + const changedFilePath = `${fixtureTester.fixturePath}/main/src/library/d/.library`; + await fs.writeFile( + changedFilePath, + (await fs.readFile(changedFilePath, {encoding: "utf8"})).replace( + `Library D`, + `Library D (updated #1)` + ) + ); + + // #3 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"library.d": { + skippedTasks: [ + "buildThemes", + "escapeNonAsciiCharacters", + "minify", + "replaceBuildtime", + ] + }} + } + }); + + // Check whether the changes are in the destPath + const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/.library`, {encoding: "utf8"}); + t.true( + builtFileContent.includes(`Library D (updated #1)`), + "Build dest contains changed file content" + ); + + // Check whether the manifest.json was updated with the new documentation + const manifestContent = await fs.readFile(`${destPath}/resources/library/d/manifest.json`, {encoding: "utf8"}); + t.true( + manifestContent.includes(`"Library D (updated #1)"`), + "Build dest contains updated description in manifest.json" + ); + + + // #4 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Update copyright in ui5.yaml (should trigger a full rebuild of the project) + const ui5YamlPath = `${fixtureTester.fixturePath}/ui5.yaml`; + await fs.writeFile( + ui5YamlPath, + (await fs.readFile(ui5YamlPath, {encoding: "utf8"})).replace( + "copyright: Some fancy copyright", + "copyright: Some updated fancy copyright" + ) + ); + + await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`, + `console.log("SOME NEW CONTENT");\n` + ); + + // #5 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"library.d": {}} + } + }); + + await fs.rm(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`); + + // #6 build (with cache, with changes - someNew.js removed) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"library.d": { + skippedTasks: [ + "buildThemes", + "enhanceManifest", + "escapeNonAsciiCharacters", + "replaceBuildtime", + ] + }}, + } + }); + + // Re-add someNew.js (restores source state to match build #5) + await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`, + `console.log("SOME NEW CONTENT");\n` + ); + + // #7 build (with cache, with changes - someNew.js re-added) + // Source state now matches build #5's cached result -> cache reused + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {}, + } + }); + + // Remove someNew.js again + await fs.rm(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`); + + // #8 build (with cache, with changes - someNew.js removed again) + // Source state matches build #6's cached result -> cache reused, everything skipped + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {}, + } + }); +}); + +test.serial("Build library.d (with various dependencies)", async (t) => { + const fixtureTester = new FixtureTester(t, "library.d"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false}, + assertions: { + projects: {"library.d": {}} + } + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Add a "library" dependency to library.d: + await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/main/src/library/d`); + + // #3 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.z": {}, + "library.d": { + skippedTasks: [ + "buildThemes", + "enhanceManifest", + "escapeNonAsciiCharacters", + "replaceBuildtime", + ] + }, + } + } + }); + + + // Add a "themelib" dependency to library.d: + await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/main/src/library/d`); + + // #4 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "themelib.z": {}, + "library.d": { + skippedTasks: [ + "buildThemes", + "enhanceManifest", + "escapeNonAsciiCharacters", + "replaceBuildtime", + ] + } + } + } + }); +}); + +test.serial("Build library.d (Custom Library preload configuration)", async (t) => { + const fixtureTester = new FixtureTester(t, "library.d"); + const destPath = fixtureTester.destPath; + + // In this test, we're testing the behavior of a custom preload configuration + // which is defined in "ui5-custom-preload-config.yaml". + // This custom preload configuration generates a library-preload.js similar to a default one. + // However, it will omit a resource ("some.js") from the bundle. + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "library.d": {} + } + } + }); + + // Check that generated preload bundle doesn't contain the omitted file: + t.false((await fs.readFile(`${destPath}/resources/library/d/library-preload.js`, {encoding: "utf8"})) + .includes("library/d/some.js")); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); +}); + +test.serial("Build theme.library.e project multiple times", async (t) => { + const fixtureTester = new FixtureTester(t, "theme.library.e"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false}, + assertions: { + projects: {"theme.library.e": {}} + } + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Change a source file in theme.library.e + const librarySourceFilePath = + `${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/library.source.less`; + await fs.appendFile(librarySourceFilePath, `\n.someNewClass {\n\tcolor: red;\n}\n`); + + // #3 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"theme.library.e": {}} + } + }); + + // Check whether the changed file is in the destPath + const builtFileContent = await fs.readFile( + `${destPath}/resources/theme/library/e/themes/my_theme/library.source.less`, {encoding: "utf8"} + ); + t.true( + builtFileContent.includes(`.someNewClass`), + "Build dest contains changed file content" + ); + + // Check whether the build output contains the new CSS rule + const builtCssContent = await fs.readFile( + `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"} + ); + t.true( + builtCssContent.includes(`.someNewClass`), + "Build dest contains new rule in library.css" + ); + + + // Add a new less file and import it in library.source.less + await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`, + `.someOtherNewClass {\n\tcolor: blue;\n}\n` + ); + await fs.appendFile(librarySourceFilePath, `\n@import "newImportFile.less";\n`); + + // #4 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"theme.library.e": {}}, + } + }); + + // Check whether the build output contains the import to the new file + const builtCssContent2 = await fs.readFile( + `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"} + ); + t.true( + builtCssContent2.includes(`.someOtherNewClass`), + "Build dest contains new rule in library.css" + ); + + + // #5 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {}, + } + }); + + + // Change content of new less file + await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`, + `.someOtherNewClass {\n\tcolor: green;\n}\n` + ); + + // #6 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"theme.library.e": {}}, + } + }); + + // Check whether the build output contains the changed content of the imported file + const builtCssContent3 = await fs.readFile( + `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"} + ); + t.true( + builtCssContent3.includes(`.someOtherNewClass{color:green}`), + "Build dest contains new rule in library.css" + ); + + + // Delete import of library.source.less + const librarySourceFileContent = (await fs.readFile(librarySourceFilePath)).toString(); + await fs.writeFile(librarySourceFilePath, + librarySourceFileContent.replace(`\n@import "newImportFile.less";\n`, "") + ); + + // Change content of new less file again + await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`, + `.someOtherNewClass {\n\tcolor: yellow;\n}\n` + ); + + // #7 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"theme.library.e": { + skippedTasks: ["buildThemes"] + }}, + } + }); + + // Check if library.css does NOT contain the imported rule anymore + t.false( + (await fs.readFile( + `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"} + )).includes(`.someOtherNewClass`), + "Build dest should NOT contain the rule in library.css anymore" + ); + + + // Delete the imported less file + await fs.rm(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`); + + // #8 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {}, // -> everything should be skipped + } + }); +}); + +test.serial("Build theme.library.e (with various dependencies)", async (t) => { + const fixtureTester = new FixtureTester(t, "theme.library.e"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {"theme.library.e": {}} + } + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Add a "library" dependency to theme.library.e: + await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme`); + + // #3 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.z": {}, + "theme.library.e": { + skippedTasks: [ + "buildThemes", + "replaceCopyright", + "replaceVersion", + ] + }, + } + } + }); +}); + +test.serial("Build component.a project multiple times", async (t) => { + const fixtureTester = new FixtureTester(t, "component.a"); + const destPath = fixtureTester.destPath; + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "component.a": {} + } + } + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Change a source file in component.a + const changedFilePath = `${fixtureTester.fixturePath}/src/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added");\n`); + + // #3 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "component.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + // Note: replaceCopyright is skipped because no copyright is configured in the project + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Check whether the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/resources/id1/test.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content"); + + + // #4 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + } + } + }); + + + // #5 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // #6 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Add a new file to component.a + await fs.writeFile(`${fixtureTester.fixturePath}/src/someNew.js`, + `console.log("SOME NEW CONTENT");\n` + ); + + // #7 build (with cache, with changes - someNew.js added) + // Tasks that don't depend on someNew.js can reuse their caches from build #3. + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"component.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + }} + } + }); + + await fs.rm(`${fixtureTester.fixturePath}/src/someNew.js`); + + // #8 build (with cache, with changes - someNew.js removed) + // Source state matches build #6's cached result -> cache reused, everything skipped + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {}, + } + }); +}); + +test.serial("Build component.a (with various dependencies)", async (t) => { + const fixtureTester = new FixtureTester(t, "component.a"); + const destPath = fixtureTester.destPath; + + // #1 build (with empty cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "component.a": {} + } + } + }); + + + // #2 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Add a "component" dependency to component.a: + await fixtureTester.addComponentDependency(`${fixtureTester.fixturePath}/src`); + + // #3 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "component.z": {}, + "component.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // Add a "library" dependency to component.a: + await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/src`); + + // #4 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.z": {}, + "component.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // Add a "themelib" dependency to component.a: + await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/src`); + + // #5 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "themelib.z": {}, + "component.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); + + + // Add a "module" dependency to component.a: + await fixtureTester.addModuleDependency(`${fixtureTester.fixturePath}/src`); + + // #6 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "module.z": {}, + "component.a": { + skippedTasks: [ + "enhanceManifest", + "escapeNonAsciiCharacters", + "generateFlexChangesBundle", + "replaceCopyright", + ] + } + } + } + }); +}); + +test.serial("Build component.a (Custom Component preload configuration)", async (t) => { + const fixtureTester = new FixtureTester(t, "component.a"); + const destPath = fixtureTester.destPath; + + // In this test, we're testing the behavior of a custom preload configuration + // which is defined in "ui5-custom-preload-config.yaml". + // This custom preload configuration generates a Component-preload.js similar to a default one. + // However, it will omit a resource ("test.js") from the bundle. + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "component.a": {} + } + } + }); + + // Check that generated preload bundle doesn't contain the omitted file: + t.false((await fs.readFile(`${destPath}/resources/id1/Component-preload.js`, {encoding: "utf8"})) + .includes("id1/test.js")); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); +}); + +test.serial("Build module.b project multiple times", async (t) => { + const fixtureTester = new FixtureTester(t, "module.b"); + const destPath = fixtureTester.destPath; + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"module.b": {}} + }, + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Change a source file in module.b + const changedFilePath = `${fixtureTester.fixturePath}/dev/devTools.js`; + await fs.appendFile(changedFilePath, `\ntest("line added");\n`); + + // #3 build (no cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"module.b": {}} + } + }); + + // Check whether the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/resources/b/module/dev/devTools.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content"); + + + // Remove a source file in module.b + await fs.rm(`${fixtureTester.fixturePath}/dev/devTools.js`); + + // #4 build (no cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"module.b": {}} + } + }); + + // Check that the removed file is NOT in the destPath anymore + // (dist output should be totally empty: no source files -> no build result) + await t.throwsAsync(fs.readFile(`${destPath}/resources/b/module/dev/devTools.js`, {encoding: "utf8"})); + + + // #5 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + + // Add a new file in module.b + await fs.mkdir(`${fixtureTester.fixturePath}/dev/newFolder`, {recursive: true}); + await fs.writeFile(`${fixtureTester.fixturePath}/dev/newFolder/newFile.js`, + `console.log("this is a new file which should be included in the build result")`); + + // #6 build (no cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"module.b": {}} + }, + }); + + // Check whether the added file is in the destPath + const newFile = await fs.readFile(`${destPath}/resources/b/module/dev/newFolder/newFile.js`, + {encoding: "utf8"}); + t.true(newFile.includes(`this is a new file which should be included in the build result`), + "Build dest contains correct file content"); + + + // Add a new path mapping: + const originalUi5Yaml = await fs.readFile(`${fixtureTester.fixturePath}/ui5.yaml`, {encoding: "utf8"}); // for later + const newFileName = "someOtherNewFile.js"; + const newFolderName = "newPathmapping"; + const virtualPath = `/resources/b/module/${newFolderName}/`; + await fs.writeFile(`${fixtureTester.fixturePath}/ui5.yaml`, + `--- +specVersion: "5.0" +type: module +metadata: + name: module.b +resources: + configuration: + paths: + /resources/b/module/dev/: dev + ${virtualPath}: ${newFolderName}` + ); + + // Create a resource for this new path mapping: + await fs.mkdir(`${fixtureTester.fixturePath}/${newFolderName}`, {recursive: true}); + await fs.writeFile(`${fixtureTester.fixturePath}/${newFolderName}/${newFileName}`, + `console.log("this should be included in the build result if the path mapping has been set")`); + + // #7 build (no cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {"module.b": {}} + }, + }); + + // Check whether the added file is in the destPath + const someOtherNewFile = await fs.readFile(`${destPath}${virtualPath}${newFileName}`, + {encoding: "utf8"}); + t.true(someOtherNewFile.includes(`path mapping has been set`), "Build dest contains correct file content"); + + + // Remove the path mapping again (revert original ui5.yaml): + await fs.writeFile(`${fixtureTester.fixturePath}/ui5.yaml`, originalUi5Yaml); + + // #8 build (with cache, with changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} // -> cache can be reused + }, + }); + + // Check that the added resource of the path mapping is NOT in the destPath anymore: + await t.throwsAsync(fs.readFile(`${destPath}${virtualPath}${newFileName}`, + {encoding: "utf8"})); + + + // #9 build (with cache, no changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + } + }, + }); +}); + +test.serial("Build module.b (with various dependencies)", async (t) => { + const fixtureTester = new FixtureTester(t, "module.b"); + const destPath = fixtureTester.destPath; + + // #1 build (no cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "module.b": {} + } + }, + }); + + + // #2 build (with cache, no changes) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: {} + } + }); + + + // Add a "library" dependency to module.b: + await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/dev`); + + // #3 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.z": {}, + "module.b": {} + } + }, + }); + + + // Add a "themelib" dependency to module.b: + await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/dev`); + + // #4 build (no cache, with changes, with dependencies) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "themelib.z": {}, + "module.b": {} + } + } + }); +}); + +test.serial("Build race condition: file modified during active build", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + await fixtureTester._initialize(); + const testFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + const originalContent = await fs.readFile(testFilePath, {encoding: "utf8"}); + const addedFileName = "added-during-build.js"; + const addedFilePath = `${fixtureTester.fixturePath}/webapp/${addedFileName}`; + + // #1 Build with race condition triggered by custom task that modifies test.js during the build. + // The build should detect the source change and throw. + const error1 = await t.throwsAsync(fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-race-condition.yaml"}, + config: {destPath, cleanDest: true}, + })); + t.true(error1.message.includes("Detected changes to source files of project application.a during the build"), + "Error message indicates source change detected"); + + // #2 Revert the source file to original content + await fs.writeFile(testFilePath, originalContent); + + // #3 Build again with normal config after reverting the source. + // Since the race condition build threw, no corrupted cache was written. + // This build should succeed and produce clean output. + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Verify the output does NOT contain the race condition modification + const finalBuiltContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.false( + finalBuiltContent.includes(`RACE CONDITION MODIFICATION`), + "Build output does not contain race condition modification after clean rebuild" + ); + + // #4 Build with race condition triggered by add-file custom task + await fs.rm(addedFilePath, {force: true}); + const error2 = await t.throwsAsync(fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-race-condition-add-file.yaml"}, + config: {destPath, cleanDest: true}, + })); + t.true(error2.message.includes("Detected changes to source files of project application.a during the build"), + "Error message indicates source change detected (add file)"); + + // #5 Revert source state by removing the file that was added during build + await fs.rm(addedFilePath, {force: true}); + + // #6 Build again with normal config after reverting. + // Cache from build #3 is still valid (same source state), so everything should be skipped. + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + // #7 Build with race condition triggered by delete-file custom task + const error3 = await t.throwsAsync(fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-race-condition-delete-file.yaml"}, + config: {destPath, cleanDest: true}, + })); + t.true(error3.message.includes("Detected changes to source files of project application.a during the build"), + "Error message indicates source change detected (delete file)"); + + // #8 Revert source state by restoring the deleted file + await fs.writeFile(testFilePath, originalContent); + + // #9 Build again with normal config after restoring. + // Cache from build #3 is still valid (same source state), so everything should be skipped. + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + // Verify test.js is present in output + const restoredBuiltFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true( + restoredBuiltFileContent.includes(`console.log`), + "Build output contains restored file after source recovery" + ); +}); + +test.serial("Build dependency race condition: frozen source reader protects against filesystem changes", + async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // Build with dependency-race-condition custom task and all dependencies included. + // library.d is built first → its sources are frozen in CAS. + // Then application.a builds, running the custom task that: + // 1. Reads library.d's some.js via the dependency reader (CAS-backed) + // 2. Modifies some.js on disk + // 3. Re-reads via the dependency reader + // 4. Asserts the content is still the original CAS-frozen content (not modified disk) + // 5. Restores the file on disk + // If the frozen reader is not working, the custom task throws and the build fails. + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-dependency-race-condition.yaml"}, + config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // Sanity check: verify library.d's some.js exists in build output + const builtContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"}); + t.truthy(builtContent, "library.d some.js exists in build output"); + }); + +test.serial("Build with dependencies: Verify sap-ui-version.json generation and regeneration", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + const versionInfoPath = `${destPath}/resources/sap-ui-version.json`; + + // Build #1: Full build with all dependencies in JSDoc mode + // JSDoc mode enables generateVersionInfo task which creates sap-ui-version.json + await fixtureTester.buildProject({ + config: { + destPath, + cleanDest: true, + jsdoc: "jsdoc", + dependencyIncludes: {includeAllDependencies: true} + }, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + const versionInfo1Content = await fs.readFile(versionInfoPath, {encoding: "utf8"}); + t.truthy(versionInfo1Content, "sap-ui-version.json should exist"); + const versionInfo1 = JSON.parse(versionInfo1Content); + + // Root project metadata + t.is(versionInfo1.name, "application.a", "Root project name"); + t.is(versionInfo1.version, "1.0.0", "Root project version"); + t.is(typeof versionInfo1.buildTimestamp, "string", "buildTimestamp is string"); + + // Libraries array + t.true(Array.isArray(versionInfo1.libraries), "libraries is array"); + const libraryNames = versionInfo1.libraries.map((lib) => lib.name).sort(); + t.deepEqual(libraryNames, ["library.a", "library.b", "library.c", "library.d"], + "Contains all dependency libraries"); + + // Each library has required fields + versionInfo1.libraries.forEach((lib) => { + t.is(typeof lib.name, "string", `Library ${lib.name} has name`); + t.is(typeof lib.version, "string", `Library ${lib.name} has version`); + t.is(typeof lib.buildTimestamp, "string", `Library ${lib.name} has buildTimestamp`); + }); + + const firstBuildTimestamp = versionInfo1.buildTimestamp; + + // Build #2: No changes, expect full cache hit + await fixtureTester.buildProject({ + config: { + destPath, + cleanDest: true, + jsdoc: "jsdoc", + dependencyIncludes: {includeAllDependencies: true} + }, + assertions: { + projects: {} // All projects cached + } + }); + + // Verify sap-ui-version.json was reused from cache (timestamp unchanged) + const versionInfo2Content = await fs.readFile(versionInfoPath, {encoding: "utf8"}); + const versionInfo2 = JSON.parse(versionInfo2Content); + t.is(versionInfo2.buildTimestamp, firstBuildTimestamp, + "buildTimestamp unchanged when cached (no source changes)"); +}); + +test.serial("Build application.a with --cache=Default", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 Build with empty cache --> all tasks execute + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 Build with valid cache, no changes --> nothing rebuilds (all cached) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for cache test");\n`); + + // #3 Build with valid cache, source changes --> only affected tasks rebuild + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added for cache test");`), + "Build dest contains changed file content"); +}); + +test.serial("Build application.a with --cache=Off", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 Build with cache=Off --> all tasks execute, cache not written + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Off}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 Build with cache=Off (again) --> all tasks execute again (no cache reuse) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Off}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #3 Build with cache=Default --> all tasks execute (no cache from previous builds) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #4 Build with cache=Default (again) --> nothing rebuilds (cache now exists) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: {} + } + }); + + // #5 Build with cache=Off --> all tasks execute (ignores existing cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Off}, + assertions: { + projects: { + "application.a": {} + } + } + }); +}); + +test.serial("Build application.a with --cache=ReadOnly", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 Build with cache=Default --> all tasks execute, cache written + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 Build with cache=ReadOnly, no changes --> nothing rebuilds (cache used) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.ReadOnly}, + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for ReadOnly test");\n`); + + // #3 Build with cache=ReadOnly --> affected tasks rebuild, BUT cache not updated + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.ReadOnly}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added for ReadOnly test");`), + "Build dest contains changed file content"); + + // #4 Build with cache=Default, no new changes --> rebuilds again (cache from #3 missing) + // This validates that ReadOnly didn't write the cache in step #3 + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); +}); + +test.serial("Build application.a with --cache=Force (1)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1: Build with cache=Default --> all tasks execute, cache written + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2: Build with cache=Force, no changes --> nothing rebuilds (cache used) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Force}, + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for Force test");\n`); + + // #3: Build with cache=Force --> ERROR (cache invalid due to source changes) + const error = await t.throwsAsync(async () => { + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Force}, + }); + }); + + t.truthy(error, "Build with Force mode should throw error when cache is stale"); + t.true(error.message.includes(`Cache is in "Force" mode but cache is stale for project application.a ` + + `due to 1 changed source file(s). ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.`)); +}); + +test.serial("Build application.a with --cache=Force (2)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1: Build with cache=Force on empty cache --> ERROR with clear message + const error = await t.throwsAsync(async () => { + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Force}, + }); + }); + + t.truthy(error, "Build with Force mode should throw error when cache is empty"); + t.true(error.message.includes(`Cache is in "Force" mode but no cache found for project application.a. ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.`)); +}); + +// FIXME: Currently failing at #2 Build assertion +test.serial("Build application.i and application.i.copy with --cache", async (t) => { + // This test covers a scenario with two projects depending on the same + // dependency (exact same content) "library.d". + // We want to verify that: + // - First, we only want to build application.i (with cache=Default) + // (caches for the root project and the dependency should be created) + // - Second, we build application.i.copy with cache=Default + // (since application.i.copy has the same dependency "library.d" + // with the exact same content, the cache for library.d can be reused. + + const fixtureTester1 = new FixtureTester(t, "application.i"); + const destPath1 = fixtureTester1.destPath; + + // #1: Build application.i with cache=Default + await fixtureTester1.buildProject({ + config: {destPath: destPath1, cleanDest: false, cache: Cache.Default, + dependencyIncludes: {includeAllDependencies: true} + }, + assertions: { + projects: { + "library.d": {}, + "application.i": {}, + }, + } + }); + + + // #2: Build application.i.copy with cache=Default + const fixtureTester2 = new FixtureTester(t, "application.i.copy"); + const destPath2 = fixtureTester2.destPath; + + await fixtureTester2.buildProject({ + config: {destPath: destPath2, cleanDest: false, cache: Cache.Default, + dependencyIncludes: {includeAllDependencies: true}}, + assertions: { + projects: { + // library.d should not be rebuilt + "application.i.copy": {}, + }, + } + }); +}); + +function getFixturePath(fixtureName) { + return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url)); +} + +function getTmpPath(folderName) { + return fileURLToPath(new URL(`../../tmp/ProjectBuilder/${folderName}`, import.meta.url)); +} + +async function rmrf(dirPath) { + return fs.rm(dirPath, {recursive: true, force: true}); +} + +class FixtureTester { + constructor(t, fixtureName) { + this._t = t; + this._sinon = t.context.sinon; + this._fixtureName = fixtureName; + this._initialized = false; + + // Public + this.fixturePath = getTmpPath(fixtureName); + this.destPath = getTmpPath(`${fixtureName}/dist`); + } + + async _initialize() { + if (this._initialized) { + return; + } + process.env.UI5_DATA_DIR = getTmpPath(`${this._fixtureName}/.ui5`); + await rmrf(this.fixturePath); // Clean up any previous test runs + await fs.cp(getFixturePath(this._fixtureName), this.fixturePath, {recursive: true}); + this._initialized = true; + } + + async buildProject({graphConfig = {}, config = {}, assertions} = {}) { + await this._initialize(); + this._sinon.resetHistory(); + + const graph = await graphFromPackageDependencies({ + ...graphConfig, + cwd: this.fixturePath, + }); + + // Execute the build + await graph.build(config); + + // Apply assertions if provided + if (assertions) { + this._assertBuild(assertions); + } + } + + _assertBuild(assertions) { + /** + * assertions object structure: + * { + * projects: { + * "projectName": { + * skippedTasks: ["task1", "task2"], + * }, + * // ... + * }, + * allProjects: ["projectName1", "projectName2"] + * } + * + * projects - for asserting all projects which are expected to be built + * allProjects - optional, for asserting all seen projects nonetheless if built or not + */ + const {projects = {}, allProjects = []} = assertions; + + const projectsInOrder = []; + const seenProjects = new Set(); + const tasksByProject = {}; + + // Extract build status to identify built projects and their order + const buildStatusEvents = this._t.context.buildStatusEventStub.args.map((args) => args[0]); + for (const event of buildStatusEvents) { + if (!seenProjects.has(event.projectName)) { + seenProjects.add(event.projectName); + if (event.status === "project-build-start") { + projectsInOrder.push(event.projectName); + } + } + } + + // Extract task status to identify skipped & executed tasks per project + const projectBuildStatusEvents = this._t.context.projectBuildStatusEventStub.args.map((args) => args[0]); + for (const event of projectBuildStatusEvents) { + if (!tasksByProject[event.projectName]) { + tasksByProject[event.projectName] = {executed: [], skipped: []}; + } + if (event.status === "task-skip") { + tasksByProject[event.projectName].skipped.push(event.taskName); + } else if (event.status === "task-start") { + tasksByProject[event.projectName].executed.push(event.taskName); + } + } + + // Assert built projects in order + const expectedProjects = Object.keys(projects); + this._t.deepEqual(projectsInOrder, expectedProjects); + + // Optional check: Assert seen projects + if (allProjects.length > 0) { + const expectedAllProjects = allProjects.sort(); + const actualAllProjects = Array.from(seenProjects).sort(); + this._t.deepEqual(actualAllProjects, expectedAllProjects, + "All seen projects (built or not) should match expected"); + } + + // Assert skipped tasks per project + for (const [projectName, expectedSkipped] of Object.entries(projects)) { + const skippedTasks = expectedSkipped.skippedTasks || []; + const actualSkipped = (tasksByProject[projectName]?.skipped || []).sort(); + const expectedArray = skippedTasks.sort(); + this._t.deepEqual(actualSkipped, expectedArray); + } + } + + /** + * Helper function to add a new module dependency ("module.z") to an arbitrary root project. + * + * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications) + */ + async addModuleDependency(sourceDir) { + await fs.mkdir(`${this.fixturePath}/node_modules/module.z/dev`, {recursive: true}); + await fs.writeFile(`${this.fixturePath}/node_modules/module.z/dev/devTools.js`, + `console.log("module.z devTools");`); + await fs.writeFile(`${this.fixturePath}/node_modules/module.z/package.json`, + `{ + "name": "module.z", + "version": "1.0.0" +}` + ); + await fs.writeFile(`${this.fixturePath}/node_modules/module.z/ui5.yaml`, + `--- +specVersion: "5.0" +type: module +metadata: + name: module.z +resources: + configuration: + paths: + /resources/z/module/dev/: dev`); + + await fs.writeFile(`${sourceDir}/moduleConsumer.js`, + `sap.ui.define(["z/module/dev/devTools"], () => {});`); + const packageJsonContent = JSON.parse( + await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"})); + if (!packageJsonContent.dependencies) { + packageJsonContent.dependencies = {}; + } + packageJsonContent.dependencies["module.z"] = "file:../module.z"; + await fs.writeFile(`${this.fixturePath}/package.json`, + JSON.stringify(packageJsonContent) + ); + } + + /** + * Helper function to add a new component dependency ("component.z") to an arbitrary root project. + * + * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications) + */ + async addComponentDependency(sourceDir) { + await fs.mkdir(`${this.fixturePath}/node_modules/component.z/src`, {recursive: true}); + await fs.writeFile(`${this.fixturePath}/node_modules/component.z/src/Component.js`, + `sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('component.z.Component', { + createContent: function () { + return new Label({ text: "Hello!" }); + } + }); +}); +`); + await fs.writeFile(`${this.fixturePath}/node_modules/component.z/src/manifest.json`, + `{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "component.z", + "type": "component", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +}`); + await fs.writeFile(`${this.fixturePath}/node_modules/component.z/ui5.yaml`, + `--- +specVersion: "5.0" +type: component +metadata: + name: component.z`); + await fs.writeFile(`${this.fixturePath}/node_modules/component.z/package.json`, + `{ + "name": "component.z", + "version": "1.0.0" +}` + ); + + await fs.writeFile(`${sourceDir}/componentConsumer.js`, + `sap.ui.define(["component/z"], () => {});`); + const packageJsonContent = JSON.parse( + await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"})); + if (!packageJsonContent.dependencies) { + packageJsonContent.dependencies = {}; + } + packageJsonContent.dependencies["component.z"] = "file:../component.z"; + await fs.writeFile(`${this.fixturePath}/package.json`, + JSON.stringify(packageJsonContent) + ); + } + + /** + * Helper function to add a new library dependency ("library.z") to an arbitrary root project. + * + * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications) + */ + async addLibraryDependency(sourceDir) { + await fs.mkdir(`${this.fixturePath}/node_modules/library.z/src/library/z`, {recursive: true}); + await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/library.js`, + ` +sap.ui.define([ + "sap/base/util/ObjectPath", + "sap/ui/core/Core", + "sap/ui/core/library" +], function (ObjectPath, Core) { + "use strict"; + + Core.initLibrary({ + name: "library.z", + version: ` + "\"${version}\"" + `, + dependencies: [ + "sap.ui.core" + ], + types: [ + "library.z.ExampleColor" + ], + interfaces: [], + elements: [], + noLibraryCSS: false + }); + const thisLib = ObjectPath.get("library.z"); + + thisLib.ExampleColor = { + Default : "Default", + Highlight : "Highlight" + }; + return thisLib; +});`); + await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/.library`, + ` + + + library.z + SAP SE + Some fancy copyright + `+"${version}"+` + + Library Z + +`); + await fs.writeFile(`${this.fixturePath}/node_modules/library.z/ui5.yaml`, + `--- +specVersion: "5.0" +type: library +metadata: + name: library.z +`); + await fs.writeFile(`${this.fixturePath}/node_modules/library.z/package.json`, + `{ + "name": "library.z", + "version": "1.0.0" +}` + ); + + await fs.writeFile(`${sourceDir}/libraryConsumer.js`, + `sap.ui.define(["library/z/library"], + (LibraryZ) => { + console.log(LibraryZ.ExampleColor.Default); +});`); + const packageJsonContent = JSON.parse( + await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"})); + if (!packageJsonContent.dependencies) { + packageJsonContent.dependencies = {}; + } + packageJsonContent.dependencies["library.z"] = "file:../library.z"; + await fs.writeFile(`${this.fixturePath}/package.json`, + JSON.stringify(packageJsonContent) + ); + } + + /** + * Helper function to add a new theme library dependency ("themelib.z") to an arbitrary root project. + * + * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications) + */ + async addThemeLibraryDependency(sourceDir) { + await fs.mkdir(`${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme`, {recursive: true}); + await fs.writeFile( + `${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme/library.source.less`, + `@mycolor: blue; +.sapUiBody { + background-color: @mycolor; +}`); + await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme/.theme`, + ` + + my_theme + me + ` +"\"${copyright}\"" + ` + ` +"\"${version}\"" + ` +`); + await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/ui5.yaml`, + `--- +specVersion: "5.0" +type: theme-library +metadata: + name: themelib.z +`); + await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/package.json`, + `{ + "name": "themelib.z", + "version": "1.0.0" +}` + ); + + await fs.writeFile(`${sourceDir}/themelibConsumer.js`, + `sap.ui.define(["sap/ui/core/Theming"], (Theming) => { + Theming.setTheme("my_theme"); + console.log(Theming.getTheme()); +});`); + const packageJsonContent = JSON.parse( + await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"})); + if (!packageJsonContent.dependencies) { + packageJsonContent.dependencies = {}; + } + packageJsonContent.dependencies["themelib.z"] = "file:../themelib.z"; + await fs.writeFile(`${this.fixturePath}/package.json`, + JSON.stringify(packageJsonContent) + ); + } +} diff --git a/packages/project/test/lib/build/ProjectBuilder.js b/packages/project/test/lib/build/ProjectBuilder.js index 64b36ab85e9..d1255d8224b 100644 --- a/packages/project/test/lib/build/ProjectBuilder.js +++ b/packages/project/test/lib/build/ProjectBuilder.js @@ -16,6 +16,8 @@ function getMockProject(type, id = "b") { getVersion: noop, getReader: () => "reader", getWorkspace: () => "workspace", + sealWorkspace: noop, + createNewWorkspaceVersion: noop, }; } @@ -67,6 +69,13 @@ test.beforeEach(async (t) => { project: getMockProject("library", "c") }); }, + traverseDependenciesDepthFirst: sinon.stub().callsFake(function* (includeRoot) { + if (includeRoot) { + yield {project: getMockProject("application", "a")}; + } + yield {project: getMockProject("library", "b")}; + yield {project: getMockProject("library", "c")}; + }), getProject: sinon.stub().callsFake((projectName) => { return getMockProject(...projectName.split(".")); }) @@ -100,20 +109,23 @@ test("build", async (t) => { const builder = new ProjectBuilder({graph, taskRepository}); const filterProjectStub = sinon.stub().returns(true); - const getProjectFilterStub = sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); + sinon.stub(builder, "_createProjectFilter").returns(filterProjectStub); const requiresBuildStub = sinon.stub().returns(true); - const runTasksStub = sinon.stub().resolves(); + const possiblyRequiresBuildStub = sinon.stub().returns(true); + const prepareProjectBuildAndValidateCacheStub = sinon.stub().resolves(false); + const buildProjectStub = sinon.stub().resolves(); + const writeBuildCacheStub = sinon.stub().resolves(); const projectBuildContextMock = { - getTaskRunner: () => { - return { - runTasks: runTasksStub, - }; - }, + possiblyRequiresBuild: possiblyRequiresBuildStub, + prepareProjectBuildAndValidateCache: prepareProjectBuildAndValidateCacheStub, + buildProject: buildProjectStub, + writeBuildCache: writeBuildCacheStub, requiresBuild: requiresBuildStub, - getProject: sinon.stub().returns(getMockProject("library")) + getProject: sinon.stub().returns(getMockProject("library")), + buildFinished: sinon.stub() }; - const createRequiredBuildContextsStub = sinon.stub(builder, "_createRequiredBuildContexts") + const getRequiredProjectContextsStub = sinon.stub(builder._buildContext, "getRequiredProjectContexts") .resolves(new Map().set("project.a", projectBuildContextMock)); const registerCleanupSigHooksStub = sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks"); @@ -122,28 +134,21 @@ test("build", async (t) => { const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks"); const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves(); - await builder.build({ + await builder.buildToTarget({ destPath: "dest/path", includedDependencies: ["dep a"], excludedDependencies: ["dep b"] }); - t.is(getProjectFilterStub.callCount, 1, "_getProjectFilter got called once"); - t.deepEqual(getProjectFilterStub.getCall(0).args[0], { - explicitIncludes: ["dep a"], - explicitExcludes: ["dep b"], - dependencyIncludes: undefined - }, "_getProjectFilter got called with correct arguments"); - - t.is(createRequiredBuildContextsStub.callCount, 1, "_createRequiredBuildContexts got called once"); - t.deepEqual(createRequiredBuildContextsStub.getCall(0).args[0], [ + t.is(getRequiredProjectContextsStub.callCount, 1, "getRequiredProjectContexts got called once"); + t.deepEqual(getRequiredProjectContextsStub.getCall(0).args[0], [ "project.a", "project.b", "project.c" - ], "_createRequiredBuildContexts got called with correct arguments"); + ], "getRequiredProjectContexts got called with correct arguments"); - t.is(requiresBuildStub.callCount, 1, "ProjectBuildContext#requiresBuild got called once"); + t.is(possiblyRequiresBuildStub.callCount, 1, "ProjectBuildContext#possiblyRequiresBuild got called once"); t.is(registerCleanupSigHooksStub.callCount, 1, "_registerCleanupSigHooksStub got called once"); - t.is(runTasksStub.callCount, 1, "TaskRunner#runTasks got called once"); + t.is(buildProjectStub.callCount, 1, "ProjectBuildContext#buildProject got called once"); t.is(writeResultsStub.callCount, 1, "_writeResults got called once"); t.is(writeResultsStub.getCall(0).args[0], projectBuildContextMock, @@ -151,18 +156,20 @@ test("build", async (t) => { t.is(writeResultsStub.getCall(0).args[1]._fsBasePath, path.resolve("dest/path") + path.sep, "_writeResults got called with correct second argument"); + t.is(writeBuildCacheStub.callCount, 1, "writeBuildCache got called once"); + t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once"); t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks", "_deregisterCleanupSigHooks got called with correct arguments"); t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once"); }); -test("build: Missing dest parameter", async (t) => { +test("build: Conflicting dependency parameters", async (t) => { const {graph, taskRepository, ProjectBuilder} = t.context; const builder = new ProjectBuilder({graph, taskRepository}); - const err = await t.throwsAsync(builder.build({ + const err = await t.throwsAsync(builder.buildToTarget({ destPath: "dest/path", dependencyIncludes: "dependencyIncludes", includedDependencies: ["dep a"], @@ -180,7 +187,7 @@ test("build: Too many dependency parameters", async (t) => { const builder = new ProjectBuilder({graph, taskRepository}); - const err = await t.throwsAsync(builder.build({ + const err = await t.throwsAsync(builder.buildToTarget({ includedDependencies: ["dep a"], excludedDependencies: ["dep b"] })); @@ -198,8 +205,8 @@ test("build: createBuildManifest in conjunction with dependencies", async (t) => }); const filterProjectStub = sinon.stub().returns(true); - sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); - const err = await t.throwsAsync(builder.build({ + sinon.stub(builder, "_createProjectFilter").returns(filterProjectStub); + const err = await t.throwsAsync(builder.buildToTarget({ destPath: "dest/path", includedDependencies: ["dep a"] })); @@ -216,20 +223,18 @@ test("build: Failure", async (t) => { const builder = new ProjectBuilder({graph, taskRepository}); const filterProjectStub = sinon.stub().returns(true); - sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); + sinon.stub(builder, "_createProjectFilter").returns(filterProjectStub); - const requiresBuildStub = sinon.stub().returns(true); - const runTasksStub = sinon.stub().rejects(new Error("Some Error")); + const possiblyRequiresBuildStub = sinon.stub().returns(true); + const prepareProjectBuildAndValidateCacheStub = sinon.stub().resolves(false); + const buildProjectStub = sinon.stub().rejects(new Error("Some Error")); const projectBuildContextMock = { - requiresBuild: requiresBuildStub, - getTaskRunner: () => { - return { - runTasks: runTasksStub - }; - }, + possiblyRequiresBuild: possiblyRequiresBuildStub, + prepareProjectBuildAndValidateCache: prepareProjectBuildAndValidateCacheStub, + buildProject: buildProjectStub, getProject: sinon.stub().returns(getMockProject("library")) }; - sinon.stub(builder, "_createRequiredBuildContexts") + sinon.stub(builder._buildContext, "getRequiredProjectContexts") .resolves(new Map().set("project.a", projectBuildContextMock)); sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks"); @@ -237,7 +242,7 @@ test("build: Failure", async (t) => { const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks"); const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves(); - const err = await t.throwsAsync(builder.build({ + const err = await t.throwsAsync(builder.buildToTarget({ destPath: "dest/path", includedDependencies: ["dep a"], excludedDependencies: ["dep b"] @@ -279,45 +284,38 @@ test.serial("build: Multiple projects", async (t) => { const builder = new ProjectBuilder({graph, taskRepository}); const filterProjectStub = sinon.stub().returns(true).onFirstCall().returns(false); - const getProjectFilterStub = sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); - - const requiresBuildAStub = sinon.stub().returns(true); - const requiresBuildBStub = sinon.stub().returns(false); - const requiresBuildCStub = sinon.stub().returns(true); - const getBuildMetadataStub = sinon.stub().returns({ - timestamp: "2022-07-28T12:00:00.000Z", - age: "xx days" - }); - const runTasksStub = sinon.stub().resolves(); + sinon.stub(builder, "_createProjectFilter").returns(filterProjectStub); + + const buildProjectAStub = sinon.stub().resolves(); + const buildProjectBStub = sinon.stub().resolves(); + const buildProjectCStub = sinon.stub().resolves(); + const writeBuildCacheStub = sinon.stub().resolves(); + const projectBuildContextMockA = { - getTaskRunner: () => { - return { - runTasks: runTasksStub - }; - }, - requiresBuild: requiresBuildAStub, - getProject: sinon.stub().returns(getMockProject("library", "a")) + possiblyRequiresBuild: sinon.stub().returns(true), + prepareProjectBuildAndValidateCache: sinon.stub().resolves(false), + buildProject: buildProjectAStub, + writeBuildCache: writeBuildCacheStub, + getProject: sinon.stub().returns(getMockProject("library", "a")), + buildFinished: sinon.stub() }; const projectBuildContextMockB = { - getTaskRunner: () => { - return { - runTasks: runTasksStub - }; - }, - getBuildMetadata: getBuildMetadataStub, - requiresBuild: requiresBuildBStub, - getProject: sinon.stub().returns(getMockProject("library", "b")) + possiblyRequiresBuild: sinon.stub().returns(false), + prepareProjectBuildAndValidateCache: sinon.stub().resolves(false), + buildProject: buildProjectBStub, + writeBuildCache: writeBuildCacheStub, + getProject: sinon.stub().returns(getMockProject("library", "b")), + buildFinished: sinon.stub() }; const projectBuildContextMockC = { - getTaskRunner: () => { - return { - runTasks: runTasksStub - }; - }, - requiresBuild: requiresBuildCStub, - getProject: sinon.stub().returns(getMockProject("library", "c")) + possiblyRequiresBuild: sinon.stub().returns(true), + prepareProjectBuildAndValidateCache: sinon.stub().resolves(false), + buildProject: buildProjectCStub, + writeBuildCache: writeBuildCacheStub, + getProject: sinon.stub().returns(getMockProject("library", "c")), + buildFinished: sinon.stub() }; - const createRequiredBuildContextsStub = sinon.stub(builder, "_createRequiredBuildContexts") + const getRequiredProjectContextsStub = sinon.stub(builder._buildContext, "getRequiredProjectContexts") .resolves(new Map() .set("project.a", projectBuildContextMockA) .set("project.b", projectBuildContextMockB) @@ -330,30 +328,22 @@ test.serial("build: Multiple projects", async (t) => { const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves(); setLogLevel("verbose"); - await builder.build({ + await builder.buildToTarget({ destPath: path.join("dest", "path"), dependencyIncludes: "dependencyIncludes" }); setLogLevel("info"); - t.is(getProjectFilterStub.callCount, 1, "_getProjectFilter got called once"); - t.deepEqual(getProjectFilterStub.getCall(0).args[0], { - explicitIncludes: [], - explicitExcludes: [], - dependencyIncludes: "dependencyIncludes" - }, "_getProjectFilter got called with correct arguments"); - - t.is(createRequiredBuildContextsStub.callCount, 1, "_createRequiredBuildContexts got called once"); - t.deepEqual(createRequiredBuildContextsStub.getCall(0).args[0], [ + t.is(getRequiredProjectContextsStub.callCount, 1, "getRequiredProjectContexts got called once"); + t.deepEqual(getRequiredProjectContextsStub.getCall(0).args[0], [ "project.b", "project.c" - ], "_createRequiredBuildContexts got called with correct arguments"); + ], "getRequiredProjectContexts got called with correct arguments"); - t.is(requiresBuildAStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.a"); - t.is(requiresBuildBStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.b"); - t.is(requiresBuildCStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.c"); t.is(registerCleanupSigHooksStub.callCount, 1, "_registerCleanupSigHooksStub got called once"); - t.is(runTasksStub.callCount, 2, "TaskRunner#runTasks got called twice"); // library.b does not require a build + t.is(buildProjectAStub.callCount, 1, "buildProject got called once for library.a"); + t.is(buildProjectBStub.callCount, 0, "buildProject not called for library.b (possiblyRequiresBuild = false)"); + t.is(buildProjectCStub.callCount, 1, "buildProject got called once for library.c"); t.is(writeResultsStub.callCount, 2, "_writeResults got called twice"); // library.a has not been requested t.is(writeResultsStub.getCall(0).args[0], projectBuildContextMockB, @@ -394,47 +384,10 @@ test.serial("build: Multiple projects", async (t) => { "BuildLogger#skipProjectBuild got called with expected argument"); }); -test("_createRequiredBuildContexts", async (t) => { - const {graph, taskRepository, ProjectBuilder, sinon} = t.context; - - const builder = new ProjectBuilder({graph, taskRepository}); - - const requiresBuildStub = sinon.stub().returns(true); - const getRequiredDependenciesStub = sinon.stub() - .returns(new Set()) - .onFirstCall().returns(new Set(["project.b"])); // required dependency of project.a - - const projectBuildContextMock = { - requiresBuild: requiresBuildStub, - getTaskRunner: () => { - return { - getRequiredDependencies: getRequiredDependenciesStub - }; - } - }; - const createProjectContextStub = sinon.stub(builder._buildContext, "createProjectContext") - .returns(projectBuildContextMock); - const projectBuildContexts = await builder._createRequiredBuildContexts(["project.a", "project.c"]); - - t.is(requiresBuildStub.callCount, 3, "TaskRunner#requiresBuild got called three times"); - t.is(getRequiredDependenciesStub.callCount, 3, "TaskRunner#getRequiredDependencies got called three times"); - - t.deepEqual(Object.fromEntries(projectBuildContexts), { - "project.a": projectBuildContextMock, - "project.b": projectBuildContextMock, // is a required dependency of project.a - "project.c": projectBuildContextMock, - }, "Returned expected project build contexts"); - - t.is(createProjectContextStub.callCount, 3, "BuildContext#createProjectContextStub got called three times"); - t.is(createProjectContextStub.getCall(0).args[0].project.getName(), "project.a", - "First call to BuildContext#createProjectContextStub with expected project"); - t.is(createProjectContextStub.getCall(1).args[0].project.getName(), "project.c", - "Second call to BuildContext#createProjectContextStub with expected project"); - t.is(createProjectContextStub.getCall(2).args[0].project.getName(), "project.b", - "Third call to BuildContext#createProjectContextStub with expected project"); -}); +// _createRequiredBuildContexts is now part of BuildContext, not ProjectBuilder +// This logic is tested through integration tests -test.serial("_getProjectFilter with dependencyIncludes", async (t) => { +test.serial("_createProjectFilter with dependencyIncludes", async (t) => { const {graph, taskRepository, sinon} = t.context; const composeProjectListStub = sinon.stub().returns({ includedDependencies: ["project.b", "project.c"], @@ -446,7 +399,7 @@ test.serial("_getProjectFilter with dependencyIncludes", async (t) => { const builder = new ProjectBuilder({graph, taskRepository}); - const filterProject = await builder._getProjectFilter({ + const filterProject = builder._createProjectFilter({ dependencyIncludes: "dependencyIncludes", explicitIncludes: "explicitIncludes", explicitExcludes: "explicitExcludes", @@ -465,7 +418,7 @@ test.serial("_getProjectFilter with dependencyIncludes", async (t) => { t.false(filterProject("project.e"), "project.e is not allowed"); }); -test.serial("_getProjectFilter with explicit include/exclude", async (t) => { +test.serial("_createProjectFilter with explicit include/exclude", async (t) => { const {graph, taskRepository, sinon} = t.context; const composeProjectListStub = sinon.stub().returns({ includedDependencies: ["project.b", "project.c"], @@ -477,7 +430,7 @@ test.serial("_getProjectFilter with explicit include/exclude", async (t) => { const builder = new ProjectBuilder({graph, taskRepository}); - const filterProject = await builder._getProjectFilter({ + const filterProject = builder._createProjectFilter({ explicitIncludes: "explicitIncludes", explicitExcludes: "explicitExcludes", }); @@ -543,7 +496,9 @@ test("_writeResults", async (t) => { write: sinon.stub().resolves() }; - await builder._writeResults(projectBuildContextMock, writerMock); + const deferredWork = []; + await builder._writeResults(projectBuildContextMock, writerMock, deferredWork); + await Promise.all(deferredWork); t.is(getReaderStub.callCount, 1, "One reader requested"); t.deepEqual(getReaderStub.getCall(0).args[0], { @@ -608,6 +563,7 @@ test.serial("_writeResults: Create build manifest", async (t) => { const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true); const projectBuildContextMock = { getProject: () => mockProject, + getBuildSignature: () => "build-signature", getTaskUtil: () => { return { isRootProject: () => true, @@ -622,7 +578,9 @@ test.serial("_writeResults: Create build manifest", async (t) => { write: sinon.stub().resolves() }; - await builder._writeResults(projectBuildContextMock, writerMock); + const deferredWork = []; + await builder._writeResults(projectBuildContextMock, writerMock, deferredWork); + await Promise.all(deferredWork); t.is(getReaderStub.callCount, 1, "One reader requested"); t.deepEqual(getReaderStub.getCall(0).args[0], { @@ -644,6 +602,10 @@ test.serial("_writeResults: Create build manifest", async (t) => { jsdoc: false, selfContained: false, }, "createBuildManifest got called with correct build configuration"); + t.is(createBuildManifestStub.getCall(0).args[2], taskRepository, + "createBuildManifest got called with correct taskRepository"); + t.is(createBuildManifestStub.getCall(0).args[3], "build-signature", + "createBuildManifest got called with correct buildSignature"); t.is(createResourceStub.callCount, 1, "One resource has been created"); t.deepEqual(createResourceStub.getCall(0).args[0], { @@ -716,7 +678,9 @@ test.serial("_writeResults: Flat build output", async (t) => { write: sinon.stub().resolves() }; - await builder._writeResults(projectBuildContextMock, writerMock); + const deferredWork = []; + await builder._writeResults(projectBuildContextMock, writerMock, deferredWork); + await Promise.all(deferredWork); t.is(getReaderStub.callCount, 2, "One reader requested"); t.deepEqual(getReaderStub.getCall(0).args[0], { diff --git a/packages/project/test/lib/build/TaskRunner.js b/packages/project/test/lib/build/TaskRunner.js index 84b46bb9bbe..3c5ad5e7765 100644 --- a/packages/project/test/lib/build/TaskRunner.js +++ b/packages/project/test/lib/build/TaskRunner.js @@ -57,8 +57,14 @@ function getMockProject(type) { getCachebusterSignatureType: noop, getCustomTasks: () => [], hasBuildManifest: () => false, - getWorkspace: () => "workspace", - isFrameworkProject: () => false + getWorkspace: () => { + return { + getName: () => "workspace" + }; + }, + isFrameworkProject: () => false, + sealWorkspace: noop, + createNewWorkspaceVersion: noop, }; } @@ -92,6 +98,7 @@ test.beforeEach(async (t) => { }; }, getRequiredDependenciesCallback: t.context.getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false), }; t.context.graph = { @@ -113,14 +120,35 @@ test.beforeEach(async (t) => { setTasks: sinon.stub(), startTask: sinon.stub(), endTask: sinon.stub(), + skipTask: sinon.stub(), verbose: sinon.stub(), perf: sinon.stub(), isLevelEnabled: sinon.stub().returns(true), }; + t.context.buildCache = { + setTasks: sinon.stub(), + prepareTaskExecutionAndValidateCache: sinon.stub().resolves(false), + recordTaskResult: sinon.stub().resolves(), + allTasksCompleted: sinon.stub().resolves([]), + prefetchStageCache: sinon.stub(), + }; + t.context.resourceFactory = { createReaderCollection: sinon.stub() - .returns("reader collection") + .returns({getName: () => "reader collection"}), + createMonitor: sinon.stub().callsFake((resource) => { + // Return a MonitoredReader-like object with both getName and getResourceRequests + if (resource && typeof resource.getName === "function") { + const name = resource.getName(); + return { + constructor: {name: "MonitoredReader"}, + getName: () => name, + getResourceRequests: sinon.stub().returns([]) + }; + } + return resource; + }) }; t.context.TaskRunner = await esmock("../../../lib/build/TaskRunner.js", { @@ -134,7 +162,7 @@ test.afterEach.always((t) => { }); test("Missing parameters", (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; t.throws(() => { new TaskRunner({ graph, @@ -152,6 +180,7 @@ test("Missing parameters", (t) => { taskUtil, taskRepository, log: projectBuildLogger, + buildCache, buildConfig }); }, { @@ -163,6 +192,7 @@ test("Missing parameters", (t) => { graph, taskRepository, log: projectBuildLogger, + buildCache, buildConfig }); }, { @@ -174,6 +204,7 @@ test("Missing parameters", (t) => { graph, taskUtil, log: projectBuildLogger, + buildCache, buildConfig }); }, { @@ -197,6 +228,7 @@ test("Missing parameters", (t) => { taskUtil, taskRepository, log: projectBuildLogger, + buildCache, }); }, { message: "TaskRunner: One or more mandatory parameters not provided" @@ -204,9 +236,10 @@ test("Missing parameters", (t) => { }); test("_initTasks: Project of type 'application'", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskRunner = new TaskRunner({ - project: getMockProject("application"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project: getMockProject("application"), graph, taskUtil, taskRepository, + log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); t.deepEqual(taskRunner._taskExecutionOrder, [ @@ -228,9 +261,10 @@ test("_initTasks: Project of type 'application'", async (t) => { }); test("_initTasks: Project of type 'library'", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskRunner = new TaskRunner({ - project: getMockProject("library"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project: getMockProject("library"), graph, taskUtil, taskRepository, + log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -254,13 +288,13 @@ test("_initTasks: Project of type 'library'", async (t) => { }); test("_initTasks: Project of type 'library' (framework project)", async (t) => { - const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context; + const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner, buildCache} = t.context; const project = getMockProject("library"); project.isFrameworkProject = () => true; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -284,9 +318,10 @@ test("_initTasks: Project of type 'library' (framework project)", async (t) => { }); test("_initTasks: Project of type 'theme-library'", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskRunner = new TaskRunner({ - project: getMockProject("theme-library"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project: getMockProject("theme-library"), graph, taskUtil, taskRepository, + log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -300,13 +335,13 @@ test("_initTasks: Project of type 'theme-library'", async (t) => { }); test("_initTasks: Project of type 'theme-library' (framework project)", async (t) => { - const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context; + const {graph, taskUtil, taskRepository, projectBuildLogger, buildCache, TaskRunner} = t.context; const project = getMockProject("theme-library"); project.isFrameworkProject = () => true; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -320,9 +355,10 @@ test("_initTasks: Project of type 'theme-library' (framework project)", async (t }); test("_initTasks: Project of type 'module'", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskRunner = new TaskRunner({ - project: getMockProject("module"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project: getMockProject("module"), graph, taskUtil, taskRepository, + log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -330,9 +366,10 @@ test("_initTasks: Project of type 'module'", async (t) => { }); test("_initTasks: Unknown project type", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskRunner = new TaskRunner({ - project: getMockProject("pony"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project: getMockProject("pony"), graph, taskUtil, taskRepository, + log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(taskRunner._initTasks()); @@ -340,14 +377,14 @@ test("_initTasks: Unknown project type", async (t) => { }); test("_initTasks: Custom tasks", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", afterTask: "minify"}, {name: "myOtherTask", beforeTask: "replaceVersion"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); t.deepEqual(taskRunner._taskExecutionOrder, [ @@ -371,14 +408,14 @@ test("_initTasks: Custom tasks", async (t) => { }); test("_initTasks: Custom tasks with no standard tasks", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); project.getCustomTasks = () => [ {name: "myTask"}, {name: "myOtherTask", beforeTask: "myTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); t.deepEqual(taskRunner._taskExecutionOrder, [ @@ -388,14 +425,14 @@ test("_initTasks: Custom tasks with no standard tasks", async (t) => { }); test("_initTasks: Custom tasks with no standard tasks and second task defining no before-/afterTask", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); project.getCustomTasks = () => [ {name: "myTask"}, {name: "myOtherTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -407,13 +444,13 @@ test("_initTasks: Custom tasks with no standard tasks and second task defining n }); test("_initTasks: Custom tasks with both, before- and afterTask reference", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", beforeTask: "minify", afterTask: "replaceVersion"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -425,13 +462,13 @@ test("_initTasks: Custom tasks with both, before- and afterTask reference", asyn }); test("_initTasks: Custom tasks with no before-/afterTask reference", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -443,13 +480,13 @@ test("_initTasks: Custom tasks with no before-/afterTask reference", async (t) = }); test("_initTasks: Custom tasks without name", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: ""} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -460,13 +497,13 @@ test("_initTasks: Custom tasks without name", async (t) => { }); test("_initTasks: Custom task with name of standard tasks", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "replaceVersion", afterTask: "minify"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -478,7 +515,7 @@ test("_initTasks: Custom task with name of standard tasks", async (t) => { }); test("_initTasks: Multiple custom tasks with same name", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", afterTask: "minify"}, @@ -486,7 +523,7 @@ test("_initTasks: Multiple custom tasks with same name", async (t) => { {name: "myTask", afterTask: "minify"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); t.deepEqual(taskRunner._taskExecutionOrder, [ @@ -511,13 +548,13 @@ test("_initTasks: Multiple custom tasks with same name", async (t) => { }); test("_initTasks: Custom tasks with unknown beforeTask", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", beforeTask: "unknownTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -529,13 +566,13 @@ test("_initTasks: Custom tasks with unknown beforeTask", async (t) => { }); test("_initTasks: Custom tasks with unknown afterTask", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", afterTask: "unknownTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -547,14 +584,14 @@ test("_initTasks: Custom tasks with unknown afterTask", async (t) => { }); test("_initTasks: Custom tasks is unknown", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; graph.getExtension.returns(undefined); const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", afterTask: "minify"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -566,13 +603,13 @@ test("_initTasks: Custom tasks is unknown", async (t) => { }); test("_initTasks: Custom tasks with removed beforeTask", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getCustomTasks = () => [ {name: "myTask", beforeTask: "removedTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); const err = await t.throwsAsync(async () => { await taskRunner._initTasks(); @@ -585,12 +622,16 @@ test("_initTasks: Custom tasks with removed beforeTask", async (t) => { }); test("_initTasks: Create dependencies reader for all dependencies", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); + // Dependencies reader is now created lazily via getDependenciesReader + // Use forceUpdate=true to bypass the cache shortcut and actually trigger graph traversal + const readerPromise = taskRunner.getDependenciesReader(new Set(["dep.a", "dep.b"]), true); + // Verify traverseBreadthFirst was called t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst called once"); t.is(graph.traverseBreadthFirst.getCall(0).args[0], "project.b", "ProjectGraph#traverseBreadthFirst called with correct project name for start"); @@ -617,21 +658,23 @@ test("_initTasks: Create dependencies reader for all dependencies", async (t) => }); await traversalCallback({ project: { - getName: () => "transitive.dep.a", - getReader: () => "transitive.dep.a reader", + getName: () => "dep.c", + getReader: () => "dep.c reader", } }); + // Now wait for the reader to be created + await readerPromise; t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once"); t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0], { - name: "Dependency reader collection of project project.b", + name: "Reduced dependency reader collection of project project.b", readers: [ - "dep.a reader", "dep.b reader", "transitive.dep.a reader" + "dep.a reader", "dep.b reader", "dep.c reader" ] }, "createReaderCollection got called with correct arguments"); }); test("Custom task is called correctly", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(false); const mockSpecVersion = { @@ -643,7 +686,8 @@ test("Custom task is called correctly", async (t) => { graph.getExtension.returns({ getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); t.context.taskUtil.getInterface.returns("taskUtil interface"); const project = getMockProject("module"); @@ -652,39 +696,39 @@ test("Custom task is called correctly", async (t) => { ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]), "Custom tasks requires all dependencies by default"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); - t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); + t.is(specVersionGteStub.getCall(2).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); t.is(specVersionGteStub.getCall(0).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on second call"); + t.is(specVersionGteStub.getCall(1).args[0], "5.0", + "SpecificationVersion#gte got called with correct arguments on second call (differential updates check)"); - t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], new Set(["dep.a", "dep.b"]), - "_createDependenciesReader got called with correct arguments"); + "getDependenciesReader got called with correct arguments"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - options: { - projectName: "project.b", - projectNamespace: "project/b", - configuration: "configuration", - }, - taskUtil: "taskUtil interface" - }, "Task got called with one argument"); + const taskArgs = taskStub.getCall(0).args[0]; + t.is(taskArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskArgs.taskUtil, "taskUtil interface", "taskUtil is correct"); + t.is(taskArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskArgs.options.configuration, "configuration", "configuration is correct"); t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, @@ -692,7 +736,7 @@ test("Custom task is called correctly", async (t) => { }); test("Custom task with legacy spec version", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(false); const mockSpecVersion = { @@ -703,7 +747,8 @@ test("Custom task with legacy spec version", async (t) => { graph.getExtension.returns({ getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); t.context.taskUtil.getInterface.returns(undefined); // simulating no taskUtil for old specVersion const project = getMockProject("module"); @@ -712,7 +757,7 @@ test("Custom task with legacy spec version", async (t) => { ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -720,31 +765,31 @@ test("Custom task with legacy spec version", async (t) => { t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]), "Custom tasks requires all dependencies by default"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); - t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); + t.is(specVersionGteStub.getCall(2).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); t.is(specVersionGteStub.getCall(0).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on second call"); + t.is(specVersionGteStub.getCall(1).args[0], "5.0", + "SpecificationVersion#gte got called with correct arguments on second call (differential updates check)"); - t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], new Set(["dep.a", "dep.b"]), - "_createDependenciesReader got called with correct arguments"); + "getDependenciesReader got called with correct arguments"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - options: { - projectName: "project.b", - projectNamespace: "project/b", - configuration: "configuration", - } - }, "Task got called with one argument"); + const taskArgs = taskStub.getCall(0).args[0]; + t.is(taskArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskArgs.options.configuration, "configuration", "configuration is correct"); t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, @@ -752,7 +797,7 @@ test("Custom task with legacy spec version", async (t) => { }); test("Custom task with legacy spec version and requiredDependenciesCallback", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(false); const mockSpecVersion = { @@ -764,7 +809,8 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as graph.getExtension.returns({ getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); t.context.taskUtil.getInterface.returns(undefined); // simulating no taskUtil for old specVersion const project = getMockProject("module"); @@ -773,7 +819,7 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -792,31 +838,31 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as } }, "requiredDependenciesCallback got called with expected arguments"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); - t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); + t.is(specVersionGteStub.getCall(2).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); t.is(specVersionGteStub.getCall(0).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on second call"); + t.is(specVersionGteStub.getCall(1).args[0], "5.0", + "SpecificationVersion#gte got called with correct arguments on second call (differential updates check)"); - t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], new Set(["dep.b"]), - "_createDependenciesReader got called with correct arguments"); + "getDependenciesReader got called with correct arguments"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - options: { - projectName: "project.b", - projectNamespace: "project/b", - configuration: "configuration", - } - }, "Task got called with one argument"); + const taskArgs = taskStub.getCall(0).args[0]; + t.is(taskArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskArgs.options.configuration, "configuration", "configuration is correct"); t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, @@ -824,7 +870,7 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as }); test("Custom task with specVersion 3.0", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(true); const mockSpecVersion = { @@ -839,7 +885,8 @@ test("Custom task with specVersion 3.0", async (t) => { graph.getExtension.returns({ getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); const project = getMockProject("module"); @@ -848,7 +895,7 @@ test("Custom task with specVersion 3.0", async (t) => { ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -884,14 +931,17 @@ test("Custom task with specVersion 3.0", async (t) => { t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]), "Custom tasks requires all dependencies by default"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); - t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); + t.is(specVersionGteStub.getCall(2).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); t.is(specVersionGteStub.getCall(0).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on second call"); + t.is(specVersionGteStub.getCall(1).args[0], "5.0", + "SpecificationVersion#gte got called with correct arguments on second call (differential updates check)"); t.is(taskUtil.getInterface.callCount, 2, "taskUtil#getInterface got called twice"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, @@ -899,29 +949,26 @@ test("Custom task with specVersion 3.0", async (t) => { t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersion, "taskUtil#getInterface got called with correct argument on second call"); - t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], new Set(["dep.b"]), - "_createDependenciesReader got called with correct arguments"); + "getDependenciesReader got called with correct arguments"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - log: "group logger", - taskUtil, - options: { - projectName: "project.b", - projectNamespace: "project/b", - taskName: "myTask", // specVersion 3.0 feature - configuration: "configuration", - }, - }, "Task got called with one argument"); + const taskArgs = taskStub.getCall(0).args[0]; + t.is(taskArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskArgs.log, "group logger", "log is correct"); + t.deepEqual(taskArgs.taskUtil, taskUtil, "taskUtil is correct"); + t.is(taskArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskArgs.options.taskName, "myTask", "taskName is correct"); + t.is(taskArgs.options.configuration, "configuration", "configuration is correct"); }); test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", async (t) => { - const {sinon, graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context; + const {sinon, graph, taskUtil, taskRepository, projectBuildLogger, buildCache, TaskRunner} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(true); const mockSpecVersion = { @@ -935,7 +982,8 @@ test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", asy getName: () => "custom task name", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); const project = getMockProject("module"); @@ -944,45 +992,45 @@ test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", asy ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(), "Custom tasks requires no dependencies by default"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); - t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); + t.is(specVersionGteStub.getCall(2).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); t.is(specVersionGteStub.getCall(0).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on second call"); + t.is(specVersionGteStub.getCall(1).args[0], "5.0", + "SpecificationVersion#gte got called with correct arguments on second call (differential updates check)"); t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, "taskUtil#getInterface got called with correct argument on first call"); - t.is(createDependencyReaderStub.callCount, 0, "_createDependenciesReader did not get called"); + t.is(createDependencyReaderStub.callCount, 0, "getDependenciesReader did not get called"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - log: "group logger", - taskUtil, - options: { - projectName: "project.b", - projectNamespace: "project/b", - taskName: "myTask", // specVersion 3.0 feature - configuration: "configuration", - }, - }, "Task got called with one argument"); + const taskArgs = taskStub.getCall(0).args[0]; + t.is(taskArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskArgs.log, "group logger", "log is correct"); + t.deepEqual(taskArgs.taskUtil, taskUtil, "taskUtil is correct"); + t.is(taskArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskArgs.options.taskName, "myTask", "taskName is correct"); + t.is(taskArgs.options.configuration, "configuration", "configuration is correct"); }); test("Multiple custom tasks with same name are called correctly", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStubA = sinon.stub(); const taskStubB = sinon.stub(); const taskStubC = sinon.stub(); @@ -1014,25 +1062,29 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { getName: () => "Task Name A", getTask: () => taskStubA, getSpecVersion: () => mockSpecVersionA, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); graph.getExtension.onSecondCall().returns({ getName: () => "Task Name B", getTask: () => taskStubB, getSpecVersion: () => mockSpecVersionB, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); graph.getExtension.onThirdCall().returns({ getName: () => "Task Name C", getTask: () => taskStubC, getSpecVersion: () => mockSpecVersionC, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); graph.getExtension.onCall(3).returns({ getName: () => "Task Name D", getTask: () => taskStubD, getSpecVersion: () => mockSpecVersionD, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); const project = getMockProject("module"); project.getCustomTasks = () => [ @@ -1042,7 +1094,7 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { {name: "myTask", afterTask: "myTask", configuration: "bird"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -1078,7 +1130,8 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { "myTask--2", ], "Correct order of custom tasks"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); await taskRunner.runTasks(); t.is(projectBuildLogger.setTasks.callCount, 1, "ProjectBuildLogger#setTask got called once"); @@ -1116,75 +1169,64 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { t.is(taskUtil.getInterface.getCall(4).args[0], mockSpecVersionB, "taskUtil#getInterface got called with correct argument on fifth call"); - t.is(createDependencyReaderStub.callCount, 3, "_createDependenciesReader got called three times"); + t.is(createDependencyReaderStub.callCount, 4, "getDependenciesReader got called four times"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], - new Set(["dep.b"]), - "_createDependenciesReader got called with correct arguments on first call"); + new Set(["dep.a", "dep.b"]), + "getDependenciesReader got called with correct arguments on first call (runTasks init)"); t.deepEqual(createDependencyReaderStub.getCall(1).args[0], - new Set(["dep.a"]), - "_createDependenciesReader got called with correct arguments on second call"); + new Set(["dep.b"]), + "getDependenciesReader got called with correct arguments on second call (Task A)"); t.deepEqual(createDependencyReaderStub.getCall(2).args[0], + new Set(["dep.a"]), + "getDependenciesReader got called with correct arguments on third call (Task D)"); + t.deepEqual(createDependencyReaderStub.getCall(3).args[0], new Set(["dep.a", "dep.b"]), - "_createDependenciesReader got called with correct arguments on third call"); + "getDependenciesReader got called with correct arguments on fourth call (Task B)"); t.is(taskStubA.callCount, 1, "Task A got called once"); t.is(taskStubA.getCall(0).args.length, 1, "Task A got called with one argument"); - t.deepEqual(taskStubA.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - taskUtil, - options: { - projectName: "project.b", - projectNamespace: "project/b", - configuration: "cat", - } - }, "Task A got called with one argument"); + const taskAArgs = taskStubA.getCall(0).args[0]; + t.is(taskAArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskAArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskAArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskAArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskAArgs.options.configuration, "cat", "configuration is correct"); t.is(taskStubB.callCount, 1, "Task B got called once"); t.is(taskStubB.getCall(0).args.length, 1, "Task B got called with one argument"); - t.deepEqual(taskStubB.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - taskUtil, - options: { - projectName: "project.b", - projectNamespace: "project/b", - configuration: "dog", - } - }, "Task B got called with one argument"); + const taskBArgs = taskStubB.getCall(0).args[0]; + t.is(taskBArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskBArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskBArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskBArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskBArgs.options.configuration, "dog", "configuration is correct"); t.is(taskStubC.callCount, 1, "Task C got called once"); t.is(taskStubC.getCall(0).args.length, 1, "Task C got called with one argument"); - t.deepEqual(taskStubC.getCall(0).args[0], { - workspace: "workspace", - log: "group logger", - taskUtil, - options: { - projectName: "project.b", - projectNamespace: "project/b", - taskName: "myTask--3", - configuration: "bird", - } - }, "Task C got called with one argument"); + const taskCArgs = taskStubC.getCall(0).args[0]; + t.is(taskCArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskCArgs.log, "group logger", "log is correct"); + t.deepEqual(taskCArgs.taskUtil, taskUtil, "taskUtil is correct"); + t.is(taskCArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskCArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskCArgs.options.taskName, "myTask--3", "taskName is correct"); + t.is(taskCArgs.options.configuration, "bird", "configuration is correct"); t.is(taskStubD.callCount, 1, "Task D got called once"); t.is(taskStubD.getCall(0).args.length, 1, "Task D got called with one argument"); - t.deepEqual(taskStubD.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - log: "group logger", - taskUtil, - options: { - projectName: "project.b", - projectNamespace: "project/b", - taskName: "myTask--4", - configuration: "bird", - } - }, "Task D got called with one argument"); + const taskDArgs = taskStubD.getCall(0).args[0]; + t.is(taskDArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskDArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskDArgs.log, "group logger", "log is correct"); + t.deepEqual(taskDArgs.taskUtil, taskUtil, "taskUtil is correct"); + t.is(taskDArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskDArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskDArgs.options.taskName, "myTask--4", "taskName is correct"); + t.is(taskDArgs.options.configuration, "bird", "configuration is correct"); }); test("Custom task: requiredDependenciesCallback returns unknown dependency", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(true); const mockSpecVersion = { @@ -1200,7 +1242,8 @@ test("Custom task: requiredDependenciesCallback returns unknown dependency", asy getName: () => "custom.task.a", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); const project = getMockProject("module"); @@ -1209,7 +1252,7 @@ test("Custom task: requiredDependenciesCallback returns unknown dependency", asy ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await t.throwsAsync(taskRunner._initTasks(), { message: @@ -1221,7 +1264,7 @@ test("Custom task: requiredDependenciesCallback returns unknown dependency", asy test("Custom task: requiredDependenciesCallback returns Array instead of Set", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(true); const mockSpecVersion = { @@ -1237,7 +1280,8 @@ test("Custom task: requiredDependenciesCallback returns Array instead of Set", a getName: () => "custom.task.a", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getSupportsDifferentialBuildsCallback: sinon.stub().returns(() => false) }); const project = getMockProject("module"); @@ -1246,7 +1290,7 @@ test("Custom task: requiredDependenciesCallback returns Array instead of Set", a ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await t.throwsAsync(taskRunner._initTasks(), { message: @@ -1256,7 +1300,7 @@ test("Custom task: requiredDependenciesCallback returns Array instead of Set", a }); test("Custom task attached to a disabled task", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, sinon, customTask} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache, sinon, customTask} = t.context; const project = getMockProject("application"); const customTaskFnStub = sinon.stub(); @@ -1269,7 +1313,7 @@ test("Custom task attached to a disabled task", async (t) => { customTask.getTask = () => customTaskFnStub; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner.runTasks(); @@ -1296,7 +1340,7 @@ test("Custom task attached to a disabled task", async (t) => { }); test.serial("_addTask", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); taskRepository.getTask.withArgs("standardTask").resolves({ @@ -1305,7 +1349,7 @@ test.serial("_addTask", async (t) => { const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -1326,24 +1370,20 @@ test.serial("_addTask", async (t) => { t.is(taskRepository.getTask.getCall(0).args[0], "standardTask", "taskRepository#getTask got called with correct argument"); t.is(taskStub.callCount, 1, "Task got called once"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - // No dependencies - options: { - projectName: "project.b", - projectNamespace: "project/b" - }, - taskUtil - }, "Task got called with correct arguments"); + const taskCallArgs = taskStub.getCall(0).args[0]; + t.is(taskCallArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskCallArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskCallArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskCallArgs.taskUtil, taskUtil, "taskUtil is correct"); }); test.serial("_addTask with options", async (t) => { - const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const taskStub = sinon.stub(); const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -1361,33 +1401,31 @@ test.serial("_addTask with options", async (t) => { t.truthy(taskRunner._tasks["standardTask"].task, "Task function got set correctly"); t.deepEqual(taskRunner._taskExecutionOrder, ["standardTask"], "Task got added to execution order"); - const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); - await taskRunner._tasks["standardTask"].task({ - workspace: "workspace", - dependencies: "dependencies", - }); + // Warm the cache (normally done by runTasks) + await taskRunner.getDependenciesReader(new Set(["dep.a", "dep.b"]), true); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") + .resolves({getName: () => "dependencies"}); + // Call the task wrapper without parameters (it creates workspace/dependencies internally) + await taskRunner._tasks["standardTask"].task(); t.is(taskRepository.getTask.callCount, 0, "taskRepository#getTask did not get called"); - t.is(createDependencyReaderStub.callCount, 0, "_createDependenciesReader did not get called"); + t.is(createDependencyReaderStub.callCount, 0, "getDependenciesReader did not get called (using cached reader)"); t.is(taskStub.callCount, 1, "Task got called once"); - t.deepEqual(taskStub.getCall(0).args[0], { - workspace: "workspace", - dependencies: taskRunner._allDependenciesReader, - options: { - projectName: "project.b", - projectNamespace: "project/b", - myTaskOption: "cat" - }, - taskUtil - }, "Task got called with correct arguments"); + const taskCallArgs = taskStub.getCall(0).args[0]; + t.is(taskCallArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); + t.is(taskCallArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); + t.is(taskCallArgs.options.projectName, "project.b", "projectName is correct"); + t.is(taskCallArgs.options.projectNamespace, "project/b", "projectNamespace is correct"); + t.is(taskCallArgs.options.myTaskOption, "cat", "myTaskOption is correct"); + t.is(taskCallArgs.taskUtil, taskUtil, "taskUtil is correct"); }); test("_addTask: Duplicate task", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -1405,10 +1443,10 @@ test("_addTask: Duplicate task", async (t) => { }); test("_addTask: Task already added to execution order", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); @@ -1424,13 +1462,13 @@ test("_addTask: Task already added to execution order", async (t) => { }); test("getRequiredDependencies: Custom Task", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); project.getCustomTasks = () => [ {name: "myTask"} ]; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), "Project with custom task >= specVersion 3.0 and no requiredDependenciesCallback " + @@ -1438,72 +1476,72 @@ test("getRequiredDependencies: Custom Task", async (t) => { }); test("getRequiredDependencies: Default application", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("application"); project.getBundles = () => []; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), "Default application project does not require dependencies"); }); test("getRequiredDependencies: Default component", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("component"); project.getBundles = () => []; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), "Default component project does not require dependencies"); }); test("getRequiredDependencies: Default library", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("library"); project.getBundles = () => []; const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); t.deepEqual(await taskRunner.getRequiredDependencies(), new Set(["dep.a", "dep.b"]), "Default library project requires dependencies"); }); test("getRequiredDependencies: Default theme-library", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("theme-library"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); t.deepEqual(await taskRunner.getRequiredDependencies(), new Set(["dep.a", "dep.b"]), "Default theme-library project requires dependencies"); }); test("getRequiredDependencies: Default module", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), "Default module project does not require dependencies"); }); -test("_createDependenciesReader", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; +test("getDependenciesReader", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); graph.traverseBreadthFirst.reset(); // Ignore the call in initTask resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask - resourceFactory.createReaderCollection.returns("custom reader collection"); - const res = await taskRunner._createDependenciesReader(new Set(["dep.a"])); + resourceFactory.createReaderCollection.returns({getName: () => "custom reader collection"}); + const res = await taskRunner.getDependenciesReader(new Set(["dep.a"])); t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst got called once"); t.is(graph.traverseBreadthFirst.getCall(0).args[0], "project.b", @@ -1550,42 +1588,45 @@ test("_createDependenciesReader", async (t) => { "dep.a reader", "dep.b reader", "dep.c reader" ] }, "createReaderCollection got called with correct arguments"); - t.is(res, "custom reader collection", "Returned expected value"); + t.is(res.getName(), "custom reader collection", "Returned expected value"); }); -test("_createDependenciesReader: All dependencies required", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; +test("getDependenciesReader: All dependencies required", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); - graph.traverseBreadthFirst.reset(); // Ignore the call in initTask - resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask - resourceFactory.createReaderCollection.returns("custom reader collection"); - const res = await taskRunner._createDependenciesReader(new Set(["dep.a", "dep.b"])); + // Initialize the cache by calling getDependenciesReader with a subset first to avoid the shortcut + // Then call with forceUpdate to populate the cache + const cachedReader = await taskRunner.getDependenciesReader(new Set(["dep.a", "dep.b"]), true); + graph.traverseBreadthFirst.reset(); // Ignore the call in init + resourceFactory.createReaderCollection.reset(); // Ignore the call in init + resourceFactory.createReaderCollection.returns({getName: () => "custom reader collection"}); + const res = await taskRunner.getDependenciesReader(new Set(["dep.a", "dep.b"])); t.is(graph.traverseBreadthFirst.callCount, 0, "ProjectGraph#traverseBreadthFirst did not get called again"); t.is(resourceFactory.createReaderCollection.callCount, 0, "createReaderCollection did not get called again"); - t.is(res, "reader collection", "Shared (all-)dependency reader returned"); + t.is(res, cachedReader, "Shared (all-)dependency reader returned"); }); -test("_createDependenciesReader: No dependencies required", async (t) => { - const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; +test("getDependenciesReader: No dependencies required", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger, buildCache} = t.context; const project = getMockProject("module"); const taskRunner = new TaskRunner({ - project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildCache, buildConfig }); await taskRunner._initTasks(); graph.traverseBreadthFirst.reset(); // Ignore the call in initTask resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask - resourceFactory.createReaderCollection.returns("custom reader collection"); - const res = await taskRunner._createDependenciesReader(new Set()); + resourceFactory.createReaderCollection.returns({getName: () => "custom reader collection"}); + const res = await taskRunner.getDependenciesReader(new Set()); t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst got called once"); t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once"); t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0].readers, [], "createReaderCollection got called with no readers"); - t.is(res, "custom reader collection", "Shared (all-)dependency reader returned"); + t.is(res.getName(), "custom reader collection", "Shared (all-)dependency reader returned"); }); diff --git a/packages/project/test/lib/build/cache/BuildCacheStorage.js b/packages/project/test/lib/build/cache/BuildCacheStorage.js new file mode 100644 index 00000000000..adfd2cdc8b0 --- /dev/null +++ b/packages/project/test/lib/build/cache/BuildCacheStorage.js @@ -0,0 +1,375 @@ +import test from "ava"; +import path from "node:path"; +import fs from "node:fs"; +import {rimraf} from "rimraf"; +import {gunzipSync} from "node:zlib"; +import BuildCacheStorage from "../../../../lib/build/cache/BuildCacheStorage.js"; + +const TEST_DIR = path.join(import.meta.dirname, "..", "..", "..", "tmp", "BuildCacheStorage"); + +test.after.always(async () => { + await rimraf(TEST_DIR); +}); + +test.beforeEach((t) => { + t.context.dbDir = path.join(TEST_DIR, `db-${Date.now()}-${Math.random().toString(36).slice(2)}`); + t.context.storage = new BuildCacheStorage(t.context.dbDir); +}); + +test.afterEach.always((t) => { + try { + t.context.storage.close(); + } catch { + // Already closed + } +}); + +// Database file creation + +test("Creates cache.db in the specified directory", (t) => { + const dbPath = path.join(t.context.dbDir, "cache.db"); + t.true(fs.existsSync(dbPath)); +}); + +// ===== Content (CAS) operations ===== + +test("hasContent: Returns false for missing content", (t) => { + t.false(t.context.storage.hasContent("sha256-missing")); +}); + +test("hasContent: Returns true after content is stored", (t) => { + const content = Buffer.from("test content"); + t.context.storage.putContent("sha256-test", content); + t.true(t.context.storage.hasContent("sha256-test")); +}); + +test("putContent + readContent: Round-trip", (t) => { + const content = Buffer.from("hello world"); + t.context.storage.putContent("sha256-hello", content); + const result = t.context.storage.readContent("sha256-hello"); + t.deepEqual(result, content); +}); + +test("putContent + readContentRaw: Returns gzip-compressed data", (t) => { + const content = Buffer.from("compressed test"); + t.context.storage.putContent("sha256-compressed", content); + const raw = t.context.storage.readContentRaw("sha256-compressed"); + t.notDeepEqual(raw, content); + t.deepEqual(gunzipSync(raw), content); +}); + +test("putContent: Deduplicates via INSERT OR IGNORE", (t) => { + const content1 = Buffer.from("original"); + t.context.storage.putContent("sha256-dedup", content1); + // Second put with same integrity but different buffer is ignored + const content2 = Buffer.from("different"); + t.context.storage.putContent("sha256-dedup", content2); + const result = t.context.storage.readContent("sha256-dedup"); + t.deepEqual(result, content1); +}); + +test("readContent: Throws for missing integrity", (t) => { + t.throws(() => t.context.storage.readContent("sha256-nonexistent"), { + message: /Content not found in CAS for integrity/ + }); +}); + +test("readContentRaw: Throws for missing integrity", (t) => { + t.throws(() => t.context.storage.readContentRaw("sha256-nonexistent"), { + message: /Content not found in CAS for integrity/ + }); +}); + +test("putContent + readContent: Large content round-trip", (t) => { + const content = Buffer.alloc(1024 * 1024); + for (let i = 0; i < content.length; i++) { + content[i] = i % 256; + } + t.context.storage.putContent("sha256-large", content); + const result = t.context.storage.readContent("sha256-large"); + t.deepEqual(result, content); +}); + +// ===== Index cache ===== + +test("readIndexCache: Returns null on cache miss", (t) => { + const result = t.context.storage.readIndexCache("project-a", "sig-1", "source"); + t.is(result, null); +}); + +test("Index cache: Round-trip write and read", (t) => { + const data = {indexTimestamp: 1000, root: {name: "", type: "directory", hash: "abc"}}; + t.context.storage.writeIndexCache("project-a", "sig-1", "source", data); + const result = t.context.storage.readIndexCache("project-a", "sig-1", "source"); + t.deepEqual(result, data); +}); + +test("Index cache: Different kind values are independent", (t) => { + const sourceData = {kind: "source", value: 1}; + const resultData = {kind: "result", value: 2}; + t.context.storage.writeIndexCache("project-a", "sig-1", "source", sourceData); + t.context.storage.writeIndexCache("project-a", "sig-1", "result", resultData); + + t.deepEqual(t.context.storage.readIndexCache("project-a", "sig-1", "source"), sourceData); + t.deepEqual(t.context.storage.readIndexCache("project-a", "sig-1", "result"), resultData); +}); + +test("Index cache: Overwrite replaces data", (t) => { + const original = {version: 1}; + const updated = {version: 2}; + t.context.storage.writeIndexCache("project-a", "sig-1", "source", original); + t.context.storage.writeIndexCache("project-a", "sig-1", "source", updated); + t.deepEqual(t.context.storage.readIndexCache("project-a", "sig-1", "source"), updated); +}); + +// ===== Stage metadata ===== + +test("readStageCache: Returns null on cache miss", (t) => { + const result = t.context.storage.readStageCache("project-a", "sig-1", "task/minify", "stage-sig-1"); + t.is(result, null); +}); + +test("Stage metadata: Round-trip write and read", (t) => { + const data = {resourceMetadata: {"/a.js": {integrity: "hash-a"}}}; + t.context.storage.writeStageCache("project-a", "sig-1", "task/minify", "stage-sig-1", data); + const result = t.context.storage.readStageCache("project-a", "sig-1", "task/minify", "stage-sig-1"); + t.deepEqual(result, data); +}); + +test("Stage metadata: Different stage signatures are independent", (t) => { + const data1 = {value: "first"}; + const data2 = {value: "second"}; + t.context.storage.writeStageCache("project-a", "sig-1", "task/minify", "stage-sig-1", data1); + t.context.storage.writeStageCache("project-a", "sig-1", "task/minify", "stage-sig-2", data2); + + t.deepEqual( + t.context.storage.readStageCache("project-a", "sig-1", "task/minify", "stage-sig-1"), data1 + ); + t.deepEqual( + t.context.storage.readStageCache("project-a", "sig-1", "task/minify", "stage-sig-2"), data2 + ); +}); + +test("Stage metadata: Stage IDs with slashes are stored correctly", (t) => { + const data = {value: "slash-test"}; + t.context.storage.writeStageCache("project-a", "sig-1", "task/myTask", "stage-sig-1", data); + t.deepEqual( + t.context.storage.readStageCache("project-a", "sig-1", "task/myTask", "stage-sig-1"), data + ); +}); + +// ===== Task metadata ===== + +test("readTaskMetadata: Returns null on cache miss", (t) => { + const result = t.context.storage.readTaskMetadata("project-a", "sig-1", "minify", "project"); + t.is(result, null); +}); + +test("Task metadata: Round-trip write and read", (t) => { + const data = {requestSetGraph: {nodes: [], nextId: 1}}; + t.context.storage.writeTaskMetadata("project-a", "sig-1", "minify", "project", data); + const result = t.context.storage.readTaskMetadata("project-a", "sig-1", "minify", "project"); + t.deepEqual(result, data); +}); + +test("Task metadata: Different types are independent", (t) => { + const projectData = {scope: "project"}; + const depData = {scope: "dependency"}; + t.context.storage.writeTaskMetadata("project-a", "sig-1", "minify", "project", projectData); + t.context.storage.writeTaskMetadata("project-a", "sig-1", "minify", "dependencies", depData); + + t.deepEqual( + t.context.storage.readTaskMetadata("project-a", "sig-1", "minify", "project"), projectData + ); + t.deepEqual( + t.context.storage.readTaskMetadata("project-a", "sig-1", "minify", "dependencies"), depData + ); +}); + +// ===== Result metadata ===== + +test("readResultMetadata: Returns null on cache miss", (t) => { + const result = t.context.storage.readResultMetadata("project-a", "sig-1", "result-sig-1"); + t.is(result, null); +}); + +test("Result metadata: Round-trip write and read", (t) => { + const data = {stageSignatures: {"task/minify": "sig-abc"}}; + t.context.storage.writeResultMetadata("project-a", "sig-1", "result-sig-1", data); + const result = t.context.storage.readResultMetadata("project-a", "sig-1", "result-sig-1"); + t.deepEqual(result, data); +}); + +test("Result metadata: Overwrite replaces data", (t) => { + const original = {version: 1}; + const updated = {version: 2}; + t.context.storage.writeResultMetadata("project-a", "sig-1", "result-sig-1", original); + t.context.storage.writeResultMetadata("project-a", "sig-1", "result-sig-1", updated); + t.deepEqual(t.context.storage.readResultMetadata("project-a", "sig-1", "result-sig-1"), updated); +}); + +// ===== Cross-project isolation ===== + +test("Different projects are fully isolated", (t) => { + const dataA = {project: "a"}; + const dataB = {project: "b"}; + t.context.storage.writeIndexCache("project-a", "sig-1", "source", dataA); + t.context.storage.writeIndexCache("project-b", "sig-1", "source", dataB); + + t.deepEqual(t.context.storage.readIndexCache("project-a", "sig-1", "source"), dataA); + t.deepEqual(t.context.storage.readIndexCache("project-b", "sig-1", "source"), dataB); +}); + +// ===== Error handling ===== + +test("Read throws wrapped error after close", (t) => { + t.context.storage.close(); + const err = t.throws(() => { + t.context.storage.readIndexCache("project-a", "sig-1", "source"); + }); + t.true(err.message.includes("Failed to read resource index cache")); + t.truthy(err.cause); +}); + +// ===== Metadata batch transactions ===== + +test("beginMetadataBatch/endMetadataBatch: Multiple writes commit atomically", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + storage.writeTaskMetadata("project-a", "sig-1", "minify", "project", {v: 2}); + storage.writeResultMetadata("project-a", "sig-1", "result-sig-1", {v: 3}); + storage.endMetadataBatch(); + + t.deepEqual(storage.readIndexCache("project-a", "sig-1", "source"), {v: 1}); + t.deepEqual(storage.readTaskMetadata("project-a", "sig-1", "minify", "project"), {v: 2}); + t.deepEqual(storage.readResultMetadata("project-a", "sig-1", "result-sig-1"), {v: 3}); +}); + +test("rollbackMetadataBatch: Discards uncommitted writes", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + storage.writeTaskMetadata("project-a", "sig-1", "minify", "project", {v: 2}); + storage.rollbackMetadataBatch(); + + t.is(storage.readIndexCache("project-a", "sig-1", "source"), null); + t.is(storage.readTaskMetadata("project-a", "sig-1", "minify", "project"), null); +}); + +test("close: Rolls back uncommitted metadata batch", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + storage.close(); + + const fresh = new BuildCacheStorage(t.context.dbDir); + t.is(fresh.readIndexCache("project-a", "sig-1", "source"), null); + fresh.close(); +}); + +test("beginMetadataBatch: Nested calls are idempotent", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + storage.endMetadataBatch(); + + t.deepEqual(storage.readIndexCache("project-a", "sig-1", "source"), {v: 1}); +}); + +// ===== Standalone content batch transactions ===== + +test("Standalone content batch: Commit persists content", (t) => { + const {storage} = t.context; + storage.beginContentBatch(); + storage.putContent("sha256-batch1", Buffer.from("batch item 1")); + storage.putContent("sha256-batch2", Buffer.from("batch item 2")); + storage.endContentBatch(); + + t.deepEqual(storage.readContent("sha256-batch1"), Buffer.from("batch item 1")); + t.deepEqual(storage.readContent("sha256-batch2"), Buffer.from("batch item 2")); +}); + +test("Standalone content batch: Rollback discards content", (t) => { + const {storage} = t.context; + storage.beginContentBatch(); + storage.putContent("sha256-rollback", Buffer.from("should be discarded")); + storage.rollbackContentBatch(); + + t.false(storage.hasContent("sha256-rollback")); +}); + +test("beginContentBatch: Nested calls are idempotent", (t) => { + const {storage} = t.context; + storage.beginContentBatch(); + storage.beginContentBatch(); + storage.putContent("sha256-idempotent", Buffer.from("idempotent")); + storage.endContentBatch(); + + t.deepEqual(storage.readContent("sha256-idempotent"), Buffer.from("idempotent")); +}); + +// ===== Nested content batch inside metadata batch (SAVEPOINT) ===== + +test("Nested content batch: Content persists when both batches commit", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + + storage.beginContentBatch(); + storage.putContent("sha256-nested", Buffer.from("nested content")); + storage.endContentBatch(); + + storage.endMetadataBatch(); + + t.deepEqual(storage.readIndexCache("project-a", "sig-1", "source"), {v: 1}); + t.deepEqual(storage.readContent("sha256-nested"), Buffer.from("nested content")); +}); + +test("Nested content batch rollback: Metadata survives, content is discarded", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + + storage.beginContentBatch(); + storage.putContent("sha256-rolled-back", Buffer.from("will be rolled back")); + storage.rollbackContentBatch(); + + storage.endMetadataBatch(); + + t.deepEqual(storage.readIndexCache("project-a", "sig-1", "source"), {v: 1}); + t.false(storage.hasContent("sha256-rolled-back")); +}); + +test("Metadata rollback: Both metadata and nested content are discarded", (t) => { + const {storage} = t.context; + storage.beginMetadataBatch(); + storage.writeIndexCache("project-a", "sig-1", "source", {v: 1}); + + storage.beginContentBatch(); + storage.putContent("sha256-outer-rb", Buffer.from("will be discarded")); + storage.endContentBatch(); + + storage.rollbackMetadataBatch(); + + t.is(storage.readIndexCache("project-a", "sig-1", "source"), null); + t.false(storage.hasContent("sha256-outer-rb")); +}); + +// ===== Validity ===== + +test("isValid: Returns true for open database", (t) => { + t.true(t.context.storage.isValid); +}); + +test("isValid: Returns false after close", (t) => { + t.context.storage.close(); + t.false(t.context.storage.isValid); +}); + +test("isValid: Returns false after database file is deleted", (t) => { + const dbPath = path.join(t.context.dbDir, "cache.db"); + fs.unlinkSync(dbPath); + t.false(t.context.storage.isValid); +}); diff --git a/packages/project/test/lib/build/cache/BuildTaskCache.js b/packages/project/test/lib/build/cache/BuildTaskCache.js new file mode 100644 index 00000000000..85f4052e3cf --- /dev/null +++ b/packages/project/test/lib/build/cache/BuildTaskCache.js @@ -0,0 +1,495 @@ +import test from "ava"; +import sinon from "sinon"; +import BuildTaskCache from "../../../../lib/build/cache/BuildTaskCache.js"; + +// Helper to create mock readers +function createMockReader(resources = []) { + const resourceMap = new Map(resources.map((r) => [r.getPath(), r])); + return { + byGlob: sinon.stub().callsFake(async (pattern) => { + // Simple pattern matching for tests + if (pattern === "/**/*") { + return Array.from(resourceMap.values()); + } + return resources.filter((r) => r.getPath().includes(pattern.replace(/[*]/g, ""))); + }), + byPath: sinon.stub().callsFake(async (path) => { + return resourceMap.get(path) || null; + }) + }; +} + +// Helper to create mock resources +function createMockResource(path, content = "test content", hash = null) { + const actualHash = hash || `hash-${path}`; + return { + getPath: () => path, + getOriginalPath: () => path, + getBuffer: async () => Buffer.from(content), + getIntegrity: async () => actualHash, + getLastModified: () => 1000, + getSize: async () => content.length, + getInode: () => 1, + getTags: () => null + }; +} + +test.afterEach.always(() => { + sinon.restore(); +}); + +// ===== CREATION AND INITIALIZATION TESTS ===== + +test("Create BuildTaskCache instance", (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + t.truthy(cache, "BuildTaskCache instance created"); + t.is(cache.getTaskName(), "testTask", "Task name matches"); + t.is(cache.getSupportsDifferentialBuilds(), false, "Differential updates disabled"); +}); + +test("Create with differential updates enabled", (t) => { + const cache = new BuildTaskCache("test.project", "testTask", true); + + t.is(cache.getSupportsDifferentialBuilds(), true, "Differential updates enabled"); +}); + +test("fromCache: restore BuildTaskCache from cached data", (t) => { + const projectRequests = { + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }; + + const dependencyRequests = { + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }; + + const cache = BuildTaskCache.fromCache("test.project", "testTask", false, + projectRequests, dependencyRequests); + + t.truthy(cache, "Cache restored from cached data"); + t.is(cache.getTaskName(), "testTask", "Task name preserved"); + t.is(cache.getSupportsDifferentialBuilds(), false, "Differential updates setting preserved"); +}); + +// ===== METADATA ACCESS TESTS ===== + +test("getTaskName: returns task name", (t) => { + const cache = new BuildTaskCache("test.project", "myTask", false); + + t.is(cache.getTaskName(), "myTask", "Task name returned"); +}); + +test("getSupportsDifferentialBuilds: returns correct value", (t) => { + const cache1 = new BuildTaskCache("test.project", "task1", false); + const cache2 = new BuildTaskCache("test.project", "task2", true); + + t.false(cache1.getSupportsDifferentialBuilds(), "Returns false when disabled"); + t.true(cache2.getSupportsDifferentialBuilds(), "Returns true when enabled"); +}); + +test("hasNewOrModifiedCacheEntries: initially true for new instance", (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + // A new instance has new entries that need to be written + t.true(cache.hasNewOrModifiedCacheEntries(), "New instance has entries to write"); +}); + +test("hasNewOrModifiedCacheEntries: true after recording requests", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const resource = createMockResource("/test.js"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, undefined, projectReader, dependencyReader); + + t.true(cache.hasNewOrModifiedCacheEntries(), "Has new entries after recording"); +}); + +// ===== SIGNATURE TESTS ===== + +test("getProjectIndexSignatures: returns signatures after recording", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const resource = createMockResource("/test.js"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, undefined, projectReader, dependencyReader); + + const signatures = cache.getProjectIndexSignatures(); + + t.true(Array.isArray(signatures), "Returns array"); + t.true(signatures.length > 0, "Has at least one signature"); + t.is(typeof signatures[0], "string", "Signature is a string"); +}); + +test("getDependencyIndexSignatures: returns signatures after recording", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const projectResource = createMockResource("/test.js"); + const depResource = createMockResource("/dep.js"); + const projectReader = createMockReader([projectResource]); + const dependencyReader = createMockReader([depResource]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + const dependencyRequests = { + paths: new Set(["/dep.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, dependencyRequests, projectReader, dependencyReader); + + const signatures = cache.getDependencyIndexSignatures(); + + t.true(Array.isArray(signatures), "Returns array"); + t.true(signatures.length > 0, "Has at least one signature"); + t.is(typeof signatures[0], "string", "Signature is a string"); +}); + +// ===== REQUEST RECORDING TESTS ===== + +test("recordRequests: handles project requests only", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const resource = createMockResource("/test.js"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + const [projectSig, depSig] = await cache.recordRequests( + projectRequests, undefined, projectReader, dependencyReader); + + t.is(typeof projectSig, "string", "Project signature returned"); + t.is(typeof depSig, "string", "Dependency signature returned"); + t.true(projectSig.length > 0, "Project signature not empty"); + t.true(depSig.length > 0, "Dependency signature not empty"); +}); + +test("recordRequests: handles both project and dependency requests", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const projectResource = createMockResource("/test.js"); + const depResource = createMockResource("/dep.js"); + const projectReader = createMockReader([projectResource]); + const dependencyReader = createMockReader([depResource]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + const dependencyRequests = { + paths: new Set(["/dep.js"]), + patterns: new Set() + }; + + const [projectSig, depSig] = await cache.recordRequests( + projectRequests, dependencyRequests, projectReader, dependencyReader); + + t.is(typeof projectSig, "string", "Project signature returned"); + t.is(typeof depSig, "string", "Dependency signature returned"); +}); + +test("recordRequests: handles glob patterns", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const resource1 = createMockResource("/src/test1.js"); + const resource2 = createMockResource("/src/test2.js"); + const projectReader = createMockReader([resource1, resource2]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(), + patterns: new Set(["/src/**/*.js"]) + }; + + const [projectSig, depSig] = await cache.recordRequests( + projectRequests, undefined, projectReader, dependencyReader); + + t.is(typeof projectSig, "string", "Project signature returned"); + t.is(typeof depSig, "string", "Dependency signature returned"); +}); + +test("recordRequests: handles empty requests", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const projectReader = createMockReader([]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(), + patterns: new Set() + }; + + const [projectSig, depSig] = await cache.recordRequests( + projectRequests, undefined, projectReader, dependencyReader); + + t.is(typeof projectSig, "string", "Project signature returned"); + t.is(typeof depSig, "string", "Dependency signature returned"); +}); + +// ===== INDEX UPDATE TESTS ===== + +test("updateProjectIndices: processes changed resources", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + // First, record some requests + const resource = createMockResource("/test.js", "initial content"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, undefined, projectReader, dependencyReader); + + // Now update with changed resource + const updatedResource = createMockResource("/test.js", "updated content", "new-hash"); + const updatedReader = createMockReader([updatedResource]); + + const changed = await cache.updateProjectIndices(updatedReader, ["/test.js"]); + + t.is(typeof changed, "boolean", "Returns boolean"); +}); + +test("updateDependencyIndices: processes changed dependencies", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + // First, record some requests + const projectResource = createMockResource("/test.js"); + const depResource = createMockResource("/dep.js", "initial"); + const projectReader = createMockReader([projectResource]); + const dependencyReader = createMockReader([depResource]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + const dependencyRequests = { + paths: new Set(["/dep.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, dependencyRequests, projectReader, dependencyReader); + + // Now update with changed dependency + const updatedDepResource = createMockResource("/dep.js", "updated", "new-dep-hash"); + const updatedDepReader = createMockReader([updatedDepResource]); + + const changed = await cache.updateDependencyIndices(updatedDepReader, ["/dep.js"]); + + t.is(typeof changed, "boolean", "Returns boolean"); +}); + +test("refreshDependencyIndices: refreshes all dependency indices", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + // First, record some requests + const projectResource = createMockResource("/test.js"); + const depResource = createMockResource("/dep.js"); + const projectReader = createMockReader([projectResource]); + const dependencyReader = createMockReader([depResource]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + const dependencyRequests = { + paths: new Set(["/dep.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, dependencyRequests, projectReader, dependencyReader); + + // Refresh all indices - returns undefined when processing changes, or false if no requests + const result = await cache.refreshDependencyIndices(dependencyReader); + + t.true(result === undefined || result === false, "Returns undefined or false"); +}); + +// ===== DELTA TESTS (for differential updates) ===== + +test("getProjectIndexDeltas: returns deltas when enabled", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", true); + + const resource = createMockResource("/test.js"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, undefined, projectReader, dependencyReader); + + const deltas = cache.getProjectIndexDeltas(); + + t.true(deltas instanceof Map, "Returns Map"); +}); + +test("getDependencyIndexDeltas: returns deltas when enabled", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", true); + + const projectResource = createMockResource("/test.js"); + const depResource = createMockResource("/dep.js"); + const projectReader = createMockReader([projectResource]); + const dependencyReader = createMockReader([depResource]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + const dependencyRequests = { + paths: new Set(["/dep.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, dependencyRequests, projectReader, dependencyReader); + + const deltas = cache.getDependencyIndexDeltas(); + + t.true(deltas instanceof Map, "Returns Map"); +}); + +// ===== SERIALIZATION TESTS ===== + +test("toCacheObjects: returns cache objects", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const resource = createMockResource("/test.js"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests, undefined, projectReader, dependencyReader); + + const [projectCache, dependencyCache] = cache.toCacheObjects(); + + t.truthy(projectCache, "Project cache object exists"); + t.truthy(dependencyCache, "Dependency cache object exists"); + t.truthy(projectCache.requestSetGraph, "Has request set graph"); + t.true(Array.isArray(projectCache.rootIndices), "Has root indices array"); +}); + +test("toCacheObjects: can restore from serialized data", async (t) => { + const cache1 = new BuildTaskCache("test.project", "testTask", false); + + const resource = createMockResource("/test.js"); + const projectReader = createMockReader([resource]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/test.js"]), + patterns: new Set() + }; + + await cache1.recordRequests(projectRequests, undefined, projectReader, dependencyReader); + + const [projectCache, dependencyCache] = cache1.toCacheObjects(); + + // Restore from cache + const cache2 = BuildTaskCache.fromCache("test.project", "testTask", false, + projectCache, dependencyCache); + + t.truthy(cache2, "Cache restored"); + t.is(cache2.getTaskName(), "testTask", "Task name preserved"); +}); + +// ===== EDGE CASES ===== + +test("Create with empty project name", (t) => { + const cache = new BuildTaskCache("", "testTask", false); + + t.truthy(cache, "Cache created with empty project name"); + t.is(cache.getTaskName(), "testTask", "Task name still accessible"); +}); + +test("Multiple recordRequests calls accumulate", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const resource1 = createMockResource("/test1.js"); + const resource2 = createMockResource("/test2.js"); + const projectReader = createMockReader([resource1, resource2]); + const dependencyReader = createMockReader([]); + + // First request + const projectRequests1 = { + paths: new Set(["/test1.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests1, undefined, projectReader, dependencyReader); + + const sigsBefore = cache.getProjectIndexSignatures(); + + // Second request with different resources + const projectRequests2 = { + paths: new Set(["/test2.js"]), + patterns: new Set() + }; + + await cache.recordRequests(projectRequests2, undefined, projectReader, dependencyReader); + + const sigsAfter = cache.getProjectIndexSignatures(); + + t.true(sigsAfter.length >= sigsBefore.length, "Signatures accumulated"); +}); + +test("Handles non-existent resource paths", async (t) => { + const cache = new BuildTaskCache("test.project", "testTask", false); + + const projectReader = createMockReader([]); + const dependencyReader = createMockReader([]); + + const projectRequests = { + paths: new Set(["/nonexistent.js"]), + patterns: new Set() + }; + + const [projectSig, depSig] = await cache.recordRequests( + projectRequests, undefined, projectReader, dependencyReader); + + t.is(typeof projectSig, "string", "Still returns signature"); + t.is(typeof depSig, "string", "Still returns dependency signature"); +}); diff --git a/packages/project/test/lib/build/cache/CacheManager.js b/packages/project/test/lib/build/cache/CacheManager.js new file mode 100644 index 00000000000..dfc3ffa1f6f --- /dev/null +++ b/packages/project/test/lib/build/cache/CacheManager.js @@ -0,0 +1,138 @@ +import test from "ava"; +import path from "node:path"; +import sinon from "sinon"; +import esmock from "esmock"; +import {rimraf} from "rimraf"; + +const TEST_DIR = path.join(import.meta.dirname, "..", "..", "..", "tmp", "CacheManager"); + +test.after.always(async () => { + await rimraf(TEST_DIR); +}); + +test.afterEach.always(() => { + sinon.restore(); + delete process.env.UI5_DATA_DIR; +}); + +function getUniqueTestDir() { + return path.join(TEST_DIR, `cm-${Date.now()}-${Math.random().toString(36).slice(2)}`); +} + +// Metadata delegation (round-trip through CacheManager) + +test.serial("Index cache: round-trip via CacheManager", async (t) => { + const testDir = getUniqueTestDir(); + process.env.UI5_DATA_DIR = testDir; + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + const data = {indexTimestamp: 1234, root: {name: "", hash: "abc"}}; + await cm.writeIndexCache("project-x", "build-sig", "source", data); + const result = await cm.readIndexCache("project-x", "build-sig", "source"); + t.deepEqual(result, data); + cm.close(); +}); + +test.serial("Stage cache: round-trip via CacheManager", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + const data = {resourceMetadata: {"/a.js": {integrity: "hash-a"}}}; + await cm.writeStageCache("project-x", "build-sig", "task/minify", "stage-sig", data); + const result = await cm.readStageCache("project-x", "build-sig", "task/minify", "stage-sig"); + t.deepEqual(result, data); + cm.close(); +}); + +test.serial("Task metadata: round-trip via CacheManager", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + const data = {requestSetGraph: {nodes: []}}; + await cm.writeTaskMetadata("project-x", "build-sig", "minify", "project", data); + const result = await cm.readTaskMetadata("project-x", "build-sig", "minify", "project"); + t.deepEqual(result, data); + cm.close(); +}); + +test.serial("Result metadata: round-trip via CacheManager", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + const data = {stageSignatures: {"task/minify": "sig-abc"}}; + await cm.writeResultMetadata("project-x", "build-sig", "result-sig", data); + const result = await cm.readResultMetadata("project-x", "build-sig", "result-sig"); + t.deepEqual(result, data); + cm.close(); +}); + +test.serial("Cache miss returns null for all metadata types", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + t.is(await cm.readIndexCache("no-project", "no-sig", "source"), null); + t.is(await cm.readStageCache("no-project", "no-sig", "no-stage", "no-sig"), null); + t.is(await cm.readTaskMetadata("no-project", "no-sig", "no-task", "project"), null); + t.is(await cm.readResultMetadata("no-project", "no-sig", "no-sig"), null); + cm.close(); +}); + +// CAS delegation + +test.serial("Content round-trip via CacheManager", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + const content = Buffer.from("test content"); + cm.putContent("sha256-test", content); + t.true(cm.hasContent("sha256-test")); + t.deepEqual(cm.readContent("sha256-test"), content); + cm.close(); +}); + +test.serial("hasResourceForStage delegates to content storage", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + t.false(cm.hasResourceForStage("sha256-missing")); + cm.putContent("sha256-exists", Buffer.from("data")); + t.true(cm.hasResourceForStage("sha256-exists")); + cm.close(); +}); + +test.serial("hasResourceForStage throws without integrity", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + + t.throws(() => cm.hasResourceForStage(null), { + message: "Integrity hash must be provided to read from cache" + }); + cm.close(); +}); + +// Singleton via create() + +test.serial("create() returns singleton per cache directory", async (t) => { + const testDir = getUniqueTestDir(); + process.env.UI5_DATA_DIR = testDir; + + const CacheManager = await esmock("../../../../lib/build/cache/CacheManager.js", { + "../../../../lib/config/Configuration.js": { + default: { + fromFile: sinon.stub().resolves({getUi5DataDir: () => null}) + } + } + }); + + const cm1 = await CacheManager.create(testDir); + const cm2 = await CacheManager.create(testDir); + t.is(cm1, cm2, "Same cache directory returns same instance"); +}); diff --git a/packages/project/test/lib/build/cache/ProjectBuildCache.js b/packages/project/test/lib/build/cache/ProjectBuildCache.js new file mode 100644 index 00000000000..74bbc2e7331 --- /dev/null +++ b/packages/project/test/lib/build/cache/ProjectBuildCache.js @@ -0,0 +1,1654 @@ +import test from "ava"; +import sinon from "sinon"; +import ProjectBuildCache from "../../../../lib/build/cache/ProjectBuildCache.js"; + +// Helper to create mock Project instances +function createMockProject(name = "test.project", id = "test-project-id") { + const stages = new Map(); + let currentStage = {getId: () => "initial"}; + let resultStageReader = null; + + // Create a reusable reader with both byGlob and byPath + const createReader = () => ({ + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }); + + const projectResources = { + getStage: sinon.stub().returns({ + getId: () => currentStage.id || "initial", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([]) + }) + }), + useStage: sinon.stub().callsFake((stageName) => { + currentStage = {id: stageName}; + }), + setStage: sinon.stub().callsFake((stageName, stage) => { + stages.set(stageName, stage); + }), + initStages: sinon.stub(), + setResultStage: sinon.stub().callsFake((reader) => { + resultStageReader = reader; + }), + useResultStage: sinon.stub().callsFake(() => { + currentStage = {id: "result"}; + }), + getResourceTagOperations: sinon.stub().returns({ + projectTagOperations: new Map(), + buildTagOperations: new Map(), + }), + buildFinished: sinon.stub(), + setFrozenSourceReader: sinon.stub(), + importTagOperations: sinon.stub(), + }; + + return { + getName: () => name, + getId: () => id, + getSourceReader: sinon.stub().callsFake(() => createReader()), + getReader: sinon.stub().callsFake(() => createReader()), + getProjectResources: () => projectResources, + _getCurrentStage: () => currentStage, + _getResultStageReader: () => resultStageReader + }; +} + +// Helper to create mock CacheManager instances +function createMockCacheManager() { + return { + readIndexCache: sinon.stub().resolves(null), + writeIndexCache: sinon.stub().resolves(), + readStageCache: sinon.stub().resolves(null), + writeStageCache: sinon.stub().resolves(), + readResultMetadata: sinon.stub().resolves(null), + writeResultMetadata: sinon.stub().resolves(), + readTaskMetadata: sinon.stub().resolves(null), + writeTaskMetadata: sinon.stub().resolves(), + writeStageResource: sinon.stub().resolves(), + hasContent: sinon.stub().returns(false), + readContent: sinon.stub().returns(Buffer.from("test")), + readContentRaw: sinon.stub().returns(Buffer.from("test")), + putContent: sinon.stub(), + hasResourceForStage: sinon.stub().returns(false), + beginContentBatch: sinon.stub(), + endContentBatch: sinon.stub(), + rollbackContentBatch: sinon.stub(), + beginMetadataBatch: sinon.stub(), + endMetadataBatch: sinon.stub(), + rollbackMetadataBatch: sinon.stub(), + }; +} + +// Helper to create mock Resource instances +function createMockResource(path, integrity = "test-hash", lastModified = 1000, size = 100, inode = 1) { + return { + getOriginalPath: () => path, + getPath: () => path, + getIntegrity: async () => integrity, + getLastModified: () => lastModified, + getSize: async () => size, + getInode: () => inode, + getTags: () => null, + getBuffer: async () => Buffer.from("test content"), + getStream: () => null + }; +} + +test.afterEach.always(() => { + sinon.restore(); +}); + +// ===== CREATION AND INITIALIZATION TESTS ===== + +test("Create ProjectBuildCache instance", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const buildSignature = "test-signature"; + + const cache = await ProjectBuildCache.create(project, buildSignature, cacheManager); + await cache.initSourceIndex(); + + t.truthy(cache, "ProjectBuildCache instance created"); + t.true(cacheManager.readIndexCache.called, "Index cache was attempted to be loaded"); +}); + +test("Create with existing index cache", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const buildSignature = "test-signature"; + + const resource = createMockResource("/test.js", "hash1", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resource]) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash1", + children: { + "test.js": { + hash: "hash1", + metadata: { + path: "/test.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [["task1", false]] + }; + + // Mock task metadata responses + cacheManager.readTaskMetadata.callsFake((projectId, buildSig, taskName, type) => { + if (type === "project") { + return Promise.resolve({ + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }); + } else if (type === "dependencies") { + return Promise.resolve({ + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }); + } + return Promise.resolve(null); + }); + + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, buildSignature, cacheManager); + await cache.initSourceIndex(); + + t.truthy(cache, "Cache created with existing index"); + const taskCache = cache.getTaskCache("task1"); + t.truthy(taskCache, "Task cache loaded from index"); +}); + +test("Initialize without any cache", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const buildSignature = "test-signature"; + + const cache = await ProjectBuildCache.create(project, buildSignature, cacheManager); + await cache.initSourceIndex(); + + t.false(cache.isFresh(), "Cache is not fresh when empty"); +}); + +test("isFresh returns false for empty cache", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + t.false(cache.isFresh(), "Empty cache is not fresh"); +}); + +test("getTaskCache returns undefined for non-existent task", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + t.is(cache.getTaskCache("nonexistent"), undefined, "Returns undefined"); +}); + +// ===== TASK MANAGEMENT TESTS ===== + +test("setTasks initializes project stages", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["task1", "task2", "task3"]); + + t.true(project.getProjectResources().initStages.calledOnce, "initStages called once"); + t.deepEqual( + project.getProjectResources().initStages.firstCall.args[0], + ["task/task1", "task/task2", "task/task3"], + "Stage names generated correctly" + ); +}); + +test("setTasks with empty task list", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks([]); + + t.true(project.getProjectResources().initStages.calledWith([]), "initStages called with empty array"); +}); + +test("allTasksCompleted switches to result stage", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + const changedPaths = await cache.allTasksCompleted(); + + t.true(project.getProjectResources().useResultStage.calledOnce, "useResultStage called"); + t.true(Array.isArray(changedPaths), "Returns array of changed paths"); + t.true(cache.isFresh(), "Cache is fresh after all tasks completed"); +}); + +test("allTasksCompleted returns changed resource paths", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + // Create cache with existing index to be able to track changes + const resource = createMockResource("/test.js", "hash1", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resource]) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash1", + children: { + "test.js": { + hash: "hash1", + metadata: { + path: "/test.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [] + }; + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + // Simulate some changes - change tracking happens during prepareProjectBuildAndValidateCache + cache.projectSourcesChanged(["/test.js"]); + + const changedPaths = await cache.allTasksCompleted(); + + t.true(Array.isArray(changedPaths), "Returns array of changed paths"); +}); + +// ===== TASK EXECUTION AND RECORDING TESTS ===== + +test("prepareTaskExecutionAndValidateCache: task needs execution when no cache exists", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["myTask"]); + const canUseCache = await cache.prepareTaskExecutionAndValidateCache("myTask"); + + t.false(canUseCache, "Task cannot use cache"); + t.true(project.getProjectResources().useStage.calledWith("task/myTask"), "Project switched to task stage"); +}); + +test("prepareTaskExecutionAndValidateCache: switches project to correct stage", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["task1", "task2"]); + await cache.prepareTaskExecutionAndValidateCache("task2"); + + t.true(project.getProjectResources().useStage.calledWith("task/task2"), "Switched to task2 stage"); +}); + +test("recordTaskResult: creates task cache", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["newTask"]); + await cache.prepareTaskExecutionAndValidateCache("newTask"); + + const projectRequests = {paths: new Set(["/input.js"]), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + + await cache.recordTaskResult("newTask", projectRequests, dependencyRequests, null, false); + + const taskCache = cache.getTaskCache("newTask"); + t.truthy(taskCache, "Task cache created"); +}); + +test("recordTaskResult with empty requests", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["task1"]); + await cache.prepareTaskExecutionAndValidateCache("task1"); + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + + await cache.recordTaskResult("task1", projectRequests, dependencyRequests, null, false); + + const taskCache = cache.getTaskCache("task1"); + t.truthy(taskCache, "Task cache created even with no requests"); +}); + +// ===== DELTA (CACHEINFO) PATH IN RECORDTASKRESULT TESTS ===== + +test("recordTaskResult with cacheInfo: merges resources from previous stage, skipping already-written paths", + async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + // Resources written by the delta execution + const deltaWrittenRes = createMockResource("/a.js", "hash-a-new", 2000, 200, 2); + const writeStub = sinon.stub().resolves(); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([deltaWrittenRes]), + write: writeStub, + }) + }); + + // Resources from the previous stage cache (Reader-type: has byGlob directly) + const prevResA = createMockResource("/a.js", "hash-a-old", 1000, 100, 1); + const prevResB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + const prevResC = createMockResource("/c.js", "hash-c", 1000, 100, 3); + + const cacheInfo = { + previousStageCache: { + signature: "prev-proj-prev-dep", + stage: { + byGlob: sinon.stub().resolves([prevResA, prevResB, prevResC]), + }, + writtenResourcePaths: ["/a.js", "/b.js", "/c.js"], + projectTagOperations: undefined, + buildTagOperations: undefined, + }, + newSignature: "new-proj-new-dep", + changedProjectResourcePaths: ["/a.js"], + changedDependencyResourcePaths: [], + }; + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, cacheInfo, false); + + t.is(writeStub.callCount, 2, "Write called for 2 non-overlapping resources"); + const writtenPaths = writeStub.getCalls().map((call) => call.args[0].getOriginalPath()); + t.true(writtenPaths.includes("/b.js"), "Previous resource /b.js merged into stage"); + t.true(writtenPaths.includes("/c.js"), "Previous resource /c.js merged into stage"); + t.false(writtenPaths.includes("/a.js"), "Already-written /a.js not merged"); + }); + +test("recordTaskResult with cacheInfo: calls importTagOperations with previous stage cache tags", + async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + const writeStub = sinon.stub().resolves(); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([]), + write: writeStub, + }) + }); + + const prevProjectTags = new Map([["/x.js", new Map([["ui5:IsDebugVariant", true]])]]); + const prevBuildTags = new Map([["/y.js", new Map([["ui5:OmitFromBuildResult", true]])]]); + + const cacheInfo = { + previousStageCache: { + signature: "prev-proj-prev-dep", + stage: { + byGlob: sinon.stub().resolves([]), + }, + writtenResourcePaths: [], + projectTagOperations: prevProjectTags, + buildTagOperations: prevBuildTags, + }, + newSignature: "new-proj-new-dep", + changedProjectResourcePaths: [], + changedDependencyResourcePaths: [], + }; + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, cacheInfo, false); + + const importStub = project.getProjectResources().importTagOperations; + t.true(importStub.calledOnce, "importTagOperations called once"); + t.is(importStub.firstCall.args[0], prevProjectTags, + "Called with previous stage projectTagOperations"); + t.is(importStub.firstCall.args[1], prevBuildTags, + "Called with previous stage buildTagOperations"); + }); + +test("recordTaskResult with cacheInfo: merges tag operations with current delta ops taking precedence", + async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + // Delta execution's own tag operations — /a.js IsDebugVariant overrides previous value + project.getProjectResources().getResourceTagOperations.returns({ + projectTagOperations: new Map([["/a.js", new Map([["ui5:IsDebugVariant", false]])]]), + buildTagOperations: new Map([["/c.js", new Map([["ui5:NewBuildTag", "val"]])]]), + }); + + // Set up stage mock with full resource metadata for writeCache to work + const writtenRes = createMockResource("/a.js", "hash-a", 2000, 200, 2); + const writeStub = sinon.stub().resolves(); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([writtenRes]), + write: writeStub, + }) + }); + + const cacheInfo = { + previousStageCache: { + signature: "prev-proj-prev-dep", + stage: { + byGlob: sinon.stub().resolves([]), + }, + writtenResourcePaths: [], + projectTagOperations: new Map([ + ["/a.js", new Map([["ui5:IsDebugVariant", true]])], + ["/b.js", new Map([["ui5:HasDebugVariant", true]])], + ]), + buildTagOperations: new Map([ + ["/a.js", new Map([["ui5:OldBuildTag", "old"]])], + ]), + }, + newSignature: "merged-sig", + changedProjectResourcePaths: [], + changedDependencyResourcePaths: [], + }; + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, cacheInfo, false); + + // Verify merged tags via writeCache -> cacheManager.writeStageCache + await cache.writeCache(); + + const stageCacheCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "task/myTask" + ); + t.is(stageCacheCalls.length, 1, "writeStageCache called once for task/myTask"); + + const metadata = stageCacheCalls[0].args[4]; + + // Project tag ops: /a.js should have delta's value (false), /b.js preserved from previous + t.is(metadata.projectTagOperations["/a.js"]["ui5:IsDebugVariant"], false, + "Delta's value for /a.js takes precedence over previous"); + t.is(metadata.projectTagOperations["/b.js"]["ui5:HasDebugVariant"], true, + "Previous value for /b.js preserved"); + + // Build tag ops: /a.js from previous, /c.js from delta + t.is(metadata.buildTagOperations["/a.js"]["ui5:OldBuildTag"], "old", + "Previous build tag for /a.js preserved"); + t.is(metadata.buildTagOperations["/c.js"]["ui5:NewBuildTag"], "val", + "Delta build tag for /c.js present"); + }); + +test("recordTaskResult with cacheInfo: uses cacheInfo.newSignature as stage signature", + async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + const writtenRes = createMockResource("/a.js", "hash-a", 2000, 200, 2); + const writeStub = sinon.stub().resolves(); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([writtenRes]), + write: writeStub, + }) + }); + + const cacheInfo = { + previousStageCache: { + signature: "prev-proj-prev-dep", + stage: { + byGlob: sinon.stub().resolves([]), + }, + writtenResourcePaths: [], + projectTagOperations: undefined, + buildTagOperations: undefined, + }, + newSignature: "custom-proj-sig-custom-dep-sig", + changedProjectResourcePaths: [], + changedDependencyResourcePaths: [], + }; + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, cacheInfo, false); + + await cache.writeCache(); + + const stageCacheCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "task/myTask" + ); + t.is(stageCacheCalls.length, 1, "writeStageCache called once for task/myTask"); + t.is(stageCacheCalls[0].args[3], "custom-proj-sig-custom-dep-sig", + "Stage signature comes from cacheInfo.newSignature"); + }); + +test("recordTaskResult with cacheInfo: uses getCachedWriter fallback when getWriter returns null", + async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + const writeStub = sinon.stub().resolves(); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([]), + write: writeStub, + }) + }); + + // Previous stage is a Stage-type (no byGlob), getWriter returns null, getCachedWriter used + const prevResE = createMockResource("/e.js", "hash-e", 1000, 100, 5); + const getCachedWriterStub = sinon.stub().returns({ + byGlob: sinon.stub().resolves([prevResE]), + }); + + const cacheInfo = { + previousStageCache: { + signature: "prev-proj-prev-dep", + stage: { + getWriter: sinon.stub().returns(null), + getCachedWriter: getCachedWriterStub, + }, + writtenResourcePaths: ["/e.js"], + projectTagOperations: undefined, + buildTagOperations: undefined, + }, + newSignature: "new-proj-new-dep", + changedProjectResourcePaths: [], + changedDependencyResourcePaths: [], + }; + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, cacheInfo, false); + + t.true(getCachedWriterStub.calledOnce, "getCachedWriter used as fallback"); + t.is(writeStub.callCount, 1, "Write called for 1 resource from cached writer"); + t.is(writeStub.firstCall.args[0].getOriginalPath(), "/e.js", + "Resource /e.js merged from getCachedWriter"); + }); + +// ===== RESOURCE CHANGE TRACKING TESTS ===== + +test("projectSourcesChanged: marks cache as requiring validation", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + // Create cache with existing index + const resource = createMockResource("/test.js", "hash1", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resource]) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash1", + children: { + "test.js": { + hash: "hash1", + metadata: { + path: "/test.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [] + }; + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + cache.projectSourcesChanged(["/test.js"]); + + t.false(cache.isFresh(), "Cache is not fresh after changes"); +}); + +test("dependencyResourcesChanged: marks cache as requiring validation", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + // Create cache with existing index + const resource = createMockResource("/test.js", "hash1", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resource]) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash1", + children: { + "test.js": { + hash: "hash1", + metadata: { + path: "/test.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [] + }; + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + cache.dependencyResourcesChanged(["/dep.js"]); + + t.false(cache.isFresh(), "Cache is not fresh after dependency changes"); +}); + +test("projectSourcesChanged: tracks multiple changes", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + cache.projectSourcesChanged(["/test1.js"]); + cache.projectSourcesChanged(["/test2.js", "/test3.js"]); + + // Changes are tracked internally + t.pass("Multiple changes tracked"); +}); + +test("prepareProjectBuildAndValidateCache: returns false for empty cache", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + const mockDependencyReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }; + + const result = await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + + t.is(result, false, "Returns false for empty cache"); +}); + +test("_refreshDependencyIndices: updates dependency indices", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + // Create cache with existing task + const resource = createMockResource("/test.js", "hash1", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resource]) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash1", + children: { + "test.js": { + hash: "hash1", + metadata: { + path: "/test.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [["task1", false]] + }; + + // Mock task metadata responses + cacheManager.readTaskMetadata.callsFake((projectId, buildSig, taskName, type) => { + if (type === "project") { + return Promise.resolve({ + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }); + } else if (type === "dependencies") { + return Promise.resolve({ + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }); + } + return Promise.resolve(null); + }); + + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + const mockDependencyReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }; + + await cache._refreshDependencyIndices(mockDependencyReader); + + t.pass("Dependency indices refreshed"); +}); + +// ===== CACHE STORAGE TESTS ===== + +test("writeCache: writes index and stage caches", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + project.getReader.returns({ + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }); + + await cache.writeCache(); + + t.true(cacheManager.writeIndexCache.called, "Index cache written"); +}); + +test("writeCache: skips writing unchanged caches", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + // Create cache with existing index + const resource = createMockResource("/test.js", "hash1", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resource]) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash1", + children: { + "test.js": { + hash: "hash1", + metadata: { + path: "/test.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [] + }; + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + project.getReader.returns({ + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }); + + // Write cache multiple times + await cache.writeCache(); + const firstCallCount = cacheManager.writeIndexCache.callCount; + + await cache.writeCache(); + const secondCallCount = cacheManager.writeIndexCache.callCount; + + t.is(secondCallCount, firstCallCount + 1, "Index written each time"); +}); + +// ===== EDGE CASES ===== + +test("Create cache with empty project name", async (t) => { + const project = createMockProject("", "empty-project"); + const cacheManager = createMockCacheManager(); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + t.truthy(cache, "Cache created with empty project name"); +}); + +test("Empty task list doesn't fail", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + await cache.setTasks([]); + + t.true(project.getProjectResources().initStages.calledWith([]), "initStages called with empty array"); +}); + +// ===== CAS SOURCE FREEZE TESTS ===== + +// Helper: Creates a ProjectBuildCache with a populated source index containing the given resources. +// Runs a single task that writes `writtenPaths` and then calls allTasksCompleted. +// Returns {cache, project, cacheManager} for assertions. +async function buildCacheWithTaskResult(resources, writtenPaths = []) { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const buildSignature = "test-sig"; + + // Source reader returns the given resources for byGlob and individual byPath + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves(resources), + byPath: sinon.stub().callsFake((path) => { + const res = resources.find((r) => r.getPath() === path); + return Promise.resolve(res || null); + }) + })); + + const cache = await ProjectBuildCache.create(project, buildSignature, cacheManager); + await cache.initSourceIndex(); + + // Set up and execute a task + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + // Simulate task writing some resources + const writtenResources = writtenPaths.map( + (p) => createMockResource(p, `hash-${p}`, 2000, 200, 2) + ); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves(writtenResources) + }) + }); + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, null, false); + + return {cache, project, cacheManager}; +} + +test("freezeUntransformedSources: writes only untransformed source files to CAS", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + const resC = createMockResource("/c.js", "hash-c", 1000, 100, 3); + const resD = createMockResource("/d.js", "hash-d", 1000, 100, 4); + + // Task writes /a.js and /b.js, so /c.js and /d.js are untransformed + const {cache, cacheManager} = await buildCacheWithTaskResult( + [resA, resB, resC, resD], + ["/a.js", "/b.js"] + ); + + await cache.allTasksCompleted(); + + // putContent should be called for untransformed files /c.js and /d.js + const putContentCalls = cacheManager.putContent.getCalls(); + const writtenIntegrities = putContentCalls.map((call) => call.args[0]); + t.true(writtenIntegrities.includes("hash-c"), "Untransformed /c.js written to CAS"); + t.true(writtenIntegrities.includes("hash-d"), "Untransformed /d.js written to CAS"); + t.false(writtenIntegrities.includes("hash-a"), "Transformed /a.js NOT written to CAS by freeze"); + t.false(writtenIntegrities.includes("hash-b"), "Transformed /b.js NOT written to CAS by freeze"); +}); + +test("freezeUntransformedSources: early return when all sources overlayed", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + + // Task writes all source files + const {cache, cacheManager} = await buildCacheWithTaskResult( + [resA, resB], + ["/a.js", "/b.js"] + ); + + await cache.allTasksCompleted(); + + // writeStageCache should NOT be called with stageId "source" + const sourceStageCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + t.is(sourceStageCalls.length, 0, "No source stage cache written when all files overlayed"); +}); + +test("freezeUntransformedSources: writes stage cache with correct stageId and signature", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + + // Task writes only /a.js, so /b.js is untransformed + const {cache, project, cacheManager} = await buildCacheWithTaskResult( + [resA, resB], + ["/a.js"] + ); + + await cache.allTasksCompleted(); + + // writeStageCache called with stageId "source" + const sourceStageCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + t.is(sourceStageCalls.length, 1, "writeStageCache called once for source stage"); + + const call = sourceStageCalls[0]; + t.is(call.args[0], "test-project-id", "Correct project ID"); + t.is(call.args[1], "test-sig", "Correct build signature"); + t.is(call.args[2], "source", "Correct stageId"); + t.is(typeof call.args[3], "string", "Signature is a string"); + t.truthy(call.args[4].resourceMetadata, "Metadata contains resourceMetadata"); + t.truthy(call.args[4].resourceMetadata["/b.js"], "resourceMetadata has entry for untransformed /b.js"); + t.falsy(call.args[4].resourceMetadata["/a.js"], "resourceMetadata does NOT have entry for transformed /a.js"); + + // Verify setFrozenSourceReader was called on project resources + const projectResources = project.getProjectResources(); + t.true(projectResources.setFrozenSourceReader.calledOnce, + "setFrozenSourceReader called once after freeze"); + t.truthy(projectResources.setFrozenSourceReader.firstCall.args[0], + "setFrozenSourceReader called with a reader"); +}); + +test("freezeUntransformedSources: throws when source file not found", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + // First call during init returns both resources; second call during freeze returns only /a.js + let callCount = 0; + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().callsFake(() => { + callCount++; + if (callCount <= 1) { + return Promise.resolve([resA, resB]); + } + return Promise.resolve([resA, resB]); + }), + byPath: sinon.stub().callsFake((path) => { + // During freeze, /b.js disappears + if (path === "/b.js") { + return Promise.resolve(null); + } + return Promise.resolve(resA); + }) + })); + + const cache = await ProjectBuildCache.create(project, "test-sig", cacheManager); + await cache.initSourceIndex(); + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves([createMockResource("/a.js", "hash-a", 2000, 200, 2)]) + }) + }); + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, null, false); + + const error = await t.throwsAsync(() => cache.allTasksCompleted()); + t.true(error.message.includes("not found during CAS freeze"), + "Error message mentions CAS freeze"); +}); + +// ===== DELTA FREEZE REUSE TESTS ===== + +/** + * Helper that creates a ProjectBuildCache from a warm cache (with index cache and previous + * frozen source metadata), runs a single task, and returns the cache + mocks for assertions. + * + * @param {object} options + * @param {Array} options.sourceResources Resources returned by the source reader + * @param {string[]} options.taskWrittenPaths Paths written by the task + * @param {object} options.previousFrozenMetadata Previous build's frozen source resourceMetadata + * @param {string} [options.cachedSourceSignature="prev-source-sig"] Signature from the cached index tree root + */ +async function buildCacheWithWarmCacheAndTaskResult({ + sourceResources, taskWrittenPaths, previousFrozenMetadata, cachedSourceSignature = "prev-source-sig" +}) { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const buildSignature = "test-sig"; + + // Source reader returns given resources for byGlob and individual byPath + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves(sourceResources), + byPath: sinon.stub().callsFake((path) => { + const res = sourceResources.find((r) => r.getPath() === path); + return Promise.resolve(res || null); + }) + })); + + // Build an indexCache that matches the source resources with no changes detected + const children = {}; + for (const res of sourceResources) { + const p = res.getPath(); + const name = p.slice(1); // strip leading / + const integrity = await res.getIntegrity(); + const size = await res.getSize(); + children[name] = { + name, + type: "resource", + hash: `node-hash-${name}`, + integrity, + lastModified: res.getLastModified(), + size, + inode: res.getInode(), + tags: null + }; + } + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 500, // Earlier than resource lastModified (1000) so matchMetadata says unchanged + root: { + name: "", + type: "directory", + hash: cachedSourceSignature, + children + } + }, + tasks: [["myTask", 0]] + }; + + // Mock task metadata for the cached task + cacheManager.readTaskMetadata.callsFake((projectId, buildSig, taskName, type) => { + return Promise.resolve({ + requestSetGraph: {nodes: [], nextId: 1}, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }); + }); + + // Return previous frozen source metadata when readStageCache is called + // with the cached source signature during initSourceIndex + cacheManager.readStageCache.callsFake((projectId, buildSig, stageId, stageSignature) => { + if (stageId === "source" && stageSignature === cachedSourceSignature && previousFrozenMetadata) { + return Promise.resolve({resourceMetadata: previousFrozenMetadata}); + } + return Promise.resolve(null); + }); + + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, buildSignature, cacheManager); + await cache.initSourceIndex(); + + // Set up and execute a task + await cache.setTasks(["myTask"]); + await cache.prepareTaskExecutionAndValidateCache("myTask"); + + // Simulate task writing some resources + const writtenResources = taskWrittenPaths.map( + (p) => createMockResource(p, `hash-${p}`, 2000, 200, 2) + ); + project.getProjectResources().getStage.returns({ + getId: () => "task/myTask", + getWriter: sinon.stub().returns({ + byGlob: sinon.stub().resolves(writtenResources) + }) + }); + + const projectRequests = {paths: new Set(), patterns: new Set()}; + const dependencyRequests = {paths: new Set(), patterns: new Set()}; + await cache.recordTaskResult("myTask", projectRequests, dependencyRequests, null, false); + + return {cache, project, cacheManager}; +} + +test("freezeUntransformedSources: fast path — reuses all entries from previous metadata", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + const resC = createMockResource("/c.js", "hash-c", 1000, 100, 3); + const resD = createMockResource("/d.js", "hash-d", 1000, 100, 4); + + // Previous frozen metadata covers /c.js and /d.js (the untransformed ones) + const previousFrozenMetadata = { + "/c.js": {integrity: "hash-c", lastModified: 1000, size: 100, inode: 3}, + "/d.js": {integrity: "hash-d", lastModified: 1000, size: 100, inode: 4}, + }; + + // Task writes /a.js and /b.js — so /c.js and /d.js are untransformed + const {cache, cacheManager} = await buildCacheWithWarmCacheAndTaskResult({ + sourceResources: [resA, resB, resC, resD], + taskWrittenPaths: ["/a.js", "/b.js"], + previousFrozenMetadata, + }); + + await cache.allTasksCompleted(); + + // writeStageResource should NOT be called — all metadata was reused + t.is(cacheManager.writeStageResource.callCount, 0, + "No CAS writes needed when all metadata is reused"); + + // writeStageCache SHOULD be called with the reused metadata + const sourceStageCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + t.is(sourceStageCalls.length, 1, "writeStageCache called once for source stage"); + + const writtenMetadata = sourceStageCalls[0].args[4].resourceMetadata; + t.truthy(writtenMetadata["/c.js"], "Reused metadata for /c.js"); + t.truthy(writtenMetadata["/d.js"], "Reused metadata for /d.js"); + t.falsy(writtenMetadata["/a.js"], "No metadata for transformed /a.js"); + t.falsy(writtenMetadata["/b.js"], "No metadata for transformed /b.js"); + t.is(writtenMetadata["/c.js"].integrity, "hash-c", "Correct integrity for /c.js"); +}); + +test("freezeUntransformedSources: delta path — only reads new files missing from previous metadata", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resB = createMockResource("/b.js", "hash-b", 1000, 100, 2); + const resC = createMockResource("/c.js", "hash-c", 1000, 100, 3); + const resD = createMockResource("/d.js", "hash-d", 1000, 100, 4); + const resE = createMockResource("/e.js", "hash-e", 1000, 100, 5); + + // Previous frozen metadata only covers /c.js and /d.js + // /e.js is a newly added file (not in previous metadata) + const previousFrozenMetadata = { + "/c.js": {integrity: "hash-c", lastModified: 1000, size: 100, inode: 3}, + "/d.js": {integrity: "hash-d", lastModified: 1000, size: 100, inode: 4}, + }; + + // Task writes /a.js and /b.js — so /c.js, /d.js, /e.js are untransformed + const {cache, cacheManager} = await buildCacheWithWarmCacheAndTaskResult({ + sourceResources: [resA, resB, resC, resD, resE], + taskWrittenPaths: ["/a.js", "/b.js"], + previousFrozenMetadata, + }); + + await cache.allTasksCompleted(); + + // putContent should be called only for /e.js (the new file) + const putContentCalls = cacheManager.putContent.getCalls(); + const writtenIntegrities = putContentCalls.map((call) => call.args[0]); + t.is(writtenIntegrities.length, 1, "Only 1 CAS write for the new file"); + t.true(writtenIntegrities.includes("hash-e"), "New file /e.js written to CAS"); + + // Merged metadata should contain all 3 untransformed files + const sourceStageCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + const writtenMetadata = sourceStageCalls[0].args[4].resourceMetadata; + t.truthy(writtenMetadata["/c.js"], "Reused metadata for /c.js"); + t.truthy(writtenMetadata["/d.js"], "Reused metadata for /d.js"); + t.truthy(writtenMetadata["/e.js"], "New metadata for /e.js"); + t.is(writtenMetadata["/c.js"].integrity, "hash-c", "Correct reused integrity for /c.js"); + t.is(writtenMetadata["/e.js"].integrity, "hash-e", "Correct new integrity for /e.js"); +}); + +test("freezeUntransformedSources: removed file excluded from reused metadata", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + const resC = createMockResource("/c.js", "hash-c", 1000, 100, 3); + + // Previous frozen metadata contains /c.js and /removed.js + // /removed.js no longer exists in source index + const previousFrozenMetadata = { + "/c.js": {integrity: "hash-c", lastModified: 1000, size: 100, inode: 3}, + "/removed.js": {integrity: "hash-removed", lastModified: 1000, size: 100, inode: 9}, + }; + + // Task writes /a.js — /c.js is untransformed. /removed.js is gone from sources. + const {cache, cacheManager} = await buildCacheWithWarmCacheAndTaskResult({ + sourceResources: [resA, resC], + taskWrittenPaths: ["/a.js"], + previousFrozenMetadata, + }); + + await cache.allTasksCompleted(); + + const sourceStageCalls = cacheManager.writeStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + const writtenMetadata = sourceStageCalls[0].args[4].resourceMetadata; + t.truthy(writtenMetadata["/c.js"], "Reused metadata for /c.js"); + t.falsy(writtenMetadata["/removed.js"], "Removed file NOT in metadata"); +}); + +// ===== RESULT METADATA SHAPE TESTS ===== + +test("writeResultCache: metadata includes sourceStageSignature", async (t) => { + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + + const {cache, cacheManager} = await buildCacheWithTaskResult( + [resA], + ["/a.js"] + ); + + await cache.allTasksCompleted(); + await cache.writeCache(); + + // writeResultMetadata should have been called + t.true(cacheManager.writeResultMetadata.called, "writeResultMetadata was called"); + + const metadataCall = cacheManager.writeResultMetadata.getCall(0); + const metadata = metadataCall.args[3]; + t.truthy(metadata.stageSignatures, "Metadata contains stageSignatures"); + t.is(typeof metadata.sourceStageSignature, "string", + "Metadata contains sourceStageSignature as string"); +}); + +// ===== RESTORE FROZEN SOURCES TESTS ===== + +test("restoreFrozenSources: cache miss skips gracefully", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resA]), + byPath: sinon.stub().resolves(resA) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash-a", + children: { + "a.js": { + hash: "hash-a", + metadata: { + path: "/a.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [["task1", false]] + }; + cacheManager.readIndexCache.resolves(indexCache); + cacheManager.readTaskMetadata.callsFake((projectId, buildSig, taskName, type) => { + return Promise.resolve({ + requestSetGraph: {nodes: [], nextId: 1}, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: true + }); + }); + + // readResultMetadata returns metadata WITH sourceStageSignature + cacheManager.readResultMetadata.resolves({ + stageSignatures: {"task/task1": "sig1-sig2"}, + sourceStageSignature: "source-sig-123" + }); + + // readStageCache for task stage returns valid data, but for "source" stage returns null + cacheManager.readStageCache.callsFake((projectId, buildSig, stageName, signature) => { + if (stageName === "source") { + return Promise.resolve(null); // Cache miss for source stage + } + // Return valid stage for task stages + return Promise.resolve({ + resourceMetadata: {"/a.js": {integrity: "hash-a", lastModified: 1000, size: 100, inode: 1}}, + projectTagOperations: {}, + buildTagOperations: {}, + }); + }); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + const mockDepReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }; + const result = await cache.prepareProjectBuildAndValidateCache(mockDepReader); + + // Should succeed without error — cache miss for source stage is non-fatal + t.truthy(result, "prepareProjectBuildAndValidateCache succeeds despite source cache miss"); + + // setFrozenSourceReader should NOT have been called + t.false(project.getProjectResources().setFrozenSourceReader.called, + "setFrozenSourceReader not called on cache miss"); + + // Verify readStageCache was called with "source" stageId + const sourceReadCalls = cacheManager.readStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + t.true(sourceReadCalls.length > 0, "readStageCache was called for source stage"); +}); + +test("restoreFrozenSources: cache hit creates CAS reader", async (t) => { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + + const resA = createMockResource("/a.js", "hash-a", 1000, 100, 1); + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves([resA]), + byPath: sinon.stub().resolves(resA) + })); + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 1000, + root: { + hash: "hash-a", + children: { + "a.js": { + hash: "hash-a", + metadata: { + path: "/a.js", + lastModified: 1000, + size: 100, + inode: 1 + } + } + } + } + }, + tasks: [["task1", false]] + }; + cacheManager.readIndexCache.resolves(indexCache); + cacheManager.readTaskMetadata.callsFake((projectId, buildSig, taskName, type) => { + return Promise.resolve({ + requestSetGraph: {nodes: [], nextId: 1}, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: true + }); + }); + + // readResultMetadata returns metadata WITH sourceStageSignature + cacheManager.readResultMetadata.resolves({ + stageSignatures: {"task/task1": "sig1-sig2"}, + sourceStageSignature: "source-sig-456" + }); + + // readStageCache returns valid data for both task and source stages + cacheManager.readStageCache.callsFake((projectId, buildSig, stageName, signature) => { + if (stageName === "source") { + return Promise.resolve({ + resourceMetadata: { + "/b.js": {integrity: "hash-b", lastModified: 1000, size: 100, inode: 2} + }, + }); + } + return Promise.resolve({ + resourceMetadata: {"/a.js": {integrity: "hash-a", lastModified: 1000, size: 100, inode: 1}}, + projectTagOperations: {}, + buildTagOperations: {}, + }); + }); + + const cache = await ProjectBuildCache.create(project, "sig", cacheManager); + await cache.initSourceIndex(); + + const mockDepReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }; + const result = await cache.prepareProjectBuildAndValidateCache(mockDepReader); + + t.truthy(result, "Cache restored successfully"); + + // Verify readStageCache was called with "source" stageId and the correct signature + // Two calls expected: first from #initSourceIndex (pre-populating knownCasIntegrities using the + // cached tree's root hash), second from #restoreFrozenSources (using the result cache's source signature) + const sourceReadCalls = cacheManager.readStageCache.getCalls().filter( + (call) => call.args[2] === "source" + ); + t.is(sourceReadCalls.length, 2, "readStageCache was called twice for source stage"); + t.is(sourceReadCalls[0].args[3], "hash-a", + "First readStageCache call uses cached tree root hash for knownCasIntegrities pre-population"); + t.is(sourceReadCalls[1].args[3], "source-sig-456", + "Second readStageCache call uses correct source signature for frozen source restore"); + + // Verify setFrozenSourceReader was called on project resources after restore + const projectResources = project.getProjectResources(); + t.true(projectResources.setFrozenSourceReader.calledOnce, + "setFrozenSourceReader called once after restore"); + t.truthy(projectResources.setFrozenSourceReader.firstCall.args[0], + "setFrozenSourceReader called with a reader"); +}); + +// ===== DEPENDENCY INDEX REFRESH OPTIMIZATION TESTS ===== + +// Helper: Creates a ProjectBuildCache in RESTORING_DEPENDENCY_INDICES state +// (warm cache loaded from disk with task metadata) and spies on _refreshDependencyIndices. +async function createCacheInRestoringState({ + resources = [createMockResource("/test.js", "hash1", 1000, 100, 1)], + tasks = [["task1", false]], +} = {}) { + const project = createMockProject(); + const cacheManager = createMockCacheManager(); + const buildSignature = "test-signature"; + + project.getSourceReader.callsFake(() => ({ + byGlob: sinon.stub().resolves(resources), + byPath: sinon.stub().callsFake((path) => { + const res = resources.find((r) => r.getPath() === path); + return Promise.resolve(res || null); + }) + })); + + // Build an indexCache matching the resources (no changes detected) + const children = {}; + for (const res of resources) { + const p = res.getPath(); + const name = p.slice(1); + children[name] = { + name, + type: "resource", + hash: `node-hash-${name}`, + integrity: await res.getIntegrity(), + lastModified: res.getLastModified(), + size: await res.getSize(), + inode: res.getInode(), + tags: null + }; + } + + const indexCache = { + version: "1.0", + indexTree: { + version: 1, + indexTimestamp: 500, // Earlier than resource lastModified so matchMetadata says unchanged + root: { + name: "", + type: "directory", + hash: "root-hash", + children + } + }, + tasks + }; + + cacheManager.readTaskMetadata.callsFake((projectId, buildSig, taskName, type) => { + return Promise.resolve({ + requestSetGraph: {nodes: [], nextId: 1}, + rootIndices: [], + deltaIndices: [], + unusedAtLeastOnce: false + }); + }); + + cacheManager.readIndexCache.resolves(indexCache); + + const cache = await ProjectBuildCache.create(project, buildSignature, cacheManager); + await cache.initSourceIndex(); + + // Spy on _refreshDependencyIndices so we can verify whether it's called + const refreshSpy = sinon.spy(cache, "_refreshDependencyIndices"); + + const mockDependencyReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null) + }; + + return {cache, project, cacheManager, refreshSpy, mockDependencyReader}; +} + +test("prepareProjectBuildAndValidateCache: skips _refreshDependencyIndices when no dependency " + + "changes propagated (warm cache)", async (t) => { + const {cache, refreshSpy, mockDependencyReader} = await createCacheInRestoringState(); + + // Do NOT call dependencyResourcesChanged — simulates warm cache with no upstream changes. + // In RESTORING_DEPENDENCY_INDICES state, cached dependency indices (from BuildTaskCache.fromCache) + // are already correct, so _refreshDependencyIndices can be skipped. + await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + + t.false(refreshSpy.called, + "_refreshDependencyIndices should NOT be called when no dependency changes were propagated"); +}); + +test("prepareProjectBuildAndValidateCache: dependency changes move state from " + + "RESTORING_DEPENDENCY_INDICES to REQUIRES_UPDATE", async (t) => { + const {cache, refreshSpy, mockDependencyReader} = await createCacheInRestoringState(); + + // Simulate an upstream dependency rebuild propagating changed paths. + // dependencyResourcesChanged() transitions state from RESTORING_DEPENDENCY_INDICES to REQUIRES_UPDATE, + // so the changes go through #flushPendingChanges (incremental delta update) rather than + // the full _refreshDependencyIndices path. + cache.dependencyResourcesChanged(["/dep/lib/SomeModule.js"]); + + await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + + // _refreshDependencyIndices is NOT called because dependencyResourcesChanged() already moved + // the state to REQUIRES_UPDATE, bypassing the RESTORING_DEPENDENCY_INDICES branch entirely. + // Instead, #flushPendingChanges handles the changes via incremental updateDependencyIndices. + t.false(refreshSpy.called, + "_refreshDependencyIndices should NOT be called — changes handled via #flushPendingChanges"); +}); + +test("prepareProjectBuildAndValidateCache: transitions from RESTORING_DEPENDENCY_INDICES " + + "to FRESH state", async (t) => { + const {cache, refreshSpy, mockDependencyReader} = await createCacheInRestoringState(); + + // First call transitions from RESTORING_DEPENDENCY_INDICES to FRESH + await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + t.false(refreshSpy.called, "_refreshDependencyIndices not called on first pass (no changes)"); + + // Second call without new changes — state is already FRESH, no refresh needed + refreshSpy.resetHistory(); + await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + + t.false(refreshSpy.called, + "_refreshDependencyIndices should NOT be called on second invocation (state is FRESH)"); +}); + +test("prepareProjectBuildAndValidateCache: subsequent dependency changes go through " + + "REQUIRES_UPDATE path, not RESTORING_DEPENDENCY_INDICES", async (t) => { + const {cache, refreshSpy, mockDependencyReader} = await createCacheInRestoringState(); + + // First call with no changes — transitions from RESTORING_DEPENDENCY_INDICES to FRESH + await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + t.false(refreshSpy.called, "_refreshDependencyIndices not called on first pass (no changes)"); + + // Now simulate a dependency change (e.g. from BuildServer watch mode) + cache.dependencyResourcesChanged(["/dep/lib/NewChange.js"]); + + // Second call — should go through REQUIRES_UPDATE / #flushPendingChanges, not _refreshDependencyIndices + refreshSpy.resetHistory(); + await cache.prepareProjectBuildAndValidateCache(mockDependencyReader); + + t.false(refreshSpy.called, + "_refreshDependencyIndices should NOT be called for REQUIRES_UPDATE state " + + "(#flushPendingChanges handles it instead)"); +}); diff --git a/packages/project/test/lib/build/cache/ResourceRequestGraph.js b/packages/project/test/lib/build/cache/ResourceRequestGraph.js new file mode 100644 index 00000000000..6f4e7f7089f --- /dev/null +++ b/packages/project/test/lib/build/cache/ResourceRequestGraph.js @@ -0,0 +1,959 @@ +import test from "ava"; +import ResourceRequestGraph, {Request} from "../../../../lib/build/cache/ResourceRequestGraph.js"; + +// Request Class Tests +test("Request: Create path request", (t) => { + const request = new Request("path", "a.js"); + t.is(request.type, "path"); + t.is(request.value, "a.js"); +}); + +test("Request: Create patterns request", (t) => { + const request = new Request("patterns", ["*.js", "*.css"]); + t.is(request.type, "patterns"); + t.deepEqual(request.value, ["*.js", "*.css"]); +}); + +test("Request: Reject invalid type", (t) => { + const error = t.throws(() => { + new Request("invalid-type", "value"); + }, {instanceOf: Error}); + t.is(error.message, "Invalid request type: invalid-type"); +}); + +test("Request: Reject non-string value for path type", (t) => { + const error = t.throws(() => { + new Request("path", ["array", "value"]); + }, {instanceOf: Error}); + t.is(error.message, "Request type 'path' requires value to be a string"); +}); + +test("Request: toKey with string value", (t) => { + const request = new Request("path", "a.js"); + t.is(request.toKey(), "path:a.js"); +}); + +test("Request: toKey with array value", (t) => { + const request = new Request("patterns", ["*.js", "*.css"]); + t.is(request.toKey(), "patterns:[\"*.js\",\"*.css\"]"); +}); + +test("Request: fromKey with string value", (t) => { + const request = Request.fromKey("path:a.js"); + t.is(request.type, "path"); + t.is(request.value, "a.js"); +}); + +test("Request: fromKey with array value", (t) => { + const request = Request.fromKey("patterns:[\"*.js\",\"*.css\"]"); + t.is(request.type, "patterns"); + t.deepEqual(request.value, ["*.js", "*.css"]); +}); + +test("Request: equals returns true for identical requests", (t) => { + const req1 = new Request("path", "a.js"); + const req2 = new Request("path", "a.js"); + t.true(req1.equals(req2)); +}); + +test("Request: equals returns false for different values", (t) => { + const req1 = new Request("path", "a.js"); + const req2 = new Request("path", "b.js"); + t.false(req1.equals(req2)); +}); + +test("Request: equals returns true for identical array values", (t) => { + const req1 = new Request("patterns", ["*.js", "*.css"]); + const req2 = new Request("patterns", ["*.js", "*.css"]); + t.true(req1.equals(req2)); +}); + +test("Request: equals returns false for different array lengths", (t) => { + const req1 = new Request("patterns", ["*.js", "*.css"]); + const req2 = new Request("patterns", ["*.js"]); + t.false(req1.equals(req2)); +}); + +test("Request: equals returns false for different array values", (t) => { + const req1 = new Request("patterns", ["*.js", "*.css"]); + const req2 = new Request("patterns", ["*.js", "*.html"]); + t.false(req1.equals(req2)); +}); + +// ResourceRequestGraph Tests +test("ResourceRequestGraph: Initialize empty graph", (t) => { + const graph = new ResourceRequestGraph(); + t.is(graph.nodes.size, 0); + t.is(graph.nextId, 1); +}); + +test("ResourceRequestGraph: Add first request set (root node)", (t) => { + const graph = new ResourceRequestGraph(); + const requests = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const nodeId = graph.addRequestSet(requests, {result: "xyz-1"}); + + t.is(nodeId, 1); + t.is(graph.nodes.size, 1); + + const node = graph.getNode(nodeId); + t.is(node.id, 1); + t.is(node.parent, null); + t.is(node.addedRequests.size, 2); + t.deepEqual(node.metadata, {result: "xyz-1"}); +}); + +test("ResourceRequestGraph: Add request set with parent relationship", (t) => { + const graph = new ResourceRequestGraph(); + + // Add first request set + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const node1 = graph.addRequestSet(set1, {result: "xyz-1"}); + + // Add second request set (superset of first) + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2, {result: "xyz-2"}); + + // Verify parent relationship + const node2Data = graph.getNode(node2); + t.is(node2Data.parent, node1); + t.is(node2Data.addedRequests.size, 1); + t.true(node2Data.addedRequests.has("path:c.js")); +}); + +test("ResourceRequestGraph: Add request set with no overlap", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const set2 = [new Request("path", "x.js")]; + const node2 = graph.addRequestSet(set2); + + const node2Data = graph.getNode(node2); + t.falsy(node2Data.parent); + t.is(node2Data.addedRequests.size, 1); + t.true(node2Data.addedRequests.has("path:x.js")); +}); + +test("ResourceRequestGraph: getMaterializedRequests returns full set", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2); + + const node2Data = graph.getNode(node2); + const materialized = node2Data.getMaterializedRequests(graph); + + t.is(materialized.length, 3); + const keys = materialized.map((r) => r.toKey()).sort(); + t.deepEqual(keys, ["path:a.js", "path:b.js", "path:c.js"]); +}); + +test("ResourceRequestGraph: getAddedRequests returns only delta", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2); + + const node2Data = graph.getNode(node2); + const added = node2Data.getAddedRequests(); + + t.is(added.length, 1); + t.is(added[0].toKey(), "path:c.js"); +}); + +test("ResourceRequestGraph: findBestParent returns node with largest subset", (t) => { + const graph = new ResourceRequestGraph(); + + // Add first request set + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1, {result: "xyz-1"}); + + // Add second request set (superset) + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2, {result: "xyz-2"}); + + // Query that contains set2 + const query = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js"), + new Request("path", "x.js") + ]; + const match = graph.findBestParent(query); + + // Should return node2 (largest subset: 3 items) + t.is(match, node2); +}); + +test("ResourceRequestGraph: findBestParent returns null when no subset found", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + // Query with no overlap + const query = [ + new Request("path", "x.js"), + new Request("path", "y.js") + ]; + const match = graph.findBestParent(query); + + t.is(match, null); +}); + +test("ResourceRequestGraph: findExactMatch returns node with identical set", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const node1 = graph.addRequestSet(set1); + + const query = [ + new Request("path", "b.js"), + new Request("path", "a.js") // Different order, but same set + ]; + const match = graph.findExactMatch(query); + + t.is(match, node1); +}); + +test("ResourceRequestGraph: findExactMatch returns null for subset", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + graph.addRequestSet(set1); + + const query = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const match = graph.findExactMatch(query); + + t.is(match, null); +}); + +test("ResourceRequestGraph: findExactMatch returns null for superset", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + const query = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const match = graph.findExactMatch(query); + + t.is(match, null); +}); + +test("ResourceRequestGraph: getMetadata returns stored metadata", (t) => { + const graph = new ResourceRequestGraph(); + const metadata = {result: "xyz", cached: true}; + + const set1 = [new Request("path", "a.js")]; + const nodeId = graph.addRequestSet(set1, metadata); + + const retrieved = graph.getMetadata(nodeId); + t.deepEqual(retrieved, metadata); +}); + +test("ResourceRequestGraph: getMetadata returns null for non-existent node", (t) => { + const graph = new ResourceRequestGraph(); + const retrieved = graph.getMetadata(999); + t.is(retrieved, null); +}); + +test("ResourceRequestGraph: setMetadata updates metadata", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const nodeId = graph.addRequestSet(set1, {original: true}); + + graph.setMetadata(nodeId, {updated: true}); + + const retrieved = graph.getMetadata(nodeId); + t.deepEqual(retrieved, {updated: true}); +}); + +test("ResourceRequestGraph: getAllNodeIds returns all node IDs", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const node1 = graph.addRequestSet(set1); + + const set2 = [new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + const ids = graph.getAllNodeIds(); + t.is(ids.length, 2); + t.true(ids.includes(node1)); + t.true(ids.includes(node2)); +}); + +test("ResourceRequestGraph: getAllRequests returns all unique requests", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), // Duplicate + new Request("path", "c.js") + ]; + graph.addRequestSet(set2); + + const allRequests = graph.getAllRequests(); + const keys = allRequests.map((r) => r.toKey()).sort(); + + // Should have 3 unique requests + t.is(keys.length, 3); + t.deepEqual(keys, ["path:a.js", "path:b.js", "path:c.js"]); +}); + +test("ResourceRequestGraph: getStats returns correct statistics", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + graph.addRequestSet(set2); + + const stats = graph.getStats(); + + t.is(stats.nodeCount, 2); + t.is(stats.averageRequestsPerNode, 2.5); // (2 + 3) / 2 + t.is(stats.averageStoredDeltaSize, 1.5); // (2 + 1) / 2 + t.is(stats.maxDepth, 1); // node2 is at depth 1 + t.is(stats.compressionRatio, 0.6); // 3 stored / 5 total +}); + +test("ResourceRequestGraph: toCacheObject exports graph structure", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const node1 = graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const node2 = graph.addRequestSet(set2); + + const exported = graph.toCacheObject(); + + t.is(exported.nodes.length, 2); + t.is(exported.nextId, 3); + + const exportedNode1 = exported.nodes.find((n) => n.id === node1); + t.truthy(exportedNode1); + t.is(exportedNode1.parent, null); + t.deepEqual(exportedNode1.addedRequests, ["path:a.js"]); + + const exportedNode2 = exported.nodes.find((n) => n.id === node2); + t.truthy(exportedNode2); + t.is(exportedNode2.parent, node1); + t.deepEqual(exportedNode2.addedRequests, ["path:b.js"]); +}); + +test("ResourceRequestGraph: fromCache reconstructs graph", (t) => { + const graph1 = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const node1 = graph1.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const node2 = graph1.addRequestSet(set2); + + // Export and reconstruct + const exported = graph1.toCacheObject(); + const graph2 = ResourceRequestGraph.fromCache(exported); + + // Verify reconstruction + t.is(graph2.nodes.size, 2); + t.is(graph2.nextId, 3); + + const reconstructedNode1 = graph2.getNode(node1); + t.truthy(reconstructedNode1); + t.is(reconstructedNode1.parent, null); + t.is(reconstructedNode1.addedRequests.size, 1); + + const reconstructedNode2 = graph2.getNode(node2); + t.truthy(reconstructedNode2); + t.is(reconstructedNode2.parent, node1); + t.is(reconstructedNode2.addedRequests.size, 1); + + // Verify materialized sets work correctly + const materialized = reconstructedNode2.getMaterializedRequests(graph2); + t.is(materialized.length, 2); +}); + +test("ResourceRequestGraph: Handles different request types", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("patterns", ["*.js"]), + ]; + const nodeId = graph.addRequestSet(set1); + + const node = graph.getNode(nodeId); + const materialized = node.getMaterializedRequests(graph); + + t.is(materialized.length, 2); + + const types = materialized.map((r) => r.type).sort(); + t.deepEqual(types, ["path", "patterns"]); +}); + +test("ResourceRequestGraph: Complex parent hierarchy", (t) => { + const graph = new ResourceRequestGraph(); + + // Level 0: Root with 2 requests + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const node1 = graph.addRequestSet(set1); + + // Level 1: Add one request + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2); + + // Level 2: Add another request + const set3 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js"), + new Request("path", "d.js") + ]; + const node3 = graph.addRequestSet(set3); + + // Verify hierarchy + const node3Data = graph.getNode(node3); + t.is(node3Data.parent, node2); + + const node2Data = graph.getNode(node2); + t.is(node2Data.parent, node1); + + const stats = graph.getStats(); + t.is(stats.maxDepth, 2); +}); + +test("ResourceRequestGraph: findBestParent chooses optimal parent", (t) => { + const graph = new ResourceRequestGraph(); + + // Create two potential parents + const set1 = [ + new Request("path", "x.js"), + new Request("path", "y.js") + ]; + graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "x.js"), + new Request("path", "y.js"), + new Request("path", "z.js"), + ]; + const node2 = graph.addRequestSet(set2); + + // New set overlaps more with set2 + const set3 = [ + new Request("path", "x.js"), + new Request("path", "y.js"), + new Request("path", "z.js"), + new Request("path", "w.js") + ]; + const node3 = graph.addRequestSet(set3); + + const node3Data = graph.getNode(node3); + // Should choose node2 as parent (only needs to add 1 request vs 5) + t.is(node3Data.parent, node2); + t.is(node3Data.addedRequests.size, 1); +}); + +test("ResourceRequestGraph: Empty request set", (t) => { + const graph = new ResourceRequestGraph(); + + const nodeId = graph.addRequestSet([]); + const node = graph.getNode(nodeId); + + t.is(node.addedRequests.size, 0); + t.is(node.parent, null); +}); + +test("ResourceRequestGraph: Caching works correctly", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + graph.addRequestSet(set1); + + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2); + + const node2Data = graph.getNode(node2); + + // First call should compute and cache + const materialized1 = node2Data.getMaterializedSet(graph); + t.is(materialized1.size, 3); + + // Second call should use cache (same result) + const materialized2 = node2Data.getMaterializedSet(graph); + t.is(materialized2.size, 3); + t.deepEqual(Array.from(materialized1).sort(), Array.from(materialized2).sort()); +}); + +test("ResourceRequestGraph: Integration", (t) => { + // Create graph + const graph = new ResourceRequestGraph(); + + // Add first request set + const set1 = [ + new Request("path", "a.js"), + new Request("path", "b.js") + ]; + const node1 = graph.addRequestSet(set1, {result: "xyz-1"}); + t.is(node1, 1); + + // Add second request set (superset of first) + const set2 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + const node2 = graph.addRequestSet(set2, {result: "xyz-2"}); + t.is(node2, 2); + + // Verify parent relationship + const node2Data = graph.getNode(node2); + t.is(node2Data.parent, node1); + t.deepEqual(Array.from(node2Data.addedRequests), ["path:c.js"]); + + // Find best match for a query + const query = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + ]; + const match = graph.findExactMatch(query); + t.is(match, node1); + + // Get metadata + const metadata = graph.getMetadata(match); + t.deepEqual(metadata, {result: "xyz-1"}); + + // Get statistics + const stats = graph.getStats(); + t.is(stats.nodeCount, 2); + t.truthy(stats.averageRequestsPerNode); +}); + +// Traversal Iterator Tests +test("ResourceRequestGraph: traverseByDepth iterates in breadth-first order", (t) => { + const graph = new ResourceRequestGraph(); + + // Create a tree structure: + // 1 (depth 0) + // / \ + // 2 3 (depth 1) + // / + // 4 (depth 2) + + const set1 = [new Request("path", "a.js")]; + const node1 = graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + const set3 = [new Request("path", "a.js"), new Request("path", "c.js")]; + const node3 = graph.addRequestSet(set3); + + const set4 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "d.js") + ]; + const node4 = graph.addRequestSet(set4); + + // Collect traversal results + const traversal = []; + for (const {nodeId, depth} of graph.traverseByDepth()) { + traversal.push({nodeId, depth}); + } + + // Verify: depth 0 node comes first, then depth 1 nodes, then depth 2 + t.is(traversal.length, 4); + t.is(traversal[0].nodeId, node1); + t.is(traversal[0].depth, 0); + + // Nodes 2 and 3 should both be at depth 1 (order may vary) + t.is(traversal[1].depth, 1); + t.is(traversal[2].depth, 1); + t.true([node2, node3].includes(traversal[1].nodeId)); + t.true([node2, node3].includes(traversal[2].nodeId)); + + // Node 4 should be at depth 2 + t.is(traversal[3].nodeId, node4); + t.is(traversal[3].depth, 2); +}); + +test("ResourceRequestGraph: traverseByDepth yields correct node information", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const node1 = graph.addRequestSet(set1, {meta: "root"}); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2, {meta: "child"}); + + const results = Array.from(graph.traverseByDepth()); + + t.is(results.length, 2); + + // First node + t.is(results[0].nodeId, node1); + t.truthy(results[0].node); + t.is(results[0].depth, 0); + t.is(results[0].parentId, null); + t.deepEqual(results[0].node.metadata, {meta: "root"}); + + // Second node + t.is(results[1].nodeId, node2); + t.is(results[1].depth, 1); + t.is(results[1].parentId, node1); + t.deepEqual(results[1].node.metadata, {meta: "child"}); +}); + +test("ResourceRequestGraph: traverseByDepth handles empty graph", (t) => { + const graph = new ResourceRequestGraph(); + const results = Array.from(graph.traverseByDepth()); + t.is(results.length, 0); +}); + +test("ResourceRequestGraph: traverseByDepth handles multiple root nodes", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + // Create a disconnected node by manipulating internal structure + // Add it without using addRequestSet to avoid automatic parent assignment + const set2 = [new Request("path", "x.js")]; + const node2 = graph.nextId++; + const requestSetNode = { + id: node2, + parent: null, + addedRequests: new Set(set2.map((r) => r.toKey())), + metadata: null, + _fullSetCache: null, + _cacheValid: false, + getMaterializedSet: function(g) { + return new Set(this.addedRequests); + }, + getMaterializedRequests: function(g) { + return Array.from(this.addedRequests).map((key) => Request.fromKey(key)); + }, + getAddedRequests: function() { + return Array.from(this.addedRequests).map((key) => Request.fromKey(key)); + }, + invalidateCache: function() { + this._cacheValid = false; + this._fullSetCache = null; + } + }; + graph.nodes.set(node2, requestSetNode); + + const results = Array.from(graph.traverseByDepth()); + + // Both roots should be at depth 0 + t.is(results.length, 2); + t.is(results[0].depth, 0); + t.is(results[1].depth, 0); + t.is(results[0].parentId, null); + t.is(results[1].parentId, null); +}); + +test("ResourceRequestGraph: traverseByDepth allows early termination", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + const set3 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "c.js") + ]; + graph.addRequestSet(set3); + + // Stop after finding node2 + let count = 0; + for (const {nodeId} of graph.traverseByDepth()) { + count++; + if (nodeId === node2) { + break; + } + } + + // Should have visited 2 nodes, not all 3 + t.is(count, 2); +}); + +test("ResourceRequestGraph: traverseByDepth allows checking deltas", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + graph.addRequestSet(set2); + + const deltas = []; + for (const {node} of graph.traverseByDepth()) { + const delta = node.getAddedRequests(); + deltas.push(delta.map((r) => r.toKey())); + } + + // First node has 1 request, second node adds 1 request + t.deepEqual(deltas, [["path:a.js"], ["path:b.js"]]); +}); + +test("ResourceRequestGraph: traverseSubtree traverses only specified subtree", (t) => { + const graph = new ResourceRequestGraph(); + + // Create structure: + // 1 + // / \ + // 2 3 + // / + // 4 + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + const set3 = [new Request("path", "a.js"), new Request("path", "c.js")]; + graph.addRequestSet(set3); + + const set4 = [ + new Request("path", "a.js"), + new Request("path", "b.js"), + new Request("path", "d.js") + ]; + const node4 = graph.addRequestSet(set4); + + // Traverse only subtree starting from node2 + const results = Array.from(graph.traverseSubtree(node2)); + + // Should only visit node2 and node4 (not node1 or node3) + t.is(results.length, 2); + t.is(results[0].nodeId, node2); + t.is(results[0].depth, 0); // Relative depth from start + t.is(results[1].nodeId, node4); + t.is(results[1].depth, 1); +}); + +test("ResourceRequestGraph: traverseSubtree with root node traverses entire tree", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const node1 = graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + graph.addRequestSet(set2); + + const results = Array.from(graph.traverseSubtree(node1)); + + // Should visit all nodes + t.is(results.length, 2); +}); + +test("ResourceRequestGraph: traverseSubtree handles non-existent node", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const results = Array.from(graph.traverseSubtree(999)); + t.is(results.length, 0); +}); + +test("ResourceRequestGraph: traverseSubtree handles leaf node", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + // Traverse from leaf node + const results = Array.from(graph.traverseSubtree(node2)); + + // Should only visit the leaf node itself + t.is(results.length, 1); + t.is(results[0].nodeId, node2); + t.is(results[0].depth, 0); +}); + +test("ResourceRequestGraph: getChildren returns child node IDs", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + const node1 = graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + const set3 = [new Request("path", "a.js"), new Request("path", "c.js")]; + const node3 = graph.addRequestSet(set3); + + const children = graph.getChildren(node1); + + t.is(children.length, 2); + t.true(children.includes(node2)); + t.true(children.includes(node3)); +}); + +test("ResourceRequestGraph: getChildren returns empty array for leaf nodes", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const set2 = [new Request("path", "a.js"), new Request("path", "b.js")]; + const node2 = graph.addRequestSet(set2); + + const children = graph.getChildren(node2); + t.is(children.length, 0); +}); + +test("ResourceRequestGraph: getChildren returns empty array for non-existent node", (t) => { + const graph = new ResourceRequestGraph(); + + const set1 = [new Request("path", "a.js")]; + graph.addRequestSet(set1); + + const children = graph.getChildren(999); + t.is(children.length, 0); +}); + +test("ResourceRequestGraph: Efficient traversal use case", (t) => { + const graph = new ResourceRequestGraph(); + + // Simulate real usage: build a graph of resource requests + const set1 = [new Request("path", "core.js"), new Request("path", "utils.js")]; + const node1 = graph.addRequestSet(set1, {cached: true}); + + const set2 = [ + new Request("path", "core.js"), + new Request("path", "utils.js"), + new Request("path", "components.js") + ]; + const node2 = graph.addRequestSet(set2, {cached: false}); + + // Traverse and collect information + const visited = []; + for (const {nodeId, node, depth} of graph.traverseByDepth()) { + visited.push({ + nodeId, + depth, + deltaSize: node.addedRequests.size, + cached: node.metadata?.cached + }); + } + + t.is(visited.length, 2); + + // Parent processed first + t.is(visited[0].nodeId, node1); + t.is(visited[0].depth, 0); + t.is(visited[0].deltaSize, 2); + t.true(visited[0].cached); + + // Child processed second with delta + t.is(visited[1].nodeId, node2); + t.is(visited[1].depth, 1); + t.is(visited[1].deltaSize, 1); // Only added "components.js" + t.false(visited[1].cached); +}); diff --git a/packages/project/test/lib/build/cache/ResourceRequestManager.js b/packages/project/test/lib/build/cache/ResourceRequestManager.js new file mode 100644 index 00000000000..161b292ac2e --- /dev/null +++ b/packages/project/test/lib/build/cache/ResourceRequestManager.js @@ -0,0 +1,697 @@ +import test from "ava"; +import sinon from "sinon"; +import ResourceRequestManager from "../../../../lib/build/cache/ResourceRequestManager.js"; +import ResourceRequestGraph from "../../../../lib/build/cache/ResourceRequestGraph.js"; + +// Helper to create mock Resource instances +function createMockResource(path, integrity = "test-hash", lastModified = 1000, size = 100, inode = 1) { + return { + getOriginalPath: () => path, + getPath: () => path, + getIntegrity: async () => integrity, + getLastModified: () => lastModified, + getSize: async () => size, + getInode: () => inode, + getTags: () => null, + getBuffer: async () => Buffer.from("test content"), + getStream: () => null + }; +} + +// Helper to create mock Reader (project or dependency) +function createMockReader(resources = new Map()) { + return { + byPath: sinon.stub().callsFake(async (path) => { + return resources.get(path) || null; + }), + byGlob: sinon.stub().callsFake(async (patterns) => { + const patternArray = Array.isArray(patterns) ? patterns : [patterns]; + const results = []; + for (const [path, resource] of resources.entries()) { + for (const pattern of patternArray) { + // Simple pattern matching + if (pattern === "/**/*" || pattern === "**/*") { + results.push(resource); + break; + } + // Convert glob pattern to regex + const regex = new RegExp(pattern.replace(/\*/g, ".*").replace(/\?/g, ".")); + if (regex.test(path)) { + results.push(resource); + break; + } + } + } + return results; + }) + }; +} + +test.afterEach.always(() => { + sinon.restore(); +}); + +// ===== CONSTRUCTOR TESTS ===== + +test("ResourceRequestManager: Create new instance", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + + t.truthy(manager, "Manager instance created"); + t.true(manager.hasNewOrModifiedCacheEntries(), "New manager has modified entries"); +}); + +test("ResourceRequestManager: Create with request graph from cache", (t) => { + const graph = new ResourceRequestGraph(); + const manager = new ResourceRequestManager("test.project", "myTask", false, graph); + + t.truthy(manager, "Manager instance created with graph"); + t.false(manager.hasNewOrModifiedCacheEntries(), "Manager restored from cache has no new entries initially"); +}); + +test("ResourceRequestManager: Create with differential update enabled", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", true); + + t.truthy(manager, "Manager instance created with differential updates"); +}); + +test("ResourceRequestManager: Create with unusedAtLeastOnce flag", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false, null, true); + + t.truthy(manager, "Manager instance created"); + const signatures = manager.getIndexSignatures(); + t.true(signatures.includes("X"), "Signatures include 'X' for unused state"); +}); + +// ===== fromCache FACTORY METHOD TESTS ===== + +test("ResourceRequestManager: fromCache with basic data", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + // Create a manager and add some requests + const manager1 = new ResourceRequestManager("test.project", "myTask", false); + await manager1.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + // Serialize and restore + const cacheData = manager1.toCacheObject(); + t.truthy(cacheData, "Cache data created"); + + const manager2 = ResourceRequestManager.fromCache("test.project", "myTask", false, cacheData); + + t.truthy(manager2, "Manager restored from cache"); + t.false(manager2.hasNewOrModifiedCacheEntries(), "Restored manager has no new entries"); +}); + +test("ResourceRequestManager: fromCache with unusedAtLeastOnce", (t) => { + const cacheData = { + requestSetGraph: { + nodes: [], + nextId: 1 + }, + rootIndices: [], + unusedAtLeastOnce: true + }; + + const manager = ResourceRequestManager.fromCache("test.project", "myTask", false, cacheData); + + t.truthy(manager, "Manager restored"); + const signatures = manager.getIndexSignatures(); + t.true(signatures.includes("X"), "Includes 'X' signature for unused state"); +}); + +// ===== addRequests TESTS ===== + +test("ResourceRequestManager: Add path requests", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const result = await manager.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + t.truthy(result, "Result returned"); + t.truthy(result.setId, "Result has setId"); + t.truthy(result.signature, "Result has signature"); + t.is(typeof result.signature, "string", "Signature is a string"); +}); + +test("ResourceRequestManager: Add pattern requests", async (t) => { + const resources = new Map([ + ["/src/a.js", createMockResource("/src/a.js", "hash-a")], + ["/src/b.js", createMockResource("/src/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const result = await manager.addRequests({ + paths: [], + patterns: [["/src/*.js"]] + }, reader); + + t.truthy(result, "Result returned"); + t.truthy(result.signature, "Result has signature"); +}); + +test("ResourceRequestManager: Add multiple request sets", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")], + ["/c.js", createMockResource("/c.js", "hash-c")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + + // First request set + const result1 = await manager.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + // Second request set (superset) + const result2 = await manager.addRequests({ + paths: ["/a.js", "/b.js", "/c.js"], + patterns: [] + }, reader); + + t.not(result1.signature, result2.signature, "Different signatures for different request sets"); + t.true(manager.hasNewOrModifiedCacheEntries(), "Has new entries"); +}); + +test("ResourceRequestManager: Reuse existing request set", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + + // Add first request set + const result1 = await manager.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + // Add identical request set + const result2 = await manager.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + t.is(result1.setId, result2.setId, "Same setId for identical requests"); + t.is(result1.signature, result2.signature, "Same signature for identical requests"); +}); + +// ===== recordNoRequests TESTS ===== + +test("ResourceRequestManager: Record no requests", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const signature = manager.recordNoRequests(); + + t.is(signature, "X", "Returns 'X' signature"); + t.true(manager.hasNewOrModifiedCacheEntries(), "Has new entries"); +}); + +test("ResourceRequestManager: Record no requests multiple times", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const sig1 = manager.recordNoRequests(); + const sig2 = manager.recordNoRequests(); + + t.is(sig1, "X", "First call returns 'X'"); + t.is(sig2, "X", "Second call returns 'X'"); +}); + +// ===== getIndexSignatures TESTS ===== + +test("ResourceRequestManager: Get signatures from empty manager", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const signatures = manager.getIndexSignatures(); + + t.is(signatures.length, 0, "No signatures for empty manager"); +}); + +test("ResourceRequestManager: Get signatures after adding requests", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + const signatures = manager.getIndexSignatures(); + + t.is(signatures.length, 1, "One signature"); + t.is(typeof signatures[0], "string", "Signature is a string"); +}); + +test("ResourceRequestManager: Get signatures includes 'X' when unused", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + manager.recordNoRequests(); + + const signatures = manager.getIndexSignatures(); + + t.true(signatures.includes("X"), "Includes 'X' signature"); +}); + +// ===== hasNewOrModifiedCacheEntries TESTS ===== + +test("ResourceRequestManager: New manager has modified entries", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + + t.true(manager.hasNewOrModifiedCacheEntries(), "New manager has modified entries"); +}); + +test("ResourceRequestManager: Restored manager has no modified entries initially", (t) => { + const graph = new ResourceRequestGraph(); + const manager = new ResourceRequestManager("test.project", "myTask", false, graph); + + t.false(manager.hasNewOrModifiedCacheEntries(), "Restored manager has no modified entries"); +}); + +test("ResourceRequestManager: Adding requests marks as modified", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + const graph = new ResourceRequestGraph(); + const manager = new ResourceRequestManager("test.project", "myTask", false, graph); + + t.false(manager.hasNewOrModifiedCacheEntries(), "Initially no modified entries"); + + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + t.true(manager.hasNewOrModifiedCacheEntries(), "Has modified entries after adding requests"); +}); + +// ===== toCacheObject TESTS ===== + +test("ResourceRequestManager: Serialize to cache object", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + const cacheObj = manager.toCacheObject(); + + t.truthy(cacheObj, "Cache object created"); + t.truthy(cacheObj.requestSetGraph, "Has requestSetGraph"); + t.truthy(cacheObj.rootIndices, "Has rootIndices"); + t.true(Array.isArray(cacheObj.rootIndices), "rootIndices is an array"); +}); + +test("ResourceRequestManager: Serialize returns undefined when no changes", (t) => { + const graph = new ResourceRequestGraph(); + const manager = new ResourceRequestManager("test.project", "myTask", false, graph); + + const cacheObj = manager.toCacheObject(); + + t.is(cacheObj, undefined, "Returns undefined when no changes"); +}); + +test("ResourceRequestManager: Serialize includes unusedAtLeastOnce", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", false); + manager.recordNoRequests(); + + const cacheObj = manager.toCacheObject(); + + t.truthy(cacheObj, "Cache object created"); + t.true(cacheObj.unusedAtLeastOnce, "Includes unusedAtLeastOnce flag"); +}); + +test("ResourceRequestManager: Serialize with differential updates", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", true); + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + const cacheObj = manager.toCacheObject(); + + t.truthy(cacheObj, "Cache object created"); + t.truthy(cacheObj.deltaIndices, "Has deltaIndices"); + t.true(Array.isArray(cacheObj.deltaIndices), "deltaIndices is an array"); +}); + +// ===== getDeltas TESTS ===== + +test("ResourceRequestManager: Get deltas returns empty map initially", (t) => { + const manager = new ResourceRequestManager("test.project", "myTask", true); + + const deltas = manager.getDeltas(); + + t.true(deltas instanceof Map, "Returns a Map"); + t.is(deltas.size, 0, "Empty initially"); +}); + +test("ResourceRequestManager: Get deltas after updates", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", true); + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + // Update resource + resources.set("/a.js", createMockResource("/a.js", "hash-a-new")); + + // Note: In a real scenario, updateIndices would be called here + // For this test, we're just checking the method exists and returns a Map + const deltas = manager.getDeltas(); + + t.true(deltas instanceof Map, "Returns a Map"); +}); + +// ===== updateIndices TESTS ===== + +test("ResourceRequestManager: updateIndices with no requests", async (t) => { + const reader = createMockReader(new Map()); + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const hasChanges = await manager.updateIndices(reader, []); + + t.false(hasChanges, "No changes when no requests recorded"); +}); + +test("ResourceRequestManager: updateIndices with no changed paths", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + const hasChanges = await manager.updateIndices(reader, []); + + t.false(hasChanges, "No changes when paths don't match"); +}); + +test("ResourceRequestManager: updateIndices with matching changed path", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + // Update the resource + resources.set("/a.js", createMockResource("/a.js", "hash-a-new")); + + const hasChanges = await manager.updateIndices(reader, ["/a.js"]); + + t.true(hasChanges, "Detects changes for matching path"); +}); + +test("ResourceRequestManager: updateIndices with pattern matches", async (t) => { + const resources = new Map([ + ["/src/a.js", createMockResource("/src/a.js", "hash-a")], + ["/src/b.js", createMockResource("/src/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({ + paths: [], + patterns: [["/src/*.js"]] + }, reader); + + // Update one resource + resources.set("/src/a.js", createMockResource("/src/a.js", "hash-a-new")); + + const hasChanges = await manager.updateIndices(reader, ["/src/a.js"]); + + t.true(hasChanges, "Detects changes for pattern-matched resources"); +}); + +test("ResourceRequestManager: updateIndices with removed resource", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({paths: ["/a.js", "/b.js"], patterns: []}, reader); + + // Remove a resource + resources.delete("/b.js"); + + const hasChanges = await manager.updateIndices(reader, ["/b.js"]); + + t.true(hasChanges, "Detects removal of resource"); +}); + +// ===== refreshIndices TESTS ===== + +/* eslint-disable-next-line */ +test.skip("ResourceRequestManager: refreshIndices with no requests", async (t) => { + const reader = createMockReader(new Map()); + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const hasChanges = await manager.refreshIndices(reader); + + t.false(hasChanges, "No changes when no requests recorded"); +}); + +test("ResourceRequestManager: refreshIndices after adding requests", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + await manager.addRequests({paths: ["/a.js", "/b.js"], patterns: []}, reader); + + // Update resources + resources.set("/a.js", createMockResource("/a.js", "hash-a-new")); + + const result = await manager.refreshIndices(reader); + + // refreshIndices doesn't return a value (undefined) when changes are made + // It only returns false when there are no requests + t.is(result, undefined, "refreshIndices returns undefined when it processes changes"); + t.pass("refreshIndices completed"); +}); + +// ===== INTEGRATION TESTS ===== + +test("ResourceRequestManager: Complete workflow", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + // 1. Create manager + const manager = new ResourceRequestManager("test.project", "myTask", false); + + // 2. Add requests + const result = await manager.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + t.truthy(result.signature, "Got signature from addRequests"); + + // 3. Get signatures + const signatures = manager.getIndexSignatures(); + t.is(signatures.length, 1, "One signature recorded"); + t.is(signatures[0], result.signature, "Signature matches"); + + // 4. Serialize + const cacheObj = manager.toCacheObject(); + t.truthy(cacheObj, "Can serialize to cache object"); + + // 5. Restore from cache + const manager2 = ResourceRequestManager.fromCache("test.project", "myTask", false, cacheObj); + t.truthy(manager2, "Can restore from cache"); + + const signatures2 = manager2.getIndexSignatures(); + t.deepEqual(signatures2, signatures, "Restored manager has same signatures"); +}); + +test("ResourceRequestManager: Differential update workflow", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")] + ]); + const reader = createMockReader(resources); + + // Create with differential updates enabled + const manager = new ResourceRequestManager("test.project", "myTask", true); + + // Add requests + await manager.addRequests({paths: ["/a.js"], patterns: []}, reader); + + // Update resource + resources.set("/a.js", createMockResource("/a.js", "hash-a-new")); + + // Update indices + const hasChanges = await manager.updateIndices(reader, ["/a.js"]); + t.true(hasChanges, "Detected changes"); + + // Get deltas + const deltas = manager.getDeltas(); + t.true(deltas instanceof Map, "Has deltas"); + + // Serialize + const cacheObj = manager.toCacheObject(); + t.truthy(cacheObj, "Can serialize"); + t.truthy(cacheObj.deltaIndices, "Has delta indices"); +}); + +test("ResourceRequestManager: Mixed path and pattern requests", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/src/b.js", createMockResource("/src/b.js", "hash-b")], + ["/src/c.js", createMockResource("/src/c.js", "hash-c")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const result = await manager.addRequests({ + paths: ["/a.js"], + patterns: [["/src/*.js"]] + }, reader); + + t.truthy(result.signature, "Got signature for mixed requests"); + + const signatures = manager.getIndexSignatures(); + t.is(signatures.length, 1, "One signature"); +}); + +test("ResourceRequestManager: Hierarchical request sets", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")], + ["/c.js", createMockResource("/c.js", "hash-c")] + ]); + const reader = createMockReader(resources); + + const manager = new ResourceRequestManager("test.project", "myTask", false); + + // First request set + const result1 = await manager.addRequests({ + paths: ["/a.js"], + patterns: [] + }, reader); + + // Second request set (superset) + const result2 = await manager.addRequests({ + paths: ["/a.js", "/b.js"], + patterns: [] + }, reader); + + // Third request set (superset) + const result3 = await manager.addRequests({ + paths: ["/a.js", "/b.js", "/c.js"], + patterns: [] + }, reader); + + const signatures = manager.getIndexSignatures(); + t.is(signatures.length, 3, "Three different signatures"); + t.not(result1.signature, result2.signature, "Different signatures"); + t.not(result2.signature, result3.signature, "Different signatures"); +}); + +test("ResourceRequestManager: Empty request sets", async (t) => { + const reader = createMockReader(new Map()); + const manager = new ResourceRequestManager("test.project", "myTask", false); + + const result = await manager.addRequests({ + paths: [], + patterns: [] + }, reader); + + t.truthy(result, "Can add empty request set"); + t.truthy(result.signature, "Has signature even when empty"); +}); + +test("ResourceRequestManager: Serialization round-trip with multiple request sets", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + // Create manager and add multiple request sets (keep it simpler - two levels) + const manager1 = new ResourceRequestManager("test.project", "myTask", false); + await manager1.addRequests({paths: ["/a.js"], patterns: []}, reader); + await manager1.addRequests({paths: ["/a.js", "/b.js"], patterns: []}, reader); + + const signatures1 = manager1.getIndexSignatures(); + + // Serialize and restore + const cacheObj = manager1.toCacheObject(); + const manager2 = ResourceRequestManager.fromCache("test.project", "myTask", false, cacheObj); + + const signatures2 = manager2.getIndexSignatures(); + + t.deepEqual(signatures2, signatures1, "Signatures preserved through serialization"); + t.false(manager2.hasNewOrModifiedCacheEntries(), "Restored manager has no new entries"); +}); + +test("ResourceRequestManager: Serialization round-trip with multiple request sets and following update", async (t) => { + const resources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], + ["/b.js", createMockResource("/b.js", "hash-b")] + ]); + const reader = createMockReader(resources); + + // Create manager and add multiple request sets (keep it simpler - two levels) + const manager1 = new ResourceRequestManager("test.project", "myTask", false); + await manager1.addRequests({paths: ["/a.js"], patterns: []}, reader); + await manager1.addRequests({paths: ["/a.js", "/b.js"], patterns: []}, reader); + + const signatures1 = manager1.getIndexSignatures(); + + // Serialize and restore + const cacheObj = manager1.toCacheObject(); + const manager2 = ResourceRequestManager.fromCache("test.project", "myTask", false, cacheObj); + + + const changedResources = new Map([ + ["/a.js", createMockResource("/a.js", "hash-a")], // Identical to first + ["/b.js", createMockResource("/b.js", "hash-y")] + ]); + const changedReader = createMockReader(changedResources); + + const hasChanges = await manager2.updateIndices(changedReader, ["/a.js", "/b.js"]); + + t.true(hasChanges, "Detected changes after update"); + + const signatures2 = manager2.getIndexSignatures(); + + t.is(signatures2[0], signatures1[0], "Unchanged signature of first request set"); + t.not(signatures2[1], signatures1[1], "Changed signature of second request set"); + t.true(manager2.hasNewOrModifiedCacheEntries(), "Restored manager has new entries"); +}); + diff --git a/packages/project/test/lib/build/cache/index/HashTree.js b/packages/project/test/lib/build/cache/index/HashTree.js new file mode 100644 index 00000000000..1ae9d93f32b --- /dev/null +++ b/packages/project/test/lib/build/cache/index/HashTree.js @@ -0,0 +1,636 @@ +import test from "ava"; +import sinon from "sinon"; +import HashTree from "../../../../../lib/build/cache/index/HashTree.js"; + +// Helper to create mock Resource instances +function createMockResource(path, integrity, lastModified, size, inode) { + const resource = { + tags: null, + getOriginalPath: () => path, + getIntegrity: async () => integrity, + getLastModified: () => lastModified, + getSize: async () => size, + getInode: () => inode, + getTags() { + return this.tags; + } + }; + return resource; +} + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("Create HashTree", (t) => { + const mt = new HashTree(); + t.truthy(mt, "HashTree instance created"); +}); + +test("Two instances with same resources produce same root hash", (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1"}, + {path: "file2.js", integrity: "hash2"}, + {path: "dir/file3.js", integrity: "hash3"} + ]; + + const tree1 = new HashTree(resources); + const tree2 = new HashTree(resources); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees with identical resources should have identical root hashes"); +}); + +test("Order of resource insertion doesn't affect root hash", (t) => { + const resources1 = [ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"}, + {path: "c.js", integrity: "hash-c"} + ]; + + const resources2 = [ + {path: "c.js", integrity: "hash-c"}, + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ]; + + const tree1 = new HashTree(resources1); + const tree2 = new HashTree(resources2); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees should produce same hash regardless of insertion order"); +}); + +test("Updating resources in two trees produces same root hash", async (t) => { + const initialResources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100}, + {path: "file2.js", integrity: "hash2", lastModified: 2000, size: 200}, + {path: "dir/file3.js", integrity: "hash3", lastModified: 3000, size: 300} + ]; + + const tree1 = new HashTree(initialResources); + const tree2 = new HashTree(initialResources); + const indexTimestamp = tree1.getIndexTimestamp(); + + // Update same resource in both trees + const resource = createMockResource("file1.js", "new-hash1", indexTimestamp + 1, 101, 1); + await tree1.upsertResources([resource]); + await tree2.upsertResources([resource]); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees should have same root hash after identical updates"); +}); + +test("Multiple updates in same order produce same root hash", async (t) => { + const initialResources = [ + {path: "a.js", integrity: "hash-a", lastModified: 1000, size: 100}, + {path: "b.js", integrity: "hash-b", lastModified: 2000, size: 200}, + {path: "c.js", integrity: "hash-c", lastModified: 3000, size: 300}, + {path: "dir/d.js", integrity: "hash-d", lastModified: 4000, size: 400} + ]; + + const tree1 = new HashTree(initialResources); + const tree2 = new HashTree(initialResources); + const indexTimestamp = tree1.getIndexTimestamp(); + + // Update multiple resources in same order + await tree1.upsertResources([createMockResource("a.js", "new-hash-a", indexTimestamp + 1, 101, 1)]); + await tree1.upsertResources([createMockResource("dir/d.js", "new-hash-d", indexTimestamp + 1, 401, 4)]); + await tree1.upsertResources([createMockResource("b.js", "new-hash-b", indexTimestamp + 1, 201, 2)]); + + await tree2.upsertResources([createMockResource("a.js", "new-hash-a", indexTimestamp + 1, 101, 1)]); + await tree2.upsertResources([createMockResource("dir/d.js", "new-hash-d", indexTimestamp + 1, 401, 4)]); + await tree2.upsertResources([createMockResource("b.js", "new-hash-b", indexTimestamp + 1, 201, 2)]); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees should have same root hash after same sequence of updates"); +}); + +test("Multiple updates in different order produce same root hash", async (t) => { + const initialResources = [ + {path: "a.js", integrity: "hash-a", lastModified: 1000, size: 100}, + {path: "b.js", integrity: "hash-b", lastModified: 2000, size: 200}, + {path: "c.js", integrity: "hash-c", lastModified: 3000, size: 300} + ]; + + const tree1 = new HashTree(initialResources); + const tree2 = new HashTree(initialResources); + const indexTimestamp = tree1.getIndexTimestamp(); + + // Update in different orders + await tree1.upsertResources([createMockResource("a.js", "new-hash-a", indexTimestamp + 1, 101, 1)]); + await tree1.upsertResources([createMockResource("b.js", "new-hash-b", indexTimestamp + 1, 201, 2)]); + await tree1.upsertResources([createMockResource("c.js", "new-hash-c", indexTimestamp + 1, 301, 3)]); + + await tree2.upsertResources([createMockResource("c.js", "new-hash-c", indexTimestamp + 1, 301, 3)]); + await tree2.upsertResources([createMockResource("a.js", "new-hash-a", indexTimestamp + 1, 101, 1)]); + await tree2.upsertResources([createMockResource("b.js", "new-hash-b", indexTimestamp + 1, 201, 2)]); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees should have same root hash regardless of update order"); +}); + +test("Batch updates produce same hash as individual updates", async (t) => { + const initialResources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100}, + {path: "file2.js", integrity: "hash2", lastModified: 2000, size: 200}, + {path: "file3.js", integrity: "hash3", lastModified: 3000, size: 300} + ]; + + const tree1 = new HashTree(initialResources); + const tree2 = new HashTree(initialResources); + const indexTimestamp = tree1.getIndexTimestamp(); + + // Individual updates + await tree1.upsertResources([createMockResource("file1.js", "new-hash1", indexTimestamp + 1, 101, 1)]); + await tree1.upsertResources([createMockResource("file2.js", "new-hash2", indexTimestamp + 1, 201, 2)]); + + // Batch update + const resources = [ + createMockResource("file1.js", "new-hash1", 1001, 101, 1), + createMockResource("file2.js", "new-hash2", 2001, 201, 2) + ]; + await tree2.upsertResources(resources); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Batch updates should produce same hash as individual updates"); +}); + +test("Updating resource changes root hash", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100}, + {path: "file2.js", integrity: "hash2", lastModified: 2000, size: 200} + ]; + + const tree = new HashTree(resources); + const originalHash = tree.getRootHash(); + const indexTimestamp = tree.getIndexTimestamp(); + + await tree.upsertResources([createMockResource("file1.js", "new-hash1", indexTimestamp + 1, 101, 1)]); + const newHash = tree.getRootHash(); + + t.not(originalHash, newHash, + "Root hash should change after resource update"); +}); + +test("Updating resource back to original value restores original hash", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100}, + {path: "file2.js", integrity: "hash2", lastModified: 2000, size: 200} + ]; + + const tree = new HashTree(resources); + const originalHash = tree.getRootHash(); + const indexTimestamp = tree.getIndexTimestamp(); + + // Update and then revert + await tree.upsertResources([createMockResource("file1.js", "new-hash1", indexTimestamp + 1, 101, 1)]); + await tree.upsertResources([createMockResource("file1.js", "hash1", 1000, 100, 1)]); + + t.is(tree.getRootHash(), originalHash, + "Root hash should be restored when resource is reverted to original value"); +}); + +test("updateResource returns changed resource path", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100} + ]; + + const tree = new HashTree(resources); + const indexTimestamp = tree.getIndexTimestamp(); + const {updated} = await tree.upsertResources([ + createMockResource("file1.js", "new-hash1", indexTimestamp + 1, 101, 1) + ]); + + t.deepEqual(updated, ["file1.js"], "Should return path of changed resource"); +}); + +test("updateResource returns empty array when integrity unchanged", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100} + ]; + + const tree = new HashTree(resources); + const {updated} = await tree.upsertResources([createMockResource("file1.js", "hash1", 1000, 100, 1)]); + + t.deepEqual(updated, [], "Should return empty array when integrity unchanged"); +}); + +test("updateResource does not change hash when integrity unchanged", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100} + ]; + + const tree = new HashTree(resources); + const originalHash = tree.getRootHash(); + await tree.upsertResources([createMockResource("file1.js", "hash1", 1000, 100, 1)]); + + t.is(tree.getRootHash(), originalHash, "Hash should not change when integrity unchanged"); +}); + +test("upsertResources returns changed resource paths", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100}, + {path: "file2.js", integrity: "hash2", lastModified: 2000, size: 200}, + {path: "file3.js", integrity: "hash3", lastModified: 3000, size: 300} + ]; + + const tree = new HashTree(resources); + const indexTimestamp = tree.getIndexTimestamp(); + + const resourceUpdates = [ + createMockResource("file1.js", "new-hash1", indexTimestamp + 1, 101, 1), // Changed + createMockResource("file2.js", "hash2", 2000, 200, 2), // unchanged + createMockResource("file3.js", "new-hash3", indexTimestamp + 1, 301, 3) // Changed + ]; + const {updated} = await tree.upsertResources(resourceUpdates); + + t.deepEqual(updated, ["file1.js", "file3.js"], "Should return only updated paths"); +}); + +test("upsertResources returns empty array when no changes", async (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100}, + {path: "file2.js", integrity: "hash2", lastModified: 2000, size: 200} + ]; + + const tree = new HashTree(resources); + const resourceUpdates = [ + createMockResource("file1.js", "hash1", 1000, 100, 1), + createMockResource("file2.js", "hash2", 2000, 200, 2) + ]; + const {updated} = await tree.upsertResources(resourceUpdates); + + t.deepEqual(updated, [], "Should return empty array when no changes"); +}); + +test("Different nested structures with same resources produce different hashes", (t) => { + const resources1 = [ + {path: "a/b/file.js", integrity: "hash1"} + ]; + + const resources2 = [ + {path: "a/file.js", integrity: "hash1"} + ]; + + const tree1 = new HashTree(resources1); + const tree2 = new HashTree(resources2); + + t.not(tree1.getRootHash(), tree2.getRootHash(), + "Different directory structures should produce different hashes"); +}); + +test("Updating unrelated resource doesn't affect consistency", async (t) => { + const initialResources = [ + {path: "file1.js", integrity: "hash1"}, + {path: "file2.js", integrity: "hash2"}, + {path: "dir/file3.js", integrity: "hash3"} + ]; + + const tree1 = new HashTree(initialResources); + const tree2 = new HashTree(initialResources); + + // Update different resources + await tree1.upsertResources([createMockResource("file1.js", "new-hash1", Date.now(), 1024, 789)]); + await tree2.upsertResources([createMockResource("file1.js", "new-hash1", Date.now(), 1024, 789)]); + + // Update an unrelated resource in both + await tree1.upsertResources([createMockResource("dir/file3.js", "new-hash3", Date.now(), 2048, 790)]); + await tree2.upsertResources([createMockResource("dir/file3.js", "new-hash3", Date.now(), 2048, 790)]); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees should remain consistent after updating multiple resources"); +}); + +test("getResourcePaths returns all resource paths in sorted order", (t) => { + const resources = [ + {path: "z.js", integrity: "hash-z"}, + {path: "a.js", integrity: "hash-a"}, + {path: "dir/b.js", integrity: "hash-b"}, + {path: "dir/nested/c.js", integrity: "hash-c"} + ]; + + const tree = new HashTree(resources); + const paths = tree.getResourcePaths(); + + t.deepEqual(paths, [ + "/a.js", + "/dir/b.js", + "/dir/nested/c.js", + "/z.js" + ], "Resource paths should be sorted alphabetically"); +}); + +test("getResourcePaths returns empty array for empty tree", (t) => { + const tree = new HashTree(); + const paths = tree.getResourcePaths(); + + t.deepEqual(paths, [], "Empty tree should return empty array"); +}); + +// ============================================================================ +// upsertResources Tests +// ============================================================================ + +test("upsertResources - insert new resources", async (t) => { + const tree = new HashTree([{path: "a.js", integrity: "hash-a"}]); + const originalHash = tree.getRootHash(); + + const result = await tree.upsertResources([ + createMockResource("b.js", "hash-b", Date.now(), 1024, 1), + createMockResource("c.js", "hash-c", Date.now(), 2048, 2) + ]); + + t.deepEqual(result.added, ["b.js", "c.js"], "Should report added resources"); + t.deepEqual(result.updated, [], "Should have no updates"); + t.deepEqual(result.unchanged, [], "Should have no unchanged"); + + t.truthy(tree.hasPath("b.js"), "Tree should have b.js"); + t.truthy(tree.hasPath("c.js"), "Tree should have c.js"); + t.not(tree.getRootHash(), originalHash, "Root hash should change"); +}); + +test("upsertResources - update existing resources", async (t) => { + const tree = new HashTree([ + {path: "a.js", integrity: "hash-a", lastModified: 1000, size: 100}, + {path: "b.js", integrity: "hash-b", lastModified: 2000, size: 200} + ]); + const originalHash = tree.getRootHash(); + const indexTimestamp = tree.getIndexTimestamp(); + + const result = await tree.upsertResources([ + createMockResource("a.js", "new-hash-a", indexTimestamp + 1, 101, 1), + createMockResource("b.js", "new-hash-b", indexTimestamp + 1, 201, 2) + ]); + + t.deepEqual(result.added, [], "Should have no additions"); + t.deepEqual(result.updated, ["a.js", "b.js"], "Should report updated resources"); + t.deepEqual(result.unchanged, [], "Should have no unchanged"); + + t.is(tree.getResourceByPath("a.js").integrity, "new-hash-a"); + t.is(tree.getResourceByPath("b.js").integrity, "new-hash-b"); + t.not(tree.getRootHash(), originalHash, "Root hash should change"); +}); + +test("upsertResources - mixed insert, update, and unchanged", async (t) => { + const timestamp = Date.now(); + const tree = new HashTree([ + {path: "a.js", integrity: "hash-a", lastModified: timestamp, size: 100, inode: 1} + ]); + const originalHash = tree.getRootHash(); + + const result = await tree.upsertResources([ + createMockResource("a.js", "hash-a", timestamp, 100, 1), // unchanged + createMockResource("b.js", "hash-b", timestamp, 200, 2), // new + createMockResource("c.js", "hash-c", timestamp, 300, 3) // new + ]); + + t.deepEqual(result.unchanged, ["a.js"], "Should report unchanged resource"); + t.deepEqual(result.added, ["b.js", "c.js"], "Should report added resources"); + t.deepEqual(result.updated, [], "Should have no updates"); + + t.not(tree.getRootHash(), originalHash, "Root hash should change (new resources added)"); +}); + +// ============================================================================ +// removeResources Tests +// ============================================================================ + +test("removeResources - remove existing resources", async (t) => { + const tree = new HashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"}, + {path: "c.js", integrity: "hash-c"} + ]); + const originalHash = tree.getRootHash(); + + const result = await tree.removeResources(["b.js", "c.js"]); + + t.deepEqual(result.removed, ["b.js", "c.js"], "Should report removed resources"); + t.deepEqual(result.notFound, [], "Should have no not found"); + + t.truthy(tree.hasPath("a.js"), "Tree should still have a.js"); + t.false(tree.hasPath("b.js"), "Tree should not have b.js"); + t.false(tree.hasPath("c.js"), "Tree should not have c.js"); + t.not(tree.getRootHash(), originalHash, "Root hash should change"); +}); + +test("removeResources - remove non-existent resources", async (t) => { + const tree = new HashTree([{path: "a.js", integrity: "hash-a"}]); + const originalHash = tree.getRootHash(); + + const result = await tree.removeResources(["b.js", "c.js"]); + + t.deepEqual(result.removed, [], "Should have no removals"); + t.deepEqual(result.notFound, ["b.js", "c.js"], "Should report not found"); + + t.is(tree.getRootHash(), originalHash, "Root hash should not change"); +}); + +test("removeResources - mixed existing and non-existent", async (t) => { + const tree = new HashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ]); + + const result = await tree.removeResources(["b.js", "c.js", "d.js"]); + + t.deepEqual(result.removed, ["b.js"], "Should report removed resources"); + t.deepEqual(result.notFound, ["c.js", "d.js"], "Should report not found"); + + t.truthy(tree.hasPath("a.js"), "Tree should still have a.js"); + t.false(tree.hasPath("b.js"), "Tree should not have b.js"); +}); + +test("removeResources - remove from nested directory", async (t) => { + const tree = new HashTree([ + {path: "dir1/dir2/a.js", integrity: "hash-a"}, + {path: "dir1/dir2/b.js", integrity: "hash-b"}, + {path: "dir1/c.js", integrity: "hash-c"} + ]); + + const result = await tree.removeResources(["dir1/dir2/a.js"]); + + t.deepEqual(result.removed, ["dir1/dir2/a.js"], "Should remove nested resource"); + t.false(tree.hasPath("dir1/dir2/a.js"), "Should not have dir1/dir2/a.js"); + t.truthy(tree.hasPath("dir1/dir2/b.js"), "Should still have dir1/dir2/b.js"); + t.truthy(tree.hasPath("dir1/c.js"), "Should still have dir1/c.js"); +}); + +test("removeResources - removing last resource in directory cleans up directory", async (t) => { + const tree = new HashTree([ + {path: "dir1/dir2/only.js", integrity: "hash-only"}, + {path: "dir1/other.js", integrity: "hash-other"} + ]); + + // Verify structure before removal + t.truthy(tree.hasPath("dir1/dir2/only.js"), "Should have dir1/dir2/only.js"); + t.truthy(tree._findNode("dir1/dir2"), "Directory dir1/dir2 should exist"); + + // Remove the only resource in dir2 + const result = await tree.removeResources(["dir1/dir2/only.js"]); + + t.deepEqual(result.removed, ["dir1/dir2/only.js"], "Should remove resource"); + t.false(tree.hasPath("dir1/dir2/only.js"), "Should not have dir1/dir2/only.js"); + + // Check if empty directory is cleaned up + const dir2Node = tree._findNode("dir1/dir2"); + t.is(dir2Node, null, "Empty directory dir1/dir2 should be removed"); + + // Parent directory should still exist with other.js + t.truthy(tree.hasPath("dir1/other.js"), "Should still have dir1/other.js"); + t.truthy(tree._findNode("dir1"), "Parent directory dir1 should still exist"); +}); + +test("removeResources - cleans up deeply nested empty directories", async (t) => { + const tree = new HashTree([ + {path: "a/b/c/d/e/deep.js", integrity: "hash-deep"}, + {path: "a/sibling.js", integrity: "hash-sibling"} + ]); + + // Verify structure before removal + t.truthy(tree.hasPath("a/b/c/d/e/deep.js"), "Should have deeply nested file"); + t.truthy(tree._findNode("a/b/c/d/e"), "Deep directory should exist"); + + // Remove the only resource in the deep hierarchy + const result = await tree.removeResources(["a/b/c/d/e/deep.js"]); + + t.deepEqual(result.removed, ["a/b/c/d/e/deep.js"], "Should remove resource"); + + // All empty directories in the chain should be removed + t.is(tree._findNode("a/b/c/d/e"), null, "Directory e should be removed"); + t.is(tree._findNode("a/b/c/d"), null, "Directory d should be removed"); + t.is(tree._findNode("a/b/c"), null, "Directory c should be removed"); + t.is(tree._findNode("a/b"), null, "Directory b should be removed"); + + // Parent directory with sibling should still exist + t.truthy(tree._findNode("a"), "Directory a should still exist (has sibling.js)"); + t.truthy(tree.hasPath("a/sibling.js"), "Sibling file should still exist"); +}); + +// ============================================================================ +// Resource Tags Tests +// ============================================================================ + +test("Different tags produce different root hashes", (t) => { + const resources1 = [ + {path: "file1.js", integrity: "hash1", tags: {"ui5:HasDebugVariant": true}} + ]; + + const resources2 = [ + {path: "file1.js", integrity: "hash1", tags: {"ui5:IsBundle": true}} + ]; + + const tree1 = new HashTree(resources1); + const tree2 = new HashTree(resources2); + + t.not(tree1.getRootHash(), tree2.getRootHash(), + "Trees with different tags should have different root hashes"); +}); + +test("Identical tags produce same root hash", (t) => { + const resources1 = [ + {path: "file1.js", integrity: "hash1", tags: {"ui5:HasDebugVariant": true, "ui5:IsBundle": false}} + ]; + + const resources2 = [ + {path: "file1.js", integrity: "hash1", tags: {"ui5:HasDebugVariant": true, "ui5:IsBundle": false}} + ]; + + const tree1 = new HashTree(resources1); + const tree2 = new HashTree(resources2); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Trees with identical tags should have same root hashes"); +}); + +test("No tags is backward compatible (null, undefined, {} all produce same hash)", (t) => { + const treeNull = new HashTree([{path: "file1.js", integrity: "hash1", tags: null}]); + const treeUndefined = new HashTree([{path: "file1.js", integrity: "hash1"}]); + const treeEmpty = new HashTree([{path: "file1.js", integrity: "hash1", tags: {}}]); + + t.is(treeNull.getRootHash(), treeUndefined.getRootHash(), + "null tags and undefined tags should produce same hash"); + t.is(treeNull.getRootHash(), treeEmpty.getRootHash(), + "null tags and empty tags should produce same hash"); +}); + +test("Tag key order does not affect hash", (t) => { + const resources1 = [ + {path: "file1.js", integrity: "hash1", tags: {"b": true, "a": true}} + ]; + + const resources2 = [ + {path: "file1.js", integrity: "hash1", tags: {"a": true, "b": true}} + ]; + + const tree1 = new HashTree(resources1); + const tree2 = new HashTree(resources2); + + t.is(tree1.getRootHash(), tree2.getRootHash(), + "Tag key order should not affect the hash"); +}); + +test("Upsert detects tag-only change (content unchanged, tags changed)", async (t) => { + const tree = new HashTree([ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100, tags: {"ui5:HasDebugVariant": true}} + ]); + const originalHash = tree.getRootHash(); + + // Upsert with same content but different tags + const resource = createMockResource("file1.js", "hash1", 1000, 100, 1); + resource.tags = {"ui5:HasDebugVariant": false}; + + const result = await tree.upsertResources([resource]); + + t.deepEqual(result.updated, ["file1.js"], "Should report resource as updated due to tag change"); + t.deepEqual(result.unchanged, [], "Should not report resource as unchanged"); + t.not(tree.getRootHash(), originalHash, "Root hash should change after tag-only update"); +}); + +test("Upsert reports unchanged when both content and tags are the same", async (t) => { + const tree = new HashTree([ + {path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100, + tags: {"ui5:HasDebugVariant": true}} + ]); + const originalHash = tree.getRootHash(); + + const resource = createMockResource("file1.js", "hash1", 1000, 100, 1); + resource.tags = {"ui5:HasDebugVariant": true}; + + const result = await tree.upsertResources([resource]); + + t.deepEqual(result.unchanged, ["file1.js"], "Should report resource as unchanged"); + t.deepEqual(result.updated, [], "Should not report resource as updated"); + t.is(tree.getRootHash(), originalHash, "Root hash should not change"); +}); + +test("Serialization roundtrip preserves tags and root hash", (t) => { + const resources = [ + {path: "file1.js", integrity: "hash1", tags: {"ui5:HasDebugVariant": true, "ui5:IsBundle": false}}, + {path: "dir/file2.js", integrity: "hash2", tags: {"custom:tag": "value"}}, + {path: "file3.js", integrity: "hash3"} // no tags + ]; + + const tree = new HashTree(resources); + const originalHash = tree.getRootHash(); + + // Serialize and deserialize + const cacheObject = tree.toCacheObject(); + const restored = HashTree.fromCache(cacheObject); + + t.is(restored.getRootHash(), originalHash, + "Restored tree should have same root hash as original"); + + // Verify tags are preserved on individual nodes + const node1 = restored.getResourceByPath("file1.js"); + t.deepEqual(node1.tags, {"ui5:HasDebugVariant": true, "ui5:IsBundle": false}, + "Tags should be preserved after serialization roundtrip"); + + const node2 = restored.getResourceByPath("dir/file2.js"); + t.deepEqual(node2.tags, {"custom:tag": "value"}, + "Tags should be preserved for nested resources"); + + const node3 = restored.getResourceByPath("file3.js"); + t.is(node3.tags, null, "Null tags should be preserved"); +}); diff --git a/packages/project/test/lib/build/cache/index/SharedHashTree.js b/packages/project/test/lib/build/cache/index/SharedHashTree.js new file mode 100644 index 00000000000..131306b7ee4 --- /dev/null +++ b/packages/project/test/lib/build/cache/index/SharedHashTree.js @@ -0,0 +1,730 @@ +import test from "ava"; +import sinon from "sinon"; +import SharedHashTree from "../../../../../lib/build/cache/index/SharedHashTree.js"; +import TreeRegistry from "../../../../../lib/build/cache/index/TreeRegistry.js"; + +// Helper to create mock Resource instances +function createMockResource(path, integrity, lastModified, size, inode) { + return { + getOriginalPath: () => path, + getIntegrity: async () => integrity, + getLastModified: () => lastModified, + getSize: async () => size, + getInode: () => inode, + getTags: () => null + }; +} + +test.afterEach.always((t) => { + sinon.restore(); +}); + +// ============================================================================ +// SharedHashTree Construction Tests +// ============================================================================ + +test("SharedHashTree - requires registry option", (t) => { + t.throws(() => { + new SharedHashTree([{path: "a.js", integrity: "hash1"}]); + }, { + message: "SharedHashTree requires a registry option" + }, "Should throw error when registry is missing"); +}); + +test("SharedHashTree - auto-registers with registry", (t) => { + const registry = new TreeRegistry(); + new SharedHashTree([{path: "a.js", integrity: "hash1"}], registry); + + t.is(registry.getTreeCount(), 1, "Should auto-register with registry"); +}); + +test("SharedHashTree - creates tree with resources", (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ]; + const tree = new SharedHashTree(resources, registry); + + t.truthy(tree.getRootHash(), "Should have root hash"); + t.true(tree.hasPath("a.js"), "Should have a.js"); + t.true(tree.hasPath("b.js"), "Should have b.js"); +}); + +// ============================================================================ +// SharedHashTree fromCache Tests +// ============================================================================ + +test("SharedHashTree.fromCache - restores tree from cache data", (t) => { + const registry = new TreeRegistry(); + + // Create original tree + const tree1 = new SharedHashTree([ + {path: "a.js", integrity: "hash-a", size: 100, lastModified: 1000, inode: 1}, + {path: "b.js", integrity: "hash-b", size: 200, lastModified: 2000, inode: 2} + ], registry); + + // Serialize tree + const cacheData = tree1.toCacheObject(); + + // Restore from cache + const registry2 = new TreeRegistry(); + const tree2 = SharedHashTree.fromCache(cacheData, registry2); + + t.truthy(tree2, "Should create tree from cache"); + t.is(tree2.getRootHash(), tree1.getRootHash(), "Root hash should match"); + t.true(tree2.hasPath("a.js"), "Should have a.js"); + t.true(tree2.hasPath("b.js"), "Should have b.js"); +}); + +test("SharedHashTree.fromCache - registers with provided registry", (t) => { + const registry1 = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"} + ], registry1); + + const cacheData = tree1.toCacheObject(); + + const registry2 = new TreeRegistry(); + t.is(registry2.getTreeCount(), 0, "Registry should be empty initially"); + + SharedHashTree.fromCache(cacheData, registry2); + + t.is(registry2.getTreeCount(), 1, "Tree should be registered with new registry"); +}); + +test("SharedHashTree.fromCache - throws on unsupported version", (t) => { + const registry = new TreeRegistry(); + + const invalidCacheData = { + version: 999, + root: { + type: "directory", + hash: "some-hash", + children: {} + } + }; + + const error = t.throws(() => { + SharedHashTree.fromCache(invalidCacheData, registry); + }, { + instanceOf: Error + }); + + t.is(error.message, "Unsupported version: 999", "Should throw error for unsupported version"); +}); + +test("SharedHashTree.fromCache - preserves tree structure", (t) => { + const registry = new TreeRegistry(); + + // Create tree with nested structure + const tree1 = new SharedHashTree([ + {path: "src/components/Button.js", integrity: "hash-button", size: 300, lastModified: 3000, inode: 3}, + {path: "src/utils/helper.js", integrity: "hash-helper", size: 400, lastModified: 4000, inode: 4}, + {path: "test/button.test.js", integrity: "hash-test", size: 500, lastModified: 5000, inode: 5} + ], registry); + + const cacheData = tree1.toCacheObject(); + + const registry2 = new TreeRegistry(); + const tree2 = SharedHashTree.fromCache(cacheData, registry2); + + // Verify all paths exist + t.true(tree2.hasPath("src/components/Button.js"), "Should have Button.js"); + t.true(tree2.hasPath("src/utils/helper.js"), "Should have helper.js"); + t.true(tree2.hasPath("test/button.test.js"), "Should have test file"); + + // Verify structure matches + const paths1 = tree1.getResourcePaths().sort(); + const paths2 = tree2.getResourcePaths().sort(); + t.deepEqual(paths2, paths1, "Resource paths should match"); +}); + +test("SharedHashTree.fromCache - preserves resource metadata", (t) => { + const registry = new TreeRegistry(); + + const tree1 = new SharedHashTree([ + {path: "file.js", integrity: "hash-abc123", size: 12345, lastModified: 9999, inode: 7777} + ], registry); + + const cacheData = tree1.toCacheObject(); + + const registry2 = new TreeRegistry(); + const tree2 = SharedHashTree.fromCache(cacheData, registry2); + + const node1 = tree1.root.children.get("file.js"); + const node2 = tree2.root.children.get("file.js"); + + t.is(node2.integrity, node1.integrity, "Should preserve integrity"); + t.is(node2.size, node1.size, "Should preserve size"); + t.is(node2.lastModified, node1.lastModified, "Should preserve lastModified"); + t.is(node2.inode, node1.inode, "Should preserve inode"); +}); + +test("SharedHashTree.fromCache - accepts indexTimestamp option", (t) => { + const registry = new TreeRegistry(); + + const tree1 = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"} + ], registry, {indexTimestamp: 5000}); + + const cacheData = tree1.toCacheObject(); + + const registry2 = new TreeRegistry(); + const tree2 = SharedHashTree.fromCache(cacheData, registry2, {indexTimestamp: 5000}); + + t.is(tree2.getIndexTimestamp(), 5000, "Should accept and use indexTimestamp option"); +}); + +test("SharedHashTree.fromCache - restored tree can be modified", async (t) => { + const registry = new TreeRegistry(); + + const tree1 = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"} + ], registry); + + const cacheData = tree1.toCacheObject(); + + const registry2 = new TreeRegistry(); + const tree2 = SharedHashTree.fromCache(cacheData, registry2); + + const originalHash = tree2.getRootHash(); + + // Modify restored tree + await tree2.upsertResources([ + createMockResource("b.js", "hash-b", Date.now(), 1024, 1) + ], Date.now()); + await registry2.flush(); + + const newHash = tree2.getRootHash(); + t.not(newHash, originalHash, "Hash should change after modification"); + t.true(tree2.hasPath("b.js"), "Should have new resource"); +}); + +test("SharedHashTree.fromCache - handles empty tree", (t) => { + const registry = new TreeRegistry(); + + const tree1 = new SharedHashTree([], registry); + const cacheData = tree1.toCacheObject(); + + const registry2 = new TreeRegistry(); + const tree2 = SharedHashTree.fromCache(cacheData, registry2); + + t.truthy(tree2, "Should create tree from empty cache"); + t.is(tree2.getResourcePaths().length, 0, "Should have no resources"); + t.truthy(tree2.getRootHash(), "Should have root hash even when empty"); +}); + +// ============================================================================ +// SharedHashTree upsertResources Tests +// ============================================================================ + +test("SharedHashTree - upsertResources schedules with registry", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const resource = createMockResource("b.js", "hash-b", Date.now(), 1024, 1); + const result = await tree.upsertResources([resource], Date.now()); + + t.is(result, undefined, "Should return undefined (scheduled mode)"); + t.is(registry.getPendingUpdateCount(), 1, "Should schedule upsert with registry"); +}); + +test("SharedHashTree - upsertResources with empty array returns immediately", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const result = await tree.upsertResources([], Date.now()); + + t.is(result, undefined, "Should return undefined"); + t.is(registry.getPendingUpdateCount(), 0, "Should not schedule anything"); +}); + +test("SharedHashTree - multiple upserts are batched", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + await tree.upsertResources([createMockResource("b.js", "hash-b", Date.now(), 1024, 1)], Date.now()); + await tree.upsertResources([createMockResource("c.js", "hash-c", Date.now(), 2048, 2)], Date.now()); + + t.is(registry.getPendingUpdateCount(), 2, "Should have 2 pending upserts"); + + const result = await registry.flush(); + t.deepEqual(result.added.sort(), ["b.js", "c.js"], "Should add both resources"); +}); + +// ============================================================================ +// SharedHashTree removeResources Tests +// ============================================================================ + +test("SharedHashTree - removeResources schedules with registry", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ], registry); + + const result = await tree.removeResources(["b.js"]); + + t.is(result, undefined, "Should return undefined (scheduled mode)"); + t.is(registry.getPendingUpdateCount(), 1, "Should schedule removal with registry"); +}); + +test("SharedHashTree - removeResources with empty array returns immediately", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const result = await tree.removeResources([]); + + t.is(result, undefined, "Should return undefined"); + t.is(registry.getPendingUpdateCount(), 0, "Should not schedule anything"); +}); + +test("SharedHashTree - multiple removals are batched", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"}, + {path: "c.js", integrity: "hash-c"} + ], registry); + + await tree.removeResources(["b.js"]); + await tree.removeResources(["c.js"]); + + t.is(registry.getPendingUpdateCount(), 2, "Should have 2 pending removals"); + + const result = await registry.flush(); + t.deepEqual(result.removed.sort(), ["b.js", "c.js"], "Should remove both resources"); +}); + +// ============================================================================ +// SharedHashTree deriveTree Tests +// ============================================================================ + +test("SharedHashTree - deriveTree creates SharedHashTree", (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const tree2 = tree1.deriveTree([{path: "b.js", integrity: "hash-b"}]); + + t.true(tree2 instanceof SharedHashTree, "Derived tree should be SharedHashTree"); + t.is(tree2.registry, registry, "Derived tree should share same registry"); +}); + +test("SharedHashTree - deriveTree registers derived tree", (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + t.is(registry.getTreeCount(), 1, "Should have 1 tree initially"); + + tree1.deriveTree([{path: "b.js", integrity: "hash-b"}]); + + t.is(registry.getTreeCount(), 2, "Should have 2 trees after derivation"); +}); + +test("SharedHashTree - deriveTree shares nodes with parent", (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ], registry); + + const tree2 = tree1.deriveTree([{path: "unique/c.js", integrity: "hash-c"}]); + + // Verify they share the "shared" directory node + const sharedDir1 = tree1.root.children.get("shared"); + const sharedDir2 = tree2.root.children.get("shared"); + + t.is(sharedDir1, sharedDir2, "Should share the same 'shared' directory node"); +}); + +test("SharedHashTree - deriveTree with empty resources", (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const tree2 = tree1.deriveTree([]); + + t.is(tree1.getRootHash(), tree2.getRootHash(), "Empty derivation should have same hash"); + t.true(tree2 instanceof SharedHashTree, "Should be SharedHashTree"); +}); + +test("deriveTree - copies only modified directories (copy-on-write)", (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ], registry); + + // Derive a new tree (should share structure per design goal) + const tree2 = tree1.deriveTree([]); + + // Check if they share the "shared" directory node initially + const dir1Before = tree1.root.children.get("shared"); + const dir2Before = tree2.root.children.get("shared"); + + t.is(dir1Before, dir2Before, "Should share same directory node after deriveTree"); + + // Now insert into tree2 via the intended API (not directly) + tree2._insertResourceWithSharing("shared/c.js", {integrity: "hash-c"}); + + // Check what happened + const dir1After = tree1.root.children.get("shared"); + const dir2After = tree2.root.children.get("shared"); + + // EXPECTED BEHAVIOR (per copy-on-write): + // - Tree2 should copy "shared" directory to add "c.js" without affecting tree1 + // - dir2After !== dir1After (tree2 has its own copy) + // - dir1After === dir1Before (tree1 unchanged) + + t.is(dir1After, dir1Before, "Tree1 should be unaffected"); + t.not(dir2After, dir1After, "Tree2 should have its own copy after modification"); +}); + +test("deriveTree - preserves structural sharing for unmodified paths", (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/nested/deep/a.js", integrity: "hash-a"}, + {path: "other/b.js", integrity: "hash-b"} + ], registry); + + // Derive tree and add to "other" directory + const tree2 = tree1.deriveTree([]); + tree2._insertResourceWithSharing("other/c.js", {integrity: "hash-c"}); + + // The "shared" directory should still be shared (not copied) + // because we didn't modify it + const sharedDir1 = tree1.root.children.get("shared"); + const sharedDir2 = tree2.root.children.get("shared"); + + t.is(sharedDir1, sharedDir2, + "Unmodified 'shared' directory should remain shared between trees"); + + // But "other" should be copied (we modified it) + const otherDir1 = tree1.root.children.get("other"); + const otherDir2 = tree2.root.children.get("other"); + + t.not(otherDir1, otherDir2, + "Modified 'other' directory should be copied in tree2"); + + // Verify tree1 wasn't affected + t.false(tree1.hasPath("other/c.js"), "Tree1 should not have c.js"); + t.true(tree2.hasPath("other/c.js"), "Tree2 should have c.js"); +}); + +test("deriveTree - changes propagate to derived trees (shared view)", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a", lastModified: 1000, size: 100} + ], registry); + + // Create derived tree - it's a view on the same data, not an independent copy + const tree2 = tree1.deriveTree([ + {path: "unique/b.js", integrity: "hash-b"} + ]); + + // Get reference to shared directory in both trees + const sharedDir1 = tree1.root.children.get("shared"); + const sharedDir2 = tree2.root.children.get("shared"); + + // By design: They SHOULD share the same node reference + t.is(sharedDir1, sharedDir2, "Trees share directory nodes (intentional design)"); + + // When tree1 is updated, tree2 sees the change (filtered view behavior) + const indexTimestamp = tree1.getIndexTimestamp(); + await tree1.upsertResources([ + createMockResource("shared/a.js", "new-hash-a", indexTimestamp + 1, 101, 1) + ]); + await registry.flush(); + + // Both trees see the update as per design + const node1 = tree1.root.children.get("shared").children.get("a.js"); + const node2 = tree2.root.children.get("shared").children.get("a.js"); + + t.is(node1, node2, "Same resource node (shared reference)"); + t.is(node1.integrity, "new-hash-a", "Tree1 sees update"); + t.is(node2.integrity, "new-hash-a", "Tree2 also sees update (intentional)"); + + // This is the intended behavior: derived trees are views, not snapshots + // Tree2 filters which resources it exposes, but underlying data is shared +}); + + +// ============================================================================ +// getAddedResources Tests +// ============================================================================ + +test("getAddedResources - returns empty array when no resources added", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ], registry); + + const derivedTree = baseTree.deriveTree([]); + + const added = derivedTree.getAddedResources(baseTree); + + t.deepEqual(added, [], "Should return empty array when no resources added"); +}); + +test("getAddedResources - returns added resources from derived tree", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ], registry); + + const derivedTree = baseTree.deriveTree([ + {path: "c.js", integrity: "hash-c", size: 100, lastModified: 1000, inode: 1}, + {path: "d.js", integrity: "hash-d", size: 200, lastModified: 2000, inode: 2} + ]); + + const added = derivedTree.getAddedResources(baseTree); + + t.is(added.length, 2, "Should return 2 added resources"); + t.deepEqual(added, [ + {path: "/c.js", integrity: "hash-c", size: 100, lastModified: 1000, inode: 1, tags: null}, + {path: "/d.js", integrity: "hash-d", size: 200, lastModified: 2000, inode: 2, tags: null} + ], "Should return correct added resources with metadata"); +}); + +test("getAddedResources - handles nested directory additions", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "root/a.js", integrity: "hash-a"} + ], registry); + + const derivedTree = baseTree.deriveTree([ + {path: "root/nested/b.js", integrity: "hash-b", size: 100, lastModified: 1000, inode: 1}, + {path: "root/nested/c.js", integrity: "hash-c", size: 200, lastModified: 2000, inode: 2} + ]); + + const added = derivedTree.getAddedResources(baseTree); + + t.is(added.length, 2, "Should return 2 added resources"); + t.true(added.some((r) => r.path === "/root/nested/b.js"), "Should include nested b.js"); + t.true(added.some((r) => r.path === "/root/nested/c.js"), "Should include nested c.js"); +}); + +test("getAddedResources - handles new directory with multiple resources", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "src/a.js", integrity: "hash-a"} + ], registry); + + const derivedTree = baseTree.deriveTree([ + {path: "lib/b.js", integrity: "hash-b", size: 100, lastModified: 1000, inode: 1}, + {path: "lib/c.js", integrity: "hash-c", size: 200, lastModified: 2000, inode: 2}, + {path: "lib/nested/d.js", integrity: "hash-d", size: 300, lastModified: 3000, inode: 3} + ]); + + const added = derivedTree.getAddedResources(baseTree); + + t.is(added.length, 3, "Should return 3 added resources"); + t.true(added.some((r) => r.path === "/lib/b.js"), "Should include lib/b.js"); + t.true(added.some((r) => r.path === "/lib/c.js"), "Should include lib/c.js"); + t.true(added.some((r) => r.path === "/lib/nested/d.js"), "Should include nested resource"); +}); + +test("getAddedResources - preserves metadata for added resources", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"} + ], registry); + + const derivedTree = baseTree.deriveTree([ + {path: "b.js", integrity: "hash-b", size: 12345, lastModified: 9999, inode: 7777} + ]); + + const added = derivedTree.getAddedResources(baseTree); + + t.is(added.length, 1, "Should return 1 added resource"); + t.is(added[0].path, "/b.js", "Should have correct path"); + t.is(added[0].integrity, "hash-b", "Should preserve integrity"); + t.is(added[0].size, 12345, "Should preserve size"); + t.is(added[0].lastModified, 9999, "Should preserve lastModified"); + t.is(added[0].inode, 7777, "Should preserve inode"); +}); + +test("getAddedResources - handles mixed shared and added resources", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ], registry); + + const derivedTree = baseTree.deriveTree([ + {path: "shared/c.js", integrity: "hash-c", size: 100, lastModified: 1000, inode: 1}, + {path: "unique/d.js", integrity: "hash-d", size: 200, lastModified: 2000, inode: 2} + ]); + + const added = derivedTree.getAddedResources(baseTree); + + t.is(added.length, 2, "Should return 2 added resources"); + t.true(added.some((r) => r.path === "/shared/c.js"), "Should include c.js in shared dir"); + t.true(added.some((r) => r.path === "/unique/d.js"), "Should include d.js in unique dir"); + t.false(added.some((r) => r.path === "/shared/a.js"), "Should not include shared a.js"); + t.false(added.some((r) => r.path === "/shared/b.js"), "Should not include shared b.js"); +}); + +test("getAddedResources - handles deeply nested additions", (t) => { + const registry = new TreeRegistry(); + const baseTree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"} + ], registry); + + const derivedTree = baseTree.deriveTree([ + {path: "dir1/dir2/dir3/dir4/deep.js", integrity: "hash-deep", size: 100, lastModified: 1000, inode: 1} + ]); + + const added = derivedTree.getAddedResources(baseTree); + + t.is(added.length, 1, "Should return 1 added resource"); + t.is(added[0].path, "/dir1/dir2/dir3/dir4/deep.js", "Should have correct deeply nested path"); + t.is(added[0].integrity, "hash-deep", "Should preserve integrity"); +}); + + +// ============================================================================ +// SharedHashTree with Registry Integration Tests +// ============================================================================ + +test("SharedHashTree - changes via registry affect tree", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const originalHash = tree.getRootHash(); + + await tree.upsertResources([createMockResource("b.js", "hash-b", Date.now(), 1024, 1)], Date.now()); + await registry.flush(); + + const newHash = tree.getRootHash(); + t.not(originalHash, newHash, "Root hash should change after flush"); + t.true(tree.hasPath("b.js"), "Tree should have new resource"); +}); + +test("SharedHashTree - batch updates via registry", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a", lastModified: 1000, size: 100} + ], registry); + + const indexTimestamp = tree.getIndexTimestamp(); + + // Schedule multiple operations + await tree.upsertResources([createMockResource("b.js", "hash-b", Date.now(), 1024, 1)], Date.now()); + await tree.upsertResources([createMockResource("a.js", "new-hash-a", indexTimestamp + 1, 101, 1)], Date.now()); + + const result = await registry.flush(); + + t.deepEqual(result.added, ["b.js"], "Should add b.js"); + t.deepEqual(result.updated, ["a.js"], "Should update a.js"); +}); + +test("SharedHashTree - multiple trees coordinate via registry", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"} + ], registry); + + const tree2 = tree1.deriveTree([{path: "unique/b.js", integrity: "hash-b"}]); + + // Verify they share directory nodes + const sharedDir1Before = tree1.root.children.get("shared"); + const sharedDir2Before = tree2.root.children.get("shared"); + t.is(sharedDir1Before, sharedDir2Before, "Should share nodes before update"); + + // Update shared resource via tree1 + const indexTimestamp = tree1.getIndexTimestamp(); + await tree1.upsertResources([ + createMockResource("shared/a.js", "new-hash-a", indexTimestamp + 1, 101, 1) + ], Date.now()); + + await registry.flush(); + + // Both trees see the change + const node1 = tree1.root.children.get("shared").children.get("a.js"); + const node2 = tree2.root.children.get("shared").children.get("a.js"); + + t.is(node1, node2, "Should share same resource node"); + t.is(node1.integrity, "new-hash-a", "Tree1 sees update"); + t.is(node2.integrity, "new-hash-a", "Tree2 sees update (shared node)"); +}); + +test("SharedHashTree - registry tracks per-tree statistics", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + const tree2 = new SharedHashTree([{path: "b.js", integrity: "hash-b"}], registry); + + await tree1.upsertResources([createMockResource("c.js", "hash-c", Date.now(), 1024, 1)], Date.now()); + await tree2.upsertResources([createMockResource("d.js", "hash-d", Date.now(), 2048, 2)], Date.now()); + + const result = await registry.flush(); + + t.is(result.treeStats.size, 2, "Should have stats for 2 trees"); + // Each tree only sees additions for resources added to itself (not to other independent trees) + const stats1 = result.treeStats.get(tree1); + const stats2 = result.treeStats.get(tree2); + + // c.js is only added to tree1, d.js is only added to tree2 + t.deepEqual(stats1.added.sort(), ["c.js"], "Tree1 should see c.js addition"); + t.deepEqual(stats2.added.sort(), ["d.js"], "Tree2 should see d.js addition"); +}); + +test("SharedHashTree - unregister removes tree from coordination", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + new SharedHashTree([{path: "b.js", integrity: "hash-b"}], registry); + + t.is(registry.getTreeCount(), 2, "Should have 2 trees"); + + registry.unregister(tree1); + + t.is(registry.getTreeCount(), 1, "Should have 1 tree after unregister"); + + // Operations on tree1 no longer coordinated + await tree1.upsertResources([createMockResource("c.js", "hash-c", Date.now(), 1024, 1)], Date.now()); + const result = await registry.flush(); + + // tree1 not in results since it's unregistered + t.is(result.treeStats.size, 1, "Should only have stats for tree2"); + t.false(result.treeStats.has(tree1), "Should not have stats for unregistered tree1"); +}); + +test("SharedHashTree - complex multi-tree coordination", async (t) => { + const registry = new TreeRegistry(); + + // Create base tree + const baseTree = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ], registry); + + // Derive two trees from base + const derived1 = baseTree.deriveTree([{path: "d1/c.js", integrity: "hash-c"}]); + const derived2 = baseTree.deriveTree([{path: "d2/d.js", integrity: "hash-d"}]); + + t.is(registry.getTreeCount(), 3, "Should have 3 trees"); + + // Schedule updates to shared resource + const indexTimestamp = baseTree.getIndexTimestamp(); + await baseTree.upsertResources([ + createMockResource("shared/a.js", "new-hash-a", indexTimestamp + 1, 101, 1) + ], Date.now()); + + const result = await registry.flush(); + + // All trees see the update + t.deepEqual(result.treeStats.get(baseTree).updated, ["shared/a.js"]); + t.deepEqual(result.treeStats.get(derived1).updated, ["shared/a.js"]); + t.deepEqual(result.treeStats.get(derived2).updated, ["shared/a.js"]); + + // Verify shared nodes + const sharedA1 = baseTree.root.children.get("shared").children.get("a.js"); + const sharedA2 = derived1.root.children.get("shared").children.get("a.js"); + const sharedA3 = derived2.root.children.get("shared").children.get("a.js"); + + t.is(sharedA1, sharedA2, "baseTree and derived1 share node"); + t.is(sharedA1, sharedA3, "baseTree and derived2 share node"); + t.is(sharedA1.integrity, "new-hash-a", "All see updated value"); +}); diff --git a/packages/project/test/lib/build/cache/index/TreeRegistry.js b/packages/project/test/lib/build/cache/index/TreeRegistry.js new file mode 100644 index 00000000000..f7f0b5e6b7a --- /dev/null +++ b/packages/project/test/lib/build/cache/index/TreeRegistry.js @@ -0,0 +1,973 @@ +import test from "ava"; +import sinon from "sinon"; +import SharedHashTree from "../../../../../lib/build/cache/index/SharedHashTree.js"; +import TreeRegistry from "../../../../../lib/build/cache/index/TreeRegistry.js"; + +// Helper to create mock Resource instances +function createMockResource(path, integrity, lastModified, size, inode) { + return { + getOriginalPath: () => path, + getIntegrity: async () => integrity, + getLastModified: () => lastModified, + getSize: async () => size, + getInode: () => inode, + getTags: () => null + }; +} + +test.afterEach.always((t) => { + sinon.restore(); +}); + +// ============================================================================ +// TreeRegistry Tests +// ============================================================================ + +test("TreeRegistry - register and track trees", (t) => { + const registry = new TreeRegistry(); + new SharedHashTree([{path: "a.js", integrity: "hash1"}], registry); + new SharedHashTree([{path: "b.js", integrity: "hash2"}], registry); + + t.is(registry.getTreeCount(), 2, "Should track both trees"); +}); + +test("TreeRegistry - schedule and flush updates", async (t) => { + const registry = new TreeRegistry(); + const resources = [{path: "file.js", integrity: "hash1"}]; + const tree = new SharedHashTree(resources, registry); + + const originalHash = tree.getRootHash(); + + const resource = createMockResource("file.js", "hash2", Date.now(), 2048, 456); + registry.scheduleUpsert(resource); + t.is(registry.getPendingUpdateCount(), 1, "Should have one pending update"); + + const result = await registry.flush(); + t.is(registry.getPendingUpdateCount(), 0, "Should have no pending updates after flush"); + t.deepEqual(result.updated, ["file.js"], "Should return changed resource path"); + + const newHash = tree.getRootHash(); + t.not(originalHash, newHash, "Root hash should change after flush"); +}); + +test("TreeRegistry - flush returns only changed resources", async (t) => { + const registry = new TreeRegistry(); + const timestamp = Date.now(); + const resources = [ + {path: "file1.js", integrity: "hash1", lastModified: timestamp, size: 1024, inode: 123}, + {path: "file2.js", integrity: "hash2", lastModified: timestamp, size: 2048, inode: 124} + ]; + new SharedHashTree(resources, registry); + + registry.scheduleUpsert(createMockResource("file1.js", "new-hash1", timestamp, 1024, 123)); + registry.scheduleUpsert(createMockResource("file2.js", "hash2", timestamp, 2048, 124)); // unchanged + + const result = await registry.flush(); + t.deepEqual(result.updated, ["file1.js"], "Should return only changed resource"); +}); + +test("TreeRegistry - flush returns empty array when no changes", async (t) => { + const registry = new TreeRegistry(); + const timestamp = Date.now(); + const resources = [{path: "file.js", integrity: "hash1", lastModified: timestamp, size: 1024, inode: 123}]; + new SharedHashTree(resources, registry); + + registry.scheduleUpsert(createMockResource("file.js", "hash1", timestamp, 1024, 123)); // same value + + const result = await registry.flush(); + t.deepEqual(result.updated, [], "Should return empty array when no actual changes"); +}); + +test("TreeRegistry - batch updates affect all trees sharing nodes", async (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const originalHash1 = tree1.getRootHash(); + + // Create derived tree that shares "shared" directory + const tree2 = tree1.deriveTree([{path: "unique/c.js", integrity: "hash-c"}]); + const originalHash2 = tree2.getRootHash(); + t.not(originalHash1, originalHash2, "Hashes should differ due to unique content"); + + // Verify they share the same "shared" directory node + const sharedDir1 = tree1.root.children.get("shared"); + const sharedDir2 = tree2.root.children.get("shared"); + t.is(sharedDir1, sharedDir2, "Should share the same 'shared' directory node"); + + // Update shared resource + registry.scheduleUpsert(createMockResource("shared/a.js", "new-hash-a", Date.now(), 2048, 999)); + const result = await registry.flush(); + + t.deepEqual(result.updated, ["shared/a.js"], "Should report the updated resource"); + + const newHash1 = tree1.getRootHash(); + const newHash2 = tree2.getRootHash(); + + t.not(originalHash1, newHash1, "Tree1 hash should change"); + t.not(originalHash2, newHash2, "Tree2 hash should change"); + t.not(newHash1, newHash2, "Hashes should differ due to unique content"); + + // Both trees should see the update + const resource1 = tree1.getResourceByPath("shared/a.js"); + const resource2 = tree2.getResourceByPath("shared/a.js"); + + t.is(resource1.integrity, "new-hash-a", "Tree1 should have updated integrity"); + t.is(resource2.integrity, "new-hash-a", "Tree2 should have updated integrity (shared node)"); +}); + +test("TreeRegistry - handles missing resources gracefully during flush", async (t) => { + const registry = new TreeRegistry(); + new SharedHashTree([{path: "exists.js", integrity: "hash1"}], registry); + + // Schedule update for non-existent resource + registry.scheduleUpsert(createMockResource("missing.js", "hash2", Date.now(), 1024, 444)); + + // Should not throw + await t.notThrows(async () => await registry.flush(), "Should handle missing resources gracefully"); +}); + +test("TreeRegistry - multiple updates to same resource", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "file.js", integrity: "v1"}], registry); + + const timestamp = Date.now(); + registry.scheduleUpsert(createMockResource("file.js", "v2", timestamp, 1024, 100)); + registry.scheduleUpsert(createMockResource("file.js", "v3", timestamp + 1, 1024, 100)); + registry.scheduleUpsert(createMockResource("file.js", "v4", timestamp + 2, 1024, 100)); + + t.is(registry.getPendingUpdateCount(), 1, "Should consolidate updates to same path"); + + await registry.flush(); + + // Should apply the last update + t.is(tree.getResourceByPath("file.js").integrity, "v4", "Should apply last update"); +}); + +test("TreeRegistry - updates without changes lead to same hash", async (t) => { + const registry = new TreeRegistry(); + const timestamp = Date.now(); + const tree = new SharedHashTree([{ + path: "/src/foo/file1.js", integrity: "v1", + }, { + path: "/src/foo/file3.js", integrity: "v1", + }, { + path: "/src/foo/file2.js", integrity: "v1", + }], registry); + const initialHash = tree.getRootHash(); + const file2Hash = tree.getResourceByPath("/src/foo/file2.js").hash; + + registry.scheduleUpsert(createMockResource("/src/foo/file2.js", "v1", timestamp, 1024, 200)); + + t.is(registry.getPendingUpdateCount(), 1, "Should have one pending update"); + + await registry.flush(); + + // Should apply the last update + t.is(tree.getResourceByPath("/src/foo/file2.js").hash.toString("hex"), file2Hash.toString("hex"), + "Should have same has for file"); + t.is(tree.getRootHash(), initialHash, "Root hash should remain unchanged"); +}); + +test("TreeRegistry - unregister tree", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash1"}], registry); + const tree2 = new SharedHashTree([{path: "b.js", integrity: "hash2"}], registry); + + t.is(registry.getTreeCount(), 2); + + registry.unregister(tree1); + t.is(registry.getTreeCount(), 1); + + // Flush should only affect tree2 + registry.scheduleUpsert(createMockResource("b.js", "new-hash2", Date.now(), 1024, 777)); + await registry.flush(); + + t.notThrows(() => tree2.getRootHash(), "Tree2 should still work"); +}); + +// ============================================================================ +// Derived Tree Tests +// ============================================================================ + +test("deriveTree - creates tree sharing subtrees", (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "dir1/a.js", integrity: "hash-a"}, + {path: "dir1/b.js", integrity: "hash-b"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([{path: "dir2/c.js", integrity: "hash-c"}]); + + // Both trees should have dir1 + t.truthy(tree2.hasPath("dir1/a.js"), "Derived tree should have shared resources"); + t.truthy(tree2.hasPath("dir2/c.js"), "Derived tree should have new resources"); + + // Tree1 should not have dir2 + t.false(tree1.hasPath("dir2/c.js"), "Original tree should not have derived resources"); +}); + +test("deriveTree - shared nodes are the same reference", (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "shared/file.js", integrity: "hash1"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([]); + + // Get the shared directory node from both trees + const dir1 = tree1.root.children.get("shared"); + const dir2 = tree2.root.children.get("shared"); + + t.is(dir1, dir2, "Shared directory nodes should be same reference"); + + // Get the file node + const file1 = dir1.children.get("file.js"); + const file2 = dir2.children.get("file.js"); + + t.is(file1, file2, "Shared resource nodes should be same reference"); +}); + +test("deriveTree - updates to shared nodes visible in all sub-trees", async (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "shared/file.js", integrity: "original"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([]); + + // Get nodes before update + const node1Before = tree1.getResourceByPath("shared/file.js"); + const node2Before = tree2.getResourceByPath("shared/file.js"); + + t.is(node1Before, node2Before, "Should be same node reference"); + t.is(node1Before.integrity, "original", "Original integrity"); + + // Update via registry + registry.scheduleUpsert(createMockResource("shared/file.js", "updated", Date.now(), 1024, 555)); + await registry.flush(); + + // Both should see the update (same node) + t.is(node1Before.integrity, "updated", "Tree1 node should be updated"); + t.is(node2Before.integrity, "updated", "Tree2 node should be updated (same reference)"); +}); + +test("deriveTree - updates to sub-tree nodes are not visible in parents", async (t) => { + const registry = new TreeRegistry(); + const sharedResources = [ + {path: "shared/file.js", integrity: "original"} + ]; + const uniqueResources = [ + {path: "unique/file.js", integrity: "original"} + ]; + + const tree1 = new SharedHashTree(sharedResources, registry); + const tree2 = tree1.deriveTree(uniqueResources); + + // Update via tree2.upsertResources to ensure it's scoped to tree2 + await tree2.upsertResources([createMockResource("unique/file.js", "updated", Date.now(), 1024, 555)], Date.now()); + await registry.flush(); + + t.deepEqual(tree1.getResourcePaths(), ["/shared/file.js"], "Parent tree should not have unique resource"); + t.is(tree2.getResourceByPath("/unique/file.js").integrity, "updated", "Derived tree should see its own update"); +}); + +test("deriveTree - multiple levels of derivation", async (t) => { + const registry = new TreeRegistry(); + + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + const tree2 = tree1.deriveTree([{path: "b.js", integrity: "hash-b"}]); + const tree3 = tree2.deriveTree([{path: "c.js", integrity: "hash-c"}]); + + t.truthy(tree3.hasPath("a.js"), "Should have resources from tree1"); + t.truthy(tree3.hasPath("b.js"), "Should have resources from tree2"); + t.truthy(tree3.hasPath("c.js"), "Should have its own resources"); + + // Update shared resource + registry.scheduleUpsert(createMockResource("a.js", "new-hash-a", Date.now(), 1024, 111)); + await registry.flush(); + + // All trees should see the update + t.is(tree1.getResourceByPath("a.js").integrity, "new-hash-a"); + t.is(tree2.getResourceByPath("a.js").integrity, "new-hash-a"); + t.is(tree3.getResourceByPath("a.js").integrity, "new-hash-a"); +}); + +test("deriveTree - efficient hash recomputation", async (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "dir1/a.js", integrity: "hash-a"}, + {path: "dir1/b.js", integrity: "hash-b"}, + {path: "dir2/c.js", integrity: "hash-c"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([{path: "dir3/d.js", integrity: "hash-d"}]); + + // Spy on _computeHash to count calls + const computeSpy = sinon.spy(tree1, "_computeHash"); + const compute2Spy = sinon.spy(tree2, "_computeHash"); + + // Update resource in shared directory + registry.scheduleUpsert(createMockResource("dir1/a.js", "new-hash-a", Date.now(), 2048, 222)); + await registry.flush(); + + // Each affected directory should be hashed once per tree + // dir1/a.js node, dir1 node, root node for each tree + t.true(computeSpy.callCount >= 3, "Tree1 should recompute affected nodes"); + t.true(compute2Spy.callCount >= 3, "Tree2 should recompute affected nodes"); +}); + +test("deriveTree - independent updates to different directories", async (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "dir1/a.js", integrity: "hash-a"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([{path: "dir2/b.js", integrity: "hash-b"}]); + + const hash1Before = tree1.getRootHash(); + const hash2Before = tree2.getRootHash(); + + // Update only in tree2's unique directory + registry.scheduleUpsert(createMockResource("dir2/b.js", "new-hash-b", Date.now(), 1024, 333)); + await registry.flush(); + + const hash1After = tree1.getRootHash(); + const hash2After = tree2.getRootHash(); + + // Both trees are affected because they share the root and dir2 is added/updated via registry + t.not(hash1Before, hash1After, "Tree1 hash changes (dir2 added to shared root)"); + t.not(hash2Before, hash2After, "Tree2 hash should change"); + + // Tree1 now has dir2 because registry ensures directory path exists + t.truthy(tree1.hasPath("dir2/b.js"), "Tree1 should now have dir2/b.js"); + t.truthy(tree2.hasPath("dir2/b.js"), "Tree2 should have dir2/b.js"); +}); + +test("deriveTree - preserves tree statistics correctly", (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "dir1/a.js", integrity: "hash-a"}, + {path: "dir1/b.js", integrity: "hash-b"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([ + {path: "dir2/c.js", integrity: "hash-c"}, + {path: "dir2/d.js", integrity: "hash-d"} + ]); + + const stats1 = tree1.getStats(); + const stats2 = tree2.getStats(); + + t.is(stats1.resources, 2, "Tree1 should have 2 resources"); + t.is(stats2.resources, 4, "Tree2 should have 4 resources"); + t.true(stats2.directories >= stats1.directories, "Tree2 should have at least as many directories"); +}); + +test("deriveTree - empty derivation creates exact copy with shared nodes", (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "file.js", integrity: "hash1"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([]); + + // Should have same structure + t.is(tree1.getRootHash(), tree2.getRootHash(), "Should have same root hash"); + + // But different root nodes (shallow copied) + t.not(tree1.root, tree2.root, "Root nodes should be different"); + + // But shared children + const child1 = tree1.root.children.get("file.js"); + const child2 = tree2.root.children.get("file.js"); + t.is(child1, child2, "Children should be shared"); +}); + +test("deriveTree - complex shared structure", async (t) => { + const registry = new TreeRegistry(); + const resources = [ + {path: "shared/deep/nested/file1.js", integrity: "hash1"}, + {path: "shared/deep/file2.js", integrity: "hash2"}, + {path: "shared/file3.js", integrity: "hash3"} + ]; + + const tree1 = new SharedHashTree(resources, registry); + const tree2 = tree1.deriveTree([ + {path: "unique/file4.js", integrity: "hash4"} + ]); + + // Update deeply nested shared file + registry.scheduleUpsert(createMockResource("shared/deep/nested/file1.js", "new-hash1", Date.now(), 2048, 666)); + await registry.flush(); + + // Both trees should reflect the change + t.is(tree1.getResourceByPath("shared/deep/nested/file1.js").integrity, "new-hash1"); + t.is(tree2.getResourceByPath("shared/deep/nested/file1.js").integrity, "new-hash1"); + + // Root hashes should both change + const paths1 = tree1.getResourcePaths(); + const paths2 = tree2.getResourcePaths(); + + t.is(paths1.length, 3, "Tree1 should have 3 resources"); + t.is(paths2.length, 4, "Tree2 should have 4 resources"); +}); + +// ============================================================================ +// upsertResources Tests with Registry +// ============================================================================ + +test("upsertResources - with registry schedules operations", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + const result = await tree.upsertResources([ + createMockResource("b.js", "hash-b", Date.now(), 1024, 1) + ]); + + t.is(result, undefined, "Should return undefined in scheduled mode"); +}); + +test("upsertResources - with registry and flush", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + const originalHash = tree.getRootHash(); + + await tree.upsertResources([ + createMockResource("b.js", "hash-b", Date.now(), 1024, 1), + createMockResource("c.js", "hash-c", Date.now(), 2048, 2) + ]); + + const result = await registry.flush(); + + t.truthy(result.added, "Result should have added array"); + t.true(result.added.includes("b.js"), "Should report b.js as added"); + t.true(result.added.includes("c.js"), "Should report c.js as added"); + + t.truthy(tree.hasPath("b.js"), "Tree should have b.js"); + t.truthy(tree.hasPath("c.js"), "Tree should have c.js"); + t.not(tree.getRootHash(), originalHash, "Root hash should change"); +}); + +test("upsertResources - with derived trees", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "shared/a.js", integrity: "hash-a"}], registry); + const tree2 = tree1.deriveTree([{path: "unique/b.js", integrity: "hash-b"}]); + + await tree1.upsertResources([ + createMockResource("shared/c.js", "hash-c", Date.now(), 1024, 3) + ]); + + await registry.flush(); + + t.truthy(tree1.hasPath("shared/c.js"), "Tree1 should have shared/c.js"); + t.truthy(tree2.hasPath("shared/c.js"), "Tree2 should also have shared/c.js"); + t.false(tree1.hasPath("unique/b.js"), "Tree1 should not have unique/b.js"); + t.truthy(tree2.hasPath("unique/b.js"), "Tree2 should have unique/b.js"); +}); + +// ============================================================================ +// removeResources Tests with Registry +// ============================================================================ + +test("removeResources - with registry schedules operations", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ], registry); + + const result = await tree.removeResources(["b.js"]); + + t.is(result, undefined, "Should return undefined in scheduled mode"); +}); + +test("removeResources - with registry and flush", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"}, + {path: "c.js", integrity: "hash-c"} + ], registry); + const originalHash = tree.getRootHash(); + + await tree.removeResources(["b.js", "c.js"]); + + const result = await registry.flush(); + + t.truthy(result.removed, "Result should have removed array"); + t.true(result.removed.includes("b.js"), "Should report b.js as removed"); + t.true(result.removed.includes("c.js"), "Should report c.js as removed"); + + t.truthy(tree.hasPath("a.js"), "Tree should still have a.js"); + t.false(tree.hasPath("b.js"), "Tree should not have b.js"); + t.false(tree.hasPath("c.js"), "Tree should not have c.js"); + t.not(tree.getRootHash(), originalHash, "Root hash should change"); +}); + +test("removeResources - with derived trees propagates removal", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ], registry); + const tree2 = tree1.deriveTree([{path: "unique/c.js", integrity: "hash-c"}]); + + // Verify both trees share the resources + t.truthy(tree1.hasPath("shared/a.js")); + t.truthy(tree1.hasPath("shared/b.js")); + t.truthy(tree2.hasPath("shared/a.js")); + t.truthy(tree2.hasPath("shared/b.js")); + + // Remove from shared directory + await tree1.removeResources(["shared/b.js"]); + await registry.flush(); + + // Both trees should see the removal + t.truthy(tree1.hasPath("shared/a.js"), "Tree1 should still have shared/a.js"); + t.false(tree1.hasPath("shared/b.js"), "Tree1 should not have shared/b.js"); + t.truthy(tree2.hasPath("shared/a.js"), "Tree2 should still have shared/a.js"); + t.false(tree2.hasPath("shared/b.js"), "Tree2 should not have shared/b.js"); + t.truthy(tree2.hasPath("unique/c.js"), "Tree2 should still have unique/c.js"); +}); + +test("removeResources - with registry cleans up empty directories", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "dir1/dir2/only.js", integrity: "hash-only"}, + {path: "dir1/other.js", integrity: "hash-other"} + ], registry); + + // Verify structure before removal + t.truthy(tree.hasPath("dir1/dir2/only.js"), "Should have dir1/dir2/only.js"); + t.truthy(tree._findNode("dir1/dir2"), "Directory dir1/dir2 should exist"); + + // Remove the only resource in dir2 + await tree.removeResources(["dir1/dir2/only.js"]); + const result = await registry.flush(); + + t.true(result.removed.includes("dir1/dir2/only.js"), "Should report resource as removed"); + t.false(tree.hasPath("dir1/dir2/only.js"), "Should not have dir1/dir2/only.js"); + + // Check if empty directory is cleaned up + const dir2Node = tree._findNode("dir1/dir2"); + t.is(dir2Node, null, "Empty directory dir1/dir2 should be removed"); + + // Parent directory should still exist with other.js + t.truthy(tree.hasPath("dir1/other.js"), "Should still have dir1/other.js"); + t.truthy(tree._findNode("dir1"), "Parent directory dir1 should still exist"); +}); + +test("removeResources - with registry cleans up deeply nested empty directories", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a/b/c/d/e/deep.js", integrity: "hash-deep"}, + {path: "a/sibling.js", integrity: "hash-sibling"} + ], registry); + + // Verify structure before removal + t.truthy(tree.hasPath("a/b/c/d/e/deep.js"), "Should have deeply nested file"); + t.truthy(tree._findNode("a/b/c/d/e"), "Deep directory should exist"); + + // Remove the only resource in the deep hierarchy + await tree.removeResources(["a/b/c/d/e/deep.js"]); + const result = await registry.flush(); + + t.true(result.removed.includes("a/b/c/d/e/deep.js"), "Should report resource as removed"); + + // All empty directories in the chain should be removed + t.is(tree._findNode("a/b/c/d/e"), null, "Directory e should be removed"); + t.is(tree._findNode("a/b/c/d"), null, "Directory d should be removed"); + t.is(tree._findNode("a/b/c"), null, "Directory c should be removed"); + t.is(tree._findNode("a/b"), null, "Directory b should be removed"); + + // Parent directory with sibling should still exist + t.truthy(tree._findNode("a"), "Directory a should still exist (has sibling.js)"); + t.truthy(tree.hasPath("a/sibling.js"), "Sibling file should still exist"); +}); + +test("removeResources - with derived trees cleans up empty directories in both trees", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/dir/only.js", integrity: "hash-only"}, + {path: "shared/other.js", integrity: "hash-other"} + ], registry); + const tree2 = tree1.deriveTree([{path: "unique/file.js", integrity: "hash-unique"}]); + + // Verify both trees share the directory structure + const sharedDirBefore = tree1.root.children.get("shared").children.get("dir"); + const sharedDirBefore2 = tree2.root.children.get("shared").children.get("dir"); + t.is(sharedDirBefore, sharedDirBefore2, "Should share the same 'shared/dir' node"); + + // Remove the only resource in shared/dir + await tree1.removeResources(["shared/dir/only.js"]); + await registry.flush(); + + // Both trees should see empty directory removal + t.is(tree1._findNode("shared/dir"), null, "Tree1: empty directory should be removed"); + t.is(tree2._findNode("shared/dir"), null, "Tree2: empty directory should be removed"); + + // Shared parent directory should still exist with other.js + t.truthy(tree1._findNode("shared"), "Tree1: shared directory should still exist"); + t.truthy(tree2._findNode("shared"), "Tree2: shared directory should still exist"); + t.truthy(tree1.hasPath("shared/other.js"), "Tree1 should still have shared/other.js"); + t.truthy(tree2.hasPath("shared/other.js"), "Tree2 should still have shared/other.js"); + + // Tree2's unique content should be unaffected + t.truthy(tree2.hasPath("unique/file.js"), "Tree2 should still have unique file"); +}); + +test("removeResources - multiple removals with registry clean up shared empty directories", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "dir1/sub1/file1.js", integrity: "hash1"}, + {path: "dir1/sub2/file2.js", integrity: "hash2"}, + {path: "dir2/file3.js", integrity: "hash3"} + ], registry); + + // Remove both files from dir1 (making both sub1 and sub2 empty) + await tree.removeResources(["dir1/sub1/file1.js", "dir1/sub2/file2.js"]); + await registry.flush(); + + // Both subdirectories should be cleaned up + t.is(tree._findNode("dir1/sub1"), null, "sub1 should be removed"); + t.is(tree._findNode("dir1/sub2"), null, "sub2 should be removed"); + + // dir1 should also be removed since it's now empty + const dir1 = tree._findNode("dir1"); + t.is(dir1, null, "dir1 should be removed (now empty)"); + + // dir2 should be unaffected + t.truthy(tree.hasPath("dir2/file3.js"), "dir2/file3.js should still exist"); +}); + +// ============================================================================ +// Combined upsert and remove operations with Registry +// ============================================================================ + +test("upsertResources and removeResources - combined operations", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"} + ], registry); + const originalHash = tree.getRootHash(); + + // Schedule both operations + await tree.upsertResources([ + createMockResource("c.js", "hash-c", Date.now(), 1024, 3) + ]); + await tree.removeResources(["b.js"]); + + const result = await registry.flush(); + + t.true(result.added.includes("c.js"), "Should add c.js"); + t.true(result.removed.includes("b.js"), "Should remove b.js"); + + t.truthy(tree.hasPath("a.js"), "Tree should have a.js"); + t.false(tree.hasPath("b.js"), "Tree should not have b.js"); + t.truthy(tree.hasPath("c.js"), "Tree should have c.js"); + t.not(tree.getRootHash(), originalHash, "Root hash should change"); +}); + +test("upsertResources and removeResources - conflicting operations on same path", async (t) => { + const registry = new TreeRegistry(); + const tree = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + + // Schedule removal then upsert (upsert should win) + await tree.removeResources(["a.js"]); + await tree.upsertResources([ + createMockResource("a.js", "new-hash-a", Date.now(), 1024, 1) + ]); + + const result = await registry.flush(); + + // Upsert cancels removal + t.deepEqual(result.removed, [], "Should have no removals"); + t.true(result.updated.includes("a.js") || result.changed.includes("a.js"), "Should update or keep a.js"); + t.truthy(tree.hasPath("a.js"), "Tree should still have a.js"); +}); + +// ============================================================================ +// Per-Tree Statistics Tests +// ============================================================================ + +test("TreeRegistry - flush returns per-tree statistics", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + const tree2 = new SharedHashTree([{path: "b.js", integrity: "hash-b"}], registry); + + // Update tree1 resource + registry.scheduleUpsert(createMockResource("a.js", "new-hash-a", Date.now(), 1024, 1)); + // Add new resource - gets added to all trees + registry.scheduleUpsert(createMockResource("c.js", "hash-c", Date.now(), 2048, 2)); + + const result = await registry.flush(); + + // Verify global results + // a.js gets updated in tree1 but added to tree2 (didn't exist before) + t.true(result.updated.includes("a.js"), "Should report a.js as updated"); + t.true(result.added.includes("c.js"), "Should report c.js as added"); + t.true(result.added.includes("a.js"), "Should report a.js as added to tree2"); + + // Verify per-tree statistics + t.truthy(result.treeStats, "Should have treeStats"); + t.is(result.treeStats.size, 2, "Should have stats for both trees"); + + const stats1 = result.treeStats.get(tree1); + const stats2 = result.treeStats.get(tree2); + + t.truthy(stats1, "Should have stats for tree1"); + t.truthy(stats2, "Should have stats for tree2"); + + // Tree1: 1 update to a.js, 1 add for c.js + t.is(stats1.updated.length, 1, "Tree1 should have 1 update (a.js)"); + t.true(stats1.updated.includes("a.js"), "Tree1 should have a.js in updated"); + t.is(stats1.added.length, 1, "Tree1 should have 1 addition (c.js)"); + t.true(stats1.added.includes("c.js"), "Tree1 should have c.js in added"); + t.is(stats1.unchanged.length, 0, "Tree1 should have 0 unchanged"); + t.is(stats1.removed.length, 0, "Tree1 should have 0 removals"); + + // Tree2: 1 add for c.js, 1 add for a.js (didn't exist in tree2) + t.is(stats2.updated.length, 0, "Tree2 should have 0 updates"); + t.is(stats2.added.length, 2, "Tree2 should have 2 additions (a.js, c.js)"); + t.true(stats2.added.includes("a.js"), "Tree2 should have a.js in added"); + t.true(stats2.added.includes("c.js"), "Tree2 should have c.js in added"); + t.is(stats2.unchanged.length, 0, "Tree2 should have 0 unchanged"); + t.is(stats2.removed.length, 0, "Tree2 should have 0 removals"); +}); + +test("TreeRegistry - per-tree statistics with shared nodes", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "shared/a.js", integrity: "hash-a"}, + {path: "shared/b.js", integrity: "hash-b"} + ], registry); + const tree2 = tree1.deriveTree([{path: "unique/c.js", integrity: "hash-c"}]); + + // Verify trees share the "shared" directory + const sharedDir1 = tree1.root.children.get("shared"); + const sharedDir2 = tree2.root.children.get("shared"); + t.is(sharedDir1, sharedDir2, "Should share the same 'shared' directory node"); + + // Update shared resource + registry.scheduleUpsert(createMockResource("shared/a.js", "new-hash-a", Date.now(), 1024, 1)); + + const result = await registry.flush(); + + // Verify global results + t.deepEqual(result.updated, ["shared/a.js"], "Should report shared/a.js as updated"); + + // Verify per-tree statistics + const stats1 = result.treeStats.get(tree1); + const stats2 = result.treeStats.get(tree2); + + // Both trees should count the update since they share the node + t.is(stats1.updated.length, 1, "Tree1 should count the shared update"); + t.true(stats1.updated.includes("shared/a.js"), "Tree1 should have shared/a.js in updated"); + t.is(stats2.updated.length, 1, "Tree2 should count the shared update"); + t.true(stats2.updated.includes("shared/a.js"), "Tree2 should have shared/a.js in updated"); + t.is(stats1.added.length, 0, "Tree1 should have 0 additions"); + t.is(stats2.added.length, 0, "Tree2 should have 0 additions"); + t.is(stats1.unchanged.length, 0, "Tree1 should have 0 unchanged"); + t.is(stats2.unchanged.length, 0, "Tree2 should have 0 unchanged"); + t.is(stats1.removed.length, 0, "Tree1 should have 0 removals"); + t.is(stats2.removed.length, 0, "Tree2 should have 0 removals"); +}); + +test("TreeRegistry - per-tree statistics with mixed operations", async (t) => { + const registry = new TreeRegistry(); + const tree1 = new SharedHashTree([ + {path: "a.js", integrity: "hash-a"}, + {path: "b.js", integrity: "hash-b"}, + {path: "c.js", integrity: "hash-c"} + ], registry); + const tree2 = tree1.deriveTree([{path: "d.js", integrity: "hash-d"}]); + + // Update a.js (affects both trees - shared) + registry.scheduleUpsert(createMockResource("a.js", "new-hash-a", Date.now(), 1024, 1)); + // Remove b.js (affects both trees - shared) + registry.scheduleRemoval("b.js"); + // Add e.js (affects both trees) + registry.scheduleUpsert(createMockResource("e.js", "hash-e", Date.now(), 2048, 5)); + // Update d.js (exists in tree2, will be added to tree1) + registry.scheduleUpsert(createMockResource("d.js", "new-hash-d", Date.now(), 1024, 4)); + + const result = await registry.flush(); + + // Verify per-tree statistics + const stats1 = result.treeStats.get(tree1); + const stats2 = result.treeStats.get(tree2); + + // Tree1: 1 update (a.js), 2 additions (e.js, d.js), 1 removal (b.js) + t.is(stats1.updated.length, 1, "Tree1 should have 1 update (a.js)"); + t.true(stats1.updated.includes("a.js"), "Tree1 should have a.js in updated"); + t.is(stats1.added.length, 2, "Tree1 should have 2 additions (e.js, d.js)"); + t.true(stats1.added.includes("e.js"), "Tree1 should have e.js in added"); + t.true(stats1.added.includes("d.js"), "Tree1 should have d.js in added"); + t.is(stats1.unchanged.length, 0, "Tree1 should have 0 unchanged"); + t.is(stats1.removed.length, 1, "Tree1 should have 1 removal (b.js)"); + t.true(stats1.removed.includes("b.js"), "Tree1 should have b.js in removed"); + + // Tree2: 2 updates (a.js shared, d.js), 1 addition (e.js), 1 removal (b.js shared) + t.is(stats2.updated.length, 2, "Tree2 should have 2 updates (a.js, d.js)"); + t.true(stats2.updated.includes("a.js"), "Tree2 should have a.js in updated"); + t.true(stats2.updated.includes("d.js"), "Tree2 should have d.js in updated"); + t.is(stats2.added.length, 1, "Tree2 should have 1 addition (e.js)"); + t.true(stats2.added.includes("e.js"), "Tree2 should have e.js in added"); + t.is(stats2.unchanged.length, 0, "Tree2 should have 0 unchanged"); + t.is(stats2.removed.length, 1, "Tree2 should have 1 removal (b.js)"); + t.true(stats2.removed.includes("b.js"), "Tree2 should have b.js in removed"); +}); + +test("TreeRegistry - per-tree statistics with no changes", async (t) => { + const registry = new TreeRegistry(); + const timestamp = Date.now(); + const tree1 = new SharedHashTree([{ + path: "a.js", + integrity: "hash-a", + lastModified: timestamp, + size: 1024, + inode: 100 + }], registry); + const tree2 = new SharedHashTree([{ + path: "b.js", + integrity: "hash-b", + lastModified: timestamp, + size: 2048, + inode: 200 + }], registry); + + // Schedule updates with unchanged metadata + // Note: These will add missing resources to the other tree + registry.scheduleUpsert(createMockResource("a.js", "hash-a", timestamp, 1024, 100)); + registry.scheduleUpsert(createMockResource("b.js", "hash-b", timestamp, 2048, 200)); + + const result = await registry.flush(); + + // a.js is unchanged in tree1 but added to tree2 + // b.js is unchanged in tree2 but added to tree1 + t.deepEqual(result.updated, [], "Should have no updates"); + t.true(result.added.includes("a.js"), "a.js should be added to tree2"); + t.true(result.added.includes("b.js"), "b.js should be added to tree1"); + t.true(result.unchanged.includes("a.js"), "a.js should be unchanged in tree1"); + t.true(result.unchanged.includes("b.js"), "b.js should be unchanged in tree2"); + + // Verify per-tree statistics + const stats1 = result.treeStats.get(tree1); + const stats2 = result.treeStats.get(tree2); + + // Tree1: a.js unchanged, b.js added + t.is(stats1.updated.length, 0, "Tree1 should have 0 updates"); + t.is(stats1.added.length, 1, "Tree1 should have 1 addition (b.js)"); + t.true(stats1.added.includes("b.js"), "Tree1 should have b.js in added"); + t.is(stats1.unchanged.length, 1, "Tree1 should have 1 unchanged (a.js)"); + t.true(stats1.unchanged.includes("a.js"), "Tree1 should have a.js in unchanged"); + t.is(stats1.removed.length, 0, "Tree1 should have 0 removals"); + + // Tree2: b.js unchanged, a.js added + t.is(stats2.updated.length, 0, "Tree2 should have 0 updates"); + t.is(stats2.added.length, 1, "Tree2 should have 1 addition (a.js)"); + t.true(stats2.added.includes("a.js"), "Tree2 should have a.js in added"); + t.is(stats2.unchanged.length, 1, "Tree2 should have 1 unchanged (b.js)"); + t.true(stats2.unchanged.includes("b.js"), "Tree2 should have b.js in unchanged"); + t.is(stats2.removed.length, 0, "Tree2 should have 0 removals"); +}); + +test("TreeRegistry - empty flush returns empty treeStats", async (t) => { + const registry = new TreeRegistry(); + new SharedHashTree([{path: "a.js", integrity: "hash-a"}], registry); + new SharedHashTree([{path: "b.js", integrity: "hash-b"}], registry); + + // Flush without scheduling any operations + const result = await registry.flush(); + + t.truthy(result.treeStats, "Should have treeStats"); + t.is(result.treeStats.size, 0, "Should have empty treeStats when no operations"); + t.deepEqual(result.added, [], "Should have no additions"); + t.deepEqual(result.updated, [], "Should have no updates"); + t.deepEqual(result.removed, [], "Should have no removals"); +}); + +test("TreeRegistry - derived tree reflects base tree resource changes in statistics", async (t) => { + const registry = new TreeRegistry(); + + // Create base tree with some resources + const baseTree = new SharedHashTree([ + {path: "shared/resource1.js", integrity: "hash1"}, + {path: "shared/resource2.js", integrity: "hash2"} + ], registry); + + // Derive a new tree from base tree (shares same registry) + // Note: deriveTree doesn't schedule the new resources, it adds them directly to the derived tree + const derivedTree = baseTree.deriveTree([ + {path: "derived/resource3.js", integrity: "hash3"} + ]); + + // Verify both trees are registered + t.is(registry.getTreeCount(), 2, "Registry should have both trees"); + + // Verify they share the same nodes + const sharedDir1 = baseTree.root.children.get("shared"); + const sharedDir2 = derivedTree.root.children.get("shared"); + t.is(sharedDir1, sharedDir2, "Both trees should share the 'shared' directory node"); + + // Update a resource that exists in base tree (and is shared with derived tree) + registry.scheduleUpsert(createMockResource("shared/resource1.js", "new-hash1", Date.now(), 2048, 100)); + + // Add a new resource to the shared path + registry.scheduleUpsert(createMockResource("shared/resource4.js", "hash4", Date.now(), 1024, 200)); + + // Remove a shared resource + registry.scheduleRemoval("shared/resource2.js"); + + const result = await registry.flush(); + + // Verify global results + t.deepEqual(result.updated, ["shared/resource1.js"], "Should report resource1 as updated"); + t.true(result.added.includes("shared/resource4.js"), "Should report resource4 as added"); + t.deepEqual(result.removed, ["shared/resource2.js"], "Should report resource2 as removed"); + + // Verify per-tree statistics + const baseStats = result.treeStats.get(baseTree); + const derivedStats = result.treeStats.get(derivedTree); + + // Base tree statistics + // Base tree will also get derived/resource3.js added via registry (since it processes all trees) + t.is(baseStats.updated.length, 1, "Base tree should have 1 update"); + t.true(baseStats.updated.includes("shared/resource1.js"), "Base tree should have resource1 in updated"); + // baseStats.added should include both resource4 and resource3 + t.true(baseStats.added.includes("shared/resource4.js"), "Base tree should have resource4 in added"); + t.is(baseStats.removed.length, 1, "Base tree should have 1 removal"); + t.true(baseStats.removed.includes("shared/resource2.js"), "Base tree should have resource2 in removed"); + + // Derived tree statistics - CRITICAL: should reflect the same changes for shared resources + // Note: resource4 shows as "updated" because it's added to an already-existing shared node that was modified + t.is(derivedStats.updated.length, 2, + "Derived tree should have 2 updates (resource1 changed, resource4 added to shared dir)"); + t.true(derivedStats.updated.includes("shared/resource1.js"), "Derived tree should have resource1 in updated"); + t.true(derivedStats.updated.includes("shared/resource4.js"), "Derived tree should have resource4 in updated"); + t.is(derivedStats.added.length, 0, "Derived tree should have 0 additions tracked separately"); + t.is(derivedStats.removed.length, 1, "Derived tree should have 1 removal (shared resource2)"); + t.true(derivedStats.removed.includes("shared/resource2.js"), "Derived tree should have resource2 in removed"); + + // Verify the actual tree state + t.is(baseTree.getResourceByPath("shared/resource1.js").integrity, "new-hash1", + "Base tree should have updated integrity"); + t.is(derivedTree.getResourceByPath("shared/resource1.js").integrity, "new-hash1", + "Derived tree should have updated integrity (shared node)"); + t.truthy(baseTree.hasPath("shared/resource4.js"), "Base tree should have new resource"); + t.truthy(derivedTree.hasPath("shared/resource4.js"), "Derived tree should have new resource (shared)"); + t.false(baseTree.hasPath("shared/resource2.js"), "Base tree should not have removed resource"); + t.false(derivedTree.hasPath("shared/resource2.js"), "Derived tree should not have removed resource (shared)"); +}); diff --git a/packages/project/test/lib/build/definitions/application.js b/packages/project/test/lib/build/definitions/application.js index 742d398e988..cc6fb8eabee 100644 --- a/packages/project/test/lib/build/definitions/application.js +++ b/packages/project/test/lib/build/definitions/application.js @@ -57,12 +57,14 @@ test("Standard build", (t) => { replaceCopyright: { options: { copyright: "copyright", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, minify: { options: { @@ -70,7 +72,8 @@ test("Standard build", (t) => { "/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, enhanceManifest: {}, generateFlexChangesBundle: {}, @@ -137,12 +140,14 @@ test("Standard build with legacy spec version", (t) => { replaceCopyright: { options: { copyright: "copyright", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, minify: { options: { @@ -150,7 +155,8 @@ test("Standard build with legacy spec version", (t) => { "/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, enhanceManifest: {}, generateFlexChangesBundle: {}, @@ -250,12 +256,14 @@ test("Custom bundles", async (t) => { replaceCopyright: { options: { copyright: "copyright", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, minify: { options: { @@ -263,7 +271,8 @@ test("Custom bundles", async (t) => { "/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, enhanceManifest: {}, generateFlexChangesBundle: {}, @@ -396,7 +405,8 @@ test("Minification excludes", (t) => { "!**/*.support.js", "!/resources/**.html", ] - } + }, + supportsDifferentialBuilds: true, }, "Correct minify task definition"); }); @@ -421,7 +431,8 @@ test("Minification excludes not applied for legacy specVersion", (t) => { "/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, "Correct minify task definition"); }); diff --git a/packages/project/test/lib/build/definitions/component.js b/packages/project/test/lib/build/definitions/component.js index abb281a86ed..1e773369234 100644 --- a/packages/project/test/lib/build/definitions/component.js +++ b/packages/project/test/lib/build/definitions/component.js @@ -56,12 +56,14 @@ test("Standard build", (t) => { replaceCopyright: { options: { copyright: "copyright", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, minify: { options: { @@ -69,7 +71,8 @@ test("Standard build", (t) => { "/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, enhanceManifest: {}, generateFlexChangesBundle: {}, @@ -162,12 +165,14 @@ test("Custom bundles", async (t) => { replaceCopyright: { options: { copyright: "copyright", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json}" - } + }, + supportsDifferentialBuilds: true, }, minify: { options: { @@ -175,7 +180,8 @@ test("Custom bundles", async (t) => { "/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, enhanceManifest: {}, generateFlexChangesBundle: {}, @@ -301,7 +307,8 @@ test("Minification excludes", (t) => { "!**/*.support.js", "!/resources/**.html", ] - } + }, + supportsDifferentialBuilds: true, }, "Correct minify task definition"); }); diff --git a/packages/project/test/lib/build/definitions/library.js b/packages/project/test/lib/build/definitions/library.js index 121e8951442..3914be256da 100644 --- a/packages/project/test/lib/build/definitions/library.js +++ b/packages/project/test/lib/build/definitions/library.js @@ -68,18 +68,21 @@ test("Standard build", async (t) => { options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceBuildtime: { options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" - } + }, + supportsDifferentialBuilds: true, }, generateJsdoc: { requiresDependencies: true, @@ -97,7 +100,8 @@ test("Standard build", async (t) => { "/resources/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, generateLibraryManifest: {}, enhanceManifest: {}, @@ -205,18 +209,21 @@ test("Standard build with legacy spec version", (t) => { options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceBuildtime: { options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" - } + }, + supportsDifferentialBuilds: true, }, generateJsdoc: { requiresDependencies: true, @@ -234,7 +241,8 @@ test("Standard build with legacy spec version", (t) => { "/resources/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, generateLibraryManifest: {}, enhanceManifest: {}, @@ -331,18 +339,21 @@ test("Custom bundles", async (t) => { options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceBuildtime: { options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" - } + }, + supportsDifferentialBuilds: true, }, generateJsdoc: { requiresDependencies: true, @@ -360,7 +371,8 @@ test("Custom bundles", async (t) => { "/resources/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, generateLibraryManifest: {}, enhanceManifest: {}, @@ -489,7 +501,8 @@ test("Minification excludes", (t) => { "!**/*.support.js", "!/resources/**.html", ] - } + }, + supportsDifferentialBuilds: true, }, "Correct minify task definition"); }); @@ -514,7 +527,8 @@ test("Minification excludes not applied for legacy specVersion", (t) => { "/resources/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, "Correct minify task definition"); }); @@ -675,18 +689,21 @@ test("Standard build: nulled taskFunction to skip tasks", (t) => { options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" - } + }, + supportsDifferentialBuilds: true, }, replaceBuildtime: { options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" - } + }, + supportsDifferentialBuilds: true, }, generateJsdoc: { requiresDependencies: true, @@ -704,7 +721,8 @@ test("Standard build: nulled taskFunction to skip tasks", (t) => { "/resources/**/*.js", "!**/*.support.js", ] - } + }, + supportsDifferentialBuilds: true, }, generateLibraryManifest: {}, enhanceManifest: {}, diff --git a/packages/project/test/lib/build/definitions/themeLibrary.js b/packages/project/test/lib/build/definitions/themeLibrary.js index 2da2457b538..9d35d11a174 100644 --- a/packages/project/test/lib/build/definitions/themeLibrary.js +++ b/packages/project/test/lib/build/definitions/themeLibrary.js @@ -53,13 +53,15 @@ test("Standard build", (t) => { options: { copyright: "copyright", pattern: "/resources/**/*.{less,theme}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/resources/**/*.{less,theme}" - } + }, + supportsDifferentialBuilds: true, }, buildThemes: { requiresDependencies: true, @@ -114,13 +116,15 @@ test("Standard build for non root project", (t) => { options: { copyright: "copyright", pattern: "/resources/**/*.{less,theme}" - } + }, + supportsDifferentialBuilds: true, }, replaceVersion: { options: { version: "version", pattern: "/resources/**/*.{less,theme}" - } + }, + supportsDifferentialBuilds: true, }, buildThemes: { requiresDependencies: true, diff --git a/packages/project/test/lib/build/helpers/BuildContext.js b/packages/project/test/lib/build/helpers/BuildContext.js index cc09a7cd870..7cfbfea5ed8 100644 --- a/packages/project/test/lib/build/helpers/BuildContext.js +++ b/packages/project/test/lib/build/helpers/BuildContext.js @@ -1,14 +1,29 @@ import test from "ava"; import sinon from "sinon"; +import esmock from "esmock"; import OutputStyleEnum from "../../../../lib/build/helpers/ProjectBuilderOutputStyle.js"; +test.beforeEach(async (t) => { + t.context.ProjectBuildContextCreateStub = sinon.stub().callsFake(async () => { + return {}; // Explicitly returning empty object to show uniqueness + }); + t.context.CacheManagerCreate = sinon.stub().returns({}); + t.context.BuildContext = await esmock("../../../../lib/build/helpers/BuildContext.js", { + "../../../../lib/build/helpers/ProjectBuildContext.js": { + create: t.context.ProjectBuildContextCreateStub + }, + "../../../../lib/build/cache/CacheManager.js": { + create: t.context.CacheManagerCreate + } + }); +}); + test.afterEach.always((t) => { sinon.restore(); }); -import BuildContext from "../../../../lib/build/helpers/BuildContext.js"; - test("Missing parameters", (t) => { + const {BuildContext} = t.context; const error1 = t.throws(() => { new BuildContext(); }); @@ -23,6 +38,8 @@ test("Missing parameters", (t) => { }); test("getRootProject", (t) => { + const {BuildContext} = t.context; + const rootProjectStub = sinon.stub() .onFirstCall().returns({getType: () => "library"}) .returns("pony"); @@ -33,6 +50,8 @@ test("getRootProject", (t) => { }); test("getGraph", (t) => { + const {BuildContext} = t.context; + const graph = { getRoot: () => ({getType: () => "library"}), }; @@ -42,6 +61,8 @@ test("getGraph", (t) => { }); test("getTaskRepository", (t) => { + const {BuildContext} = t.context; + const graph = { getRoot: () => ({getType: () => "library"}), }; @@ -51,6 +72,8 @@ test("getTaskRepository", (t) => { }); test("getBuildConfig: Default values", (t) => { + const {BuildContext} = t.context; + const graph = { getRoot: () => ({getType: () => "library"}), }; @@ -68,6 +91,8 @@ test("getBuildConfig: Default values", (t) => { }); test("getBuildConfig: Custom values", (t) => { + const {BuildContext} = t.context; + const buildContext = new BuildContext({ getRoot: () => { return { @@ -96,6 +121,8 @@ test("getBuildConfig: Custom values", (t) => { }); test("createBuildManifest not supported for type application", (t) => { + const {BuildContext} = t.context; + const err = t.throws(() => { new BuildContext({ getRoot: () => { @@ -113,6 +140,8 @@ test("createBuildManifest not supported for type application", (t) => { }); test("createBuildManifest not supported for type module", (t) => { + const {BuildContext} = t.context; + const err = t.throws(() => { new BuildContext({ getRoot: () => { @@ -130,6 +159,8 @@ test("createBuildManifest not supported for type module", (t) => { }); test("createBuildManifest not supported for self-contained build", (t) => { + const {BuildContext} = t.context; + const err = t.throws(() => { new BuildContext({ getRoot: () => { @@ -148,6 +179,8 @@ test("createBuildManifest not supported for self-contained build", (t) => { }); test("createBuildManifest supported for css-variables build", (t) => { + const {BuildContext} = t.context; + t.notThrows(() => { new BuildContext({ getRoot: () => { @@ -163,6 +196,8 @@ test("createBuildManifest supported for css-variables build", (t) => { }); test("createBuildManifest supported for jsdoc build", (t) => { + const {BuildContext} = t.context; + t.notThrows(() => { new BuildContext({ getRoot: () => { @@ -178,6 +213,8 @@ test("createBuildManifest supported for jsdoc build", (t) => { }); test("outputStyle='Namespace' supported for type application", (t) => { + const {BuildContext} = t.context; + t.notThrows(() => { new BuildContext({ getRoot: () => { @@ -192,6 +229,8 @@ test("outputStyle='Namespace' supported for type application", (t) => { }); test("outputStyle='Flat' not supported for type theme-library", (t) => { + const {BuildContext} = t.context; + const err = t.throws(() => { new BuildContext({ getRoot: () => { @@ -210,6 +249,8 @@ test("outputStyle='Flat' not supported for type theme-library", (t) => { }); test("outputStyle='Flat' not supported for type module", (t) => { + const {BuildContext} = t.context; + const err = t.throws(() => { new BuildContext({ getRoot: () => { @@ -228,6 +269,8 @@ test("outputStyle='Flat' not supported for type module", (t) => { }); test("outputStyle='Flat' not supported for createBuildManifest build", (t) => { + const {BuildContext} = t.context; + const err = t.throws(() => { new BuildContext({ getRoot: () => { @@ -246,6 +289,8 @@ test("outputStyle='Flat' not supported for createBuildManifest build", (t) => { }); test("getOption", (t) => { + const {BuildContext} = t.context; + const graph = { getRoot: () => ({getType: () => "library"}), }; @@ -260,23 +305,25 @@ test("getOption", (t) => { "(not exposed as build option)"); }); -test("createProjectContext", async (t) => { - const graph = { - getRoot: () => ({getType: () => "library"}), - }; +test("getProjectContext", async (t) => { + const {BuildContext} = t.context; + + const rootProjectStub = sinon.stub() + .returns({getType: () => "library", getRootPath: () => ""}); + const graph = {getRoot: rootProjectStub, getProject: () => "project"}; + const buildContext = new BuildContext(graph, "taskRepository"); - const projectBuildContext = await buildContext.createProjectContext({ - project: { - getName: () => "project", - getType: () => "type", - }, - }); + const projectBuildContext = await buildContext.getProjectContext("project"); + t.is(t.context.ProjectBuildContextCreateStub.callCount, 1); - t.deepEqual(buildContext._projectBuildContexts, [projectBuildContext], - "Project build context has been added to internal array"); + const projectBuildContext2 = await buildContext.getProjectContext("project"); + t.is(t.context.ProjectBuildContextCreateStub.callCount, 1); + t.is(projectBuildContext, projectBuildContext2); }); test("executeCleanupTasks", async (t) => { + const {BuildContext} = t.context; + const graph = { getRoot: () => ({getType: () => "library"}), }; @@ -284,12 +331,8 @@ test("executeCleanupTasks", async (t) => { const executeCleanupTasks = sinon.stub().resolves(); - buildContext._projectBuildContexts.push({ - executeCleanupTasks - }); - buildContext._projectBuildContexts.push({ - executeCleanupTasks - }); + buildContext._projectBuildContexts.set("project", {executeCleanupTasks}); + buildContext._projectBuildContexts.set("project2", {executeCleanupTasks}); await buildContext.executeCleanupTasks(); diff --git a/packages/project/test/lib/build/helpers/ProjectBuildContext.js b/packages/project/test/lib/build/helpers/ProjectBuildContext.js index 03f9a568325..16310e07119 100644 --- a/packages/project/test/lib/build/helpers/ProjectBuildContext.js +++ b/packages/project/test/lib/build/helpers/ProjectBuildContext.js @@ -16,20 +16,22 @@ import ProjectBuildContext from "../../../../lib/build/helpers/ProjectBuildConte test("Missing parameters", (t) => { t.throws(() => { - new ProjectBuildContext({ - project: { + new ProjectBuildContext( + undefined, + { getName: () => "project", getType: () => "type", - }, - }); + } + ); }, { message: `Missing parameter 'buildContext'` }, "Correct error message"); t.throws(() => { - new ProjectBuildContext({ - buildContext: "buildContext", - }); + new ProjectBuildContext( + "buildContext", + undefined + ); }, { message: `Missing parameter 'project'` }, "Correct error message"); @@ -40,54 +42,61 @@ test("isRootProject: true", (t) => { getName: () => "root project", getType: () => "type", }; - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getRootProject: () => rootProject - }, - project: rootProject - }); + const buildContext = { + getRootProject: () => rootProject + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + rootProject + ); t.true(projectBuildContext.isRootProject(), "Correctly identified root project"); }); test("isRootProject: false", (t) => { - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getRootProject: () => "root project" - }, - project: { - getName: () => "not the root project", - getType: () => "type", - } - }); + const buildContext = { + getRootProject: () => "root project" + }; + const project = { + getName: () => "not the root project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project + ); t.false(projectBuildContext.isRootProject(), "Correctly identified non-root project"); }); test("getBuildOption", (t) => { const getOptionStub = sinon.stub().returns("pony"); - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getOption: getOptionStub - }, - project: { - getName: () => "project", - getType: () => "type", - } - }); + const buildContext = { + getOption: getOptionStub + }; + const project = { + getName: () => "project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project + ); t.is(projectBuildContext.getOption("option"), "pony", "Returned value is correct"); t.is(getOptionStub.getCall(0).args[0], "option", "getOption called with correct argument"); }); test("registerCleanupTask", (t) => { - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project: { - getName: () => "project", - getType: () => "type", - } - }); + const buildContext = {}; + const project = { + getName: () => "project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project + ); projectBuildContext.registerCleanupTask("my task 1"); projectBuildContext.registerCleanupTask("my task 2"); @@ -96,13 +105,15 @@ test("registerCleanupTask", (t) => { }); test("executeCleanupTasks", (t) => { - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project: { - getName: () => "project", - getType: () => "type", - } - }); + const buildContext = {}; + const project = { + getName: () => "project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project + ); const task1 = sinon.stub().resolves(); const task2 = sinon.stub().resolves(); projectBuildContext.registerCleanupTask(task1); @@ -115,65 +126,35 @@ test("executeCleanupTasks", (t) => { }); test.serial("getResourceTagCollection", async (t) => { - const projectAcceptsTagStub = sinon.stub().returns(false); - projectAcceptsTagStub.withArgs("project-tag").returns(true); - const projectContextAcceptsTagStub = sinon.stub().returns(false); - projectContextAcceptsTagStub.withArgs("project-context-tag").returns(true); - - class DummyResourceTagCollection { - constructor({allowedTags, allowedNamespaces}) { - t.deepEqual(allowedTags, [ - "ui5:OmitFromBuildResult", - "ui5:IsBundle" - ], - "Correct allowedTags parameter supplied"); - - t.deepEqual(allowedNamespaces, [ - "build" - ], - "Correct allowedNamespaces parameter supplied"); - } - acceptsTag(tag) { - // Redirect to stub - return projectContextAcceptsTagStub(tag); - } - } - - const ProjectBuildContext = await esmock("../../../../lib/build/helpers/ProjectBuildContext.js", { - "@ui5/fs/internal/ResourceTagCollection": DummyResourceTagCollection - }); - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project: { - getName: () => "project", - getType: () => "type", - } - }); + const ProjectBuildContext = (await import("../../../../lib/build/helpers/ProjectBuildContext.js")).default; + const buildContext = {}; + const project = { + getName: () => "project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project + ); - const fakeProjectCollection = { - acceptsTag: projectAcceptsTagStub + const fakeCollection = { + acceptsTag: sinon.stub().returns(true) }; + const getResourceTagCollectionStub = sinon.stub().returns(fakeCollection); const fakeResource = { getProject: () => { return { - getResourceTagCollection: () => fakeProjectCollection + getResourceTagCollection: getResourceTagCollectionStub }; }, getPath: () => "/resource/path", hasProject: () => true }; - const collection1 = projectBuildContext.getResourceTagCollection(fakeResource, "project-tag"); - t.is(collection1, fakeProjectCollection, "Returned tag collection of resource project"); - - const collection2 = projectBuildContext.getResourceTagCollection(fakeResource, "project-context-tag"); - t.true(collection2 instanceof DummyResourceTagCollection, - "Returned tag collection of project build context"); - - t.throws(() => { - projectBuildContext.getResourceTagCollection(fakeResource, "not-accepted-tag"); - }, { - message: `Could not find collection for resource /resource/path and tag not-accepted-tag` - }); + const collection = projectBuildContext.getResourceTagCollection(fakeResource, "some-tag"); + t.is(collection, fakeCollection, "Returned tag collection from resource's project"); + t.is(getResourceTagCollectionStub.callCount, 1, "getResourceTagCollection called once"); + t.is(getResourceTagCollectionStub.firstCall.args[0], fakeResource, "Called with resource"); + t.is(getResourceTagCollectionStub.firstCall.args[1], "some-tag", "Called with tag"); }); test("getResourceTagCollection: Assigns project to resource if necessary", (t) => { @@ -181,13 +162,11 @@ test("getResourceTagCollection: Assigns project to resource if necessary", (t) = getName: () => "project", getType: () => "type", }; - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project: fakeProject, - log: { - silly: () => {} - } - }); + const buildContext = {}; + const projectBuildContext = new ProjectBuildContext( + buildContext, + fakeProject + ); const setProjectStub = sinon.stub(); const fakeResource = { @@ -215,16 +194,17 @@ test("getProject", (t) => { getType: () => "type", }; const getProjectStub = sinon.stub().returns("pony"); - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getGraph: () => { - return { - getProject: getProjectStub - }; - } - }, + const buildContext = { + getGraph: () => { + return { + getProject: getProjectStub + }; + } + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); + ); t.is(projectBuildContext.getProject("pony project"), "pony", "Returned correct value"); t.is(getProjectStub.callCount, 1, "ProjectGraph#getProject got called once"); @@ -240,16 +220,17 @@ test("getProject: No name provided", (t) => { getType: () => "type", }; const getProjectStub = sinon.stub().returns("pony"); - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getGraph: () => { - return { - getProject: getProjectStub - }; - } - }, + const buildContext = { + getGraph: () => { + return { + getProject: getProjectStub + }; + } + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); + ); t.is(projectBuildContext.getProject(), project, "Returned correct value"); t.is(getProjectStub.callCount, 0, "ProjectGraph#getProject has not been called"); @@ -261,16 +242,17 @@ test("getDependencies", (t) => { getType: () => "type", }; const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]); - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getGraph: () => { - return { - getDependencies: getDependenciesStub - }; - } - }, + const buildContext = { + getGraph: () => { + return { + getDependencies: getDependenciesStub + }; + } + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); + ); t.deepEqual(projectBuildContext.getDependencies("pony project"), ["dep a", "dep b"], "Returned correct value"); t.is(getDependenciesStub.callCount, 1, "ProjectGraph#getDependencies got called once"); @@ -284,16 +266,17 @@ test("getDependencies: No name provided", (t) => { getType: () => "type", }; const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]); - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getGraph: () => { - return { - getDependencies: getDependenciesStub - }; - } - }, + const buildContext = { + getGraph: () => { + return { + getDependencies: getDependenciesStub + }; + } + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); + ); t.deepEqual(projectBuildContext.getDependencies(), ["dep a", "dep b"], "Returned correct value"); t.is(getDependenciesStub.callCount, 1, "ProjectGraph#getDependencies got called once"); @@ -302,20 +285,22 @@ test("getDependencies: No name provided", (t) => { }); test("getTaskUtil", (t) => { - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project: { - getName: () => "project", - getType: () => "type", - } - }); + const buildContext = {}; + const project = { + getName: () => "project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project + ); t.truthy(projectBuildContext.getTaskUtil(), "Returned a TaskUtil instance"); t.is(projectBuildContext.getTaskUtil(), projectBuildContext.getTaskUtil(), "Caches TaskUtil instance"); }); test.serial("getTaskRunner", async (t) => { - t.plan(3); + t.plan(4); const project = { getName: () => "project", getType: () => "type", @@ -325,10 +310,14 @@ test.serial("getTaskRunner", async (t) => { constructor(params) { t.true(params.log instanceof ProjectBuildLogger, "TaskRunner receives an instance of ProjectBuildLogger"); params.log = "log"; // replace log instance with string for deep comparison + t.is(params.buildCache, buildCache, + "TaskRunner receives the ProjectBuildCache instance"); + params.buildCache = "buildCache"; // replace buildCache instance with string for deep comparison t.deepEqual(params, { graph: "graph", project: project, log: "log", + buildCache: "buildCache", taskUtil: "taskUtil", taskRepository: "taskRepository", buildConfig: "buildConfig" @@ -339,14 +328,18 @@ test.serial("getTaskRunner", async (t) => { "../../../../lib/build/TaskRunner.js": TaskRunnerMock }); - const projectBuildContext = new ProjectBuildContext({ - buildContext: { - getGraph: () => "graph", - getTaskRepository: () => "taskRepository", - getBuildConfig: () => "buildConfig", - }, - project - }); + const buildContext = { + getGraph: () => "graph", + getTaskRepository: () => "taskRepository", + getBuildConfig: () => "buildConfig", + }; + const buildCache = {}; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project, + undefined, + buildCache, + ); projectBuildContext.getTaskUtil = () => "taskUtil"; @@ -354,76 +347,42 @@ test.serial("getTaskRunner", async (t) => { t.is(projectBuildContext.getTaskRunner(), taskRunner, "Returns cached TaskRunner instance"); }); - -test.serial("createProjectContext", async (t) => { - t.plan(4); - - const project = { - getName: sinon.stub().returns("foo"), - getType: sinon.stub().returns("bar"), - }; - const taskRunner = {"task": "runner"}; - class ProjectContextMock { - constructor({buildContext, project}) { - t.is(buildContext, testBuildContext, "Correct buildContext parameter"); - t.is(project, project, "Correct project parameter"); - } - getTaskUtil() { - return "taskUtil"; - } - setTaskRunner(_taskRunner) { - t.is(_taskRunner, taskRunner); - } - } - const BuildContext = await esmock("../../../../lib/build/helpers/BuildContext.js", { - "../../../../lib/build/helpers/ProjectBuildContext.js": ProjectContextMock, - "../../../../lib/build/TaskRunner.js": { - create: sinon.stub().resolves(taskRunner) - } - }); - const graph = { - getRoot: () => ({getType: () => "library"}), - }; - const testBuildContext = new BuildContext(graph, "taskRepository"); - - const projectContext = await testBuildContext.createProjectContext({ - project - }); - - t.true(projectContext instanceof ProjectContextMock, - "Project context is an instance of ProjectContextMock"); - t.is(testBuildContext._projectBuildContexts[0], projectContext, - "BuildContext stored correct ProjectBuildContext"); -}); - -test("requiresBuild: has no build-manifest", (t) => { +test("possiblyRequiresBuild: has no build-manifest", (t) => { const project = { getName: sinon.stub().returns("foo"), getType: sinon.stub().returns("bar"), getBuildManifest: () => null }; - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project - }); - t.true(projectBuildContext.requiresBuild(), "Project without build-manifest requires to be build"); + const buildContext = {}; + const buildCache = { + isFresh: sinon.stub().returns(false) + }; + const projectBuildContext = new ProjectBuildContext( + buildContext, + project, + undefined, + buildCache + ); + t.true(projectBuildContext.possiblyRequiresBuild(), "Project without build-manifest requires to be build"); }); -test("requiresBuild: has build-manifest", (t) => { +test("possiblyRequiresBuild: has build-manifest", (t) => { const project = { getName: sinon.stub().returns("foo"), getType: sinon.stub().returns("bar"), getBuildManifest: () => { return { + manifestVersion: "0.1", timestamp: "2022-07-28T12:00:00.000Z" }; } }; - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, + const buildContext = {}; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); - t.false(projectBuildContext.requiresBuild(), "Project with build-manifest does not require to be build"); + ); + t.false(projectBuildContext.possiblyRequiresBuild(), "Project with build-manifest does not require to be build"); }); test.serial("getBuildMetadata", (t) => { @@ -432,15 +391,17 @@ test.serial("getBuildMetadata", (t) => { getType: sinon.stub().returns("bar"), getBuildManifest: () => { return { + manifestVersion: "0.1", timestamp: "2022-07-28T12:00:00.000Z" }; } }; const getTimeStub = sinon.stub(Date.prototype, "getTime").callThrough().onFirstCall().returns(1659016800000); - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, + const buildContext = {}; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); + ); t.deepEqual(projectBuildContext.getBuildMetadata(), { timestamp: "2022-07-28T12:00:00.000Z", @@ -455,9 +416,10 @@ test("getBuildMetadata: has no build-manifest", (t) => { getType: sinon.stub().returns("bar"), getBuildManifest: () => null }; - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, + const buildContext = {}; + const projectBuildContext = new ProjectBuildContext( + buildContext, project - }); + ); t.is(projectBuildContext.getBuildMetadata(), null, "Project has no build manifest"); }); diff --git a/packages/project/test/lib/build/helpers/composeProjectList.js b/packages/project/test/lib/build/helpers/composeProjectList.js index f8f58185f38..8556efcdfbb 100644 --- a/packages/project/test/lib/build/helpers/composeProjectList.js +++ b/packages/project/test/lib/build/helpers/composeProjectList.js @@ -224,9 +224,9 @@ test.serial("createDependencyLists: include all", async (t) => { excludeDependencyRegExp: [], excludeDependencyTree: [], expectedIncludedDependencies: [ - "library.d", "library.b", "library.c", - "library.d-depender", "library.a", "library.g", - "library.e", "library.f" + "library.d", "library.b", "library.a", + "library.e", "library.c", "library.f", + "library.d-depender", "library.g" ], expectedExcludedDependencies: [] }); @@ -239,7 +239,7 @@ test.serial("createDependencyLists: includeDependencyTree has lower priority tha excludeDependency: ["library.f"], excludeDependencyRegExp: ["^library\\.[acd]$"], expectedIncludedDependencies: ["library.b"], - expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.a"] + expectedExcludedDependencies: ["library.f", "library.d", "library.a", "library.c"] }); }); @@ -249,7 +249,7 @@ test.serial("createDependencyLists: excludeDependencyTree has lower priority tha includeDependency: ["library.f"], includeDependencyRegExp: ["^library\\.[acd]$"], excludeDependencyTree: ["library.f"], - expectedIncludedDependencies: ["library.f", "library.d", "library.c", "library.a"], + expectedIncludedDependencies: ["library.f", "library.d", "library.a", "library.c"], expectedExcludedDependencies: ["library.b"] }); }); @@ -261,8 +261,8 @@ test.serial("createDependencyLists: include all, exclude tree and include single includeDependencyRegExp: ["^library\\.[acd]$"], excludeDependencyTree: ["library.f"], expectedIncludedDependencies: [ - "library.f", "library.d", "library.c", "library.a", "library.d-depender", - "library.g", "library.e" + "library.f", "library.d", "library.a", "library.c", "library.e", + "library.d-depender", "library.g" ], expectedExcludedDependencies: ["library.b"] }); @@ -287,7 +287,7 @@ test.serial("createDependencyLists: defaultIncludeDependency/RegExp has lower pr excludeDependency: ["library.f"], excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"], expectedIncludedDependencies: ["library.b"], - expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"] + expectedExcludedDependencies: ["library.f", "library.d", "library.a", "library.c", "library.d-depender"] }); }); test.serial("createDependencyLists: include all and defaultIncludeDependency/RegExp", async (t) => { @@ -297,8 +297,8 @@ test.serial("createDependencyLists: include all and defaultIncludeDependency/Reg defaultIncludeDependencyRegExp: ["^library\\.d$"], excludeDependency: ["library.f"], excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"], - expectedIncludedDependencies: ["library.b", "library.g", "library.e"], - expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"] + expectedIncludedDependencies: ["library.b", "library.e", "library.g"], + expectedExcludedDependencies: ["library.f", "library.d", "library.a", "library.c", "library.d-depender"] }); }); diff --git a/packages/project/test/lib/build/helpers/createBuildManifest.integration.js b/packages/project/test/lib/build/helpers/createBuildManifest.integration.js index 015ca68bd1d..c6b49792b52 100644 --- a/packages/project/test/lib/build/helpers/createBuildManifest.integration.js +++ b/packages/project/test/lib/build/helpers/createBuildManifest.integration.js @@ -45,13 +45,13 @@ const buildConfig = { test("Create project from application project providing a build manifest", async (t) => { const inputProject = await Specification.create(applicationAConfig); - inputProject.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + inputProject.getProjectResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); const taskRepository = { getVersions: async () => ({a: "a", b: "b"}) }; - const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository); + const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository, "yyy"); const m = new Module({ id: "build-descr-application.a.id", version: "2.0.0", @@ -63,7 +63,7 @@ test("Create project from application project providing a build manifest", async t.truthy(project, "Module was able to create project from build manifest metadata"); t.is(project.getName(), project.getName(), "Archive project has correct name"); t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); - t.is(project.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, + t.is(project.getProjectResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, "Archive project has correct tag"); t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module"); @@ -77,13 +77,13 @@ test("Create project from application project providing a build manifest", async test("Create project from library project providing a build manifest", async (t) => { const inputProject = await Specification.create(libraryEConfig); - inputProject.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); + inputProject.getProjectResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); const taskRepository = { getVersions: async () => ({a: "a", b: "b"}) }; - const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository); + const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository, "zzz"); const m = new Module({ id: "build-descr-library.e.id", version: "2.0.0", @@ -95,7 +95,7 @@ test("Create project from library project providing a build manifest", async (t) t.truthy(project, "Module was able to create project from build manifest metadata"); t.is(project.getName(), project.getName(), "Archive project has correct name"); t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); - t.is(project.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, + t.is(project.getProjectResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, "Archive project has correct tag"); t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module"); diff --git a/packages/project/test/lib/build/helpers/createBuildManifest.js b/packages/project/test/lib/build/helpers/createBuildManifest.js index 7b2266b8897..e62b41a5e36 100644 --- a/packages/project/test/lib/build/helpers/createBuildManifest.js +++ b/packages/project/test/lib/build/helpers/createBuildManifest.js @@ -64,15 +64,26 @@ test("Missing parameter: taskRepository", async (t) => { }); }); +test("Missing parameter: signature", async (t) => { + const project = await Specification.create(applicationProjectInput); + + const taskRepository = { + getVersions: async () => ({builderVersion: "", fsVersion: ""}) + }; + await t.throwsAsync(createBuildManifest(project, "buildConfig", taskRepository), { + message: "Missing parameter 'signature'" + }); +}); + test("Create application from project with build manifest", async (t) => { const project = await Specification.create(applicationProjectInput); - project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + project.getProjectResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); const taskRepository = { getVersions: async () => ({builderVersion: "", fsVersion: ""}) }; - const metadata = await createBuildManifest(project, "buildConfig", taskRepository); + const metadata = await createBuildManifest(project, "buildConfig", taskRepository, "yyy"); t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); metadata.buildManifest.timestamp = ""; @@ -99,7 +110,8 @@ test("Create application from project with build manifest", async (t) => { } }, buildManifest: { - manifestVersion: "0.2", + manifestVersion: "1.0", + signature: "yyy", buildConfig: "buildConfig", namespace: "id1", timestamp: "", @@ -121,13 +133,13 @@ test("Create application from project with build manifest", async (t) => { test("Create library from project with build manifest", async (t) => { const project = await Specification.create(libraryProjectInput); - project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); + project.getProjectResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); const taskRepository = { getVersions: async () => ({builderVersion: "", fsVersion: ""}) }; - const metadata = await createBuildManifest(project, "buildConfig", taskRepository); + const metadata = await createBuildManifest(project, "buildConfig", taskRepository, "zzz"); t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); metadata.buildManifest.timestamp = ""; @@ -155,7 +167,8 @@ test("Create library from project with build manifest", async (t) => { } }, buildManifest: { - manifestVersion: "0.2", + manifestVersion: "1.0", + signature: "zzz", buildConfig: "buildConfig", namespace: "library/d", timestamp: "", diff --git a/packages/project/test/lib/graph/ProjectGraph.js b/packages/project/test/lib/graph/ProjectGraph.js index 46295f96723..9f546a47685 100644 --- a/packages/project/test/lib/graph/ProjectGraph.js +++ b/packages/project/test/lib/graph/ProjectGraph.js @@ -1152,6 +1152,550 @@ test("traverseDepthFirst: Dependency declaration order is followed", async (t) = ]); }); +test("traverseDependenciesDepthFirst: Basic traversal without including start module", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.c", + "library.b" + ], "Should traverse dependencies in depth-first order, excluding start module"); +}); + +test("traverseDependenciesDepthFirst: Basic traversal including start module", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a", true)) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.c", + "library.b", + "library.a" + ], "Should traverse dependencies in depth-first order, including start module"); +}); + +test("traverseDependenciesDepthFirst: Using boolean as first parameter", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst(true)) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.c", + "library.b", + "library.a" + ], "Should traverse from root and include root when boolean is passed as first parameter"); +}); + +test("traverseDependenciesDepthFirst: No dependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [], "Should return empty results when project has no dependencies"); +}); + +test("traverseDependenciesDepthFirst: Diamond dependency structure", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.d"); + graph.declareDependency("library.c", "library.d"); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.d", + "library.b", + "library.c" + ], "Should visit library.d once, then library.b, then library.c"); +}); + +test("traverseDependenciesDepthFirst: Complex dependency chain", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + graph.addProject(await createProject("library.e")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.d"); + graph.declareDependency("library.d", "library.e"); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.e", + "library.d", + "library.c", + "library.b" + ], "Should traverse entire dependency chain in depth-first order"); +}); + +test("traverseDependenciesDepthFirst: No project visited twice", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.d"); + graph.declareDependency("library.c", "library.d"); + + const results = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + results.push(result.project.getName()); + } + + // library.d should appear only once + const dCount = results.filter((name) => name === "library.d").length; + t.is(dCount, 1, "library.d should be visited exactly once"); + t.is(results.length, 3, "Should visit exactly 3 projects"); +}); + +test("traverseDependenciesDepthFirst: Can't find start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + + const error = t.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const result of graph.traverseDependenciesDepthFirst("library.nonexistent")) { + // Should not reach here + } + }); + t.is(error.message, + "Failed to start graph traversal: Could not find project library.nonexistent in project graph", + "Should throw with expected error message"); +}); + +test("traverseDependenciesDepthFirst: dependencies parameter", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.d"); + + const results = []; + const dependencies = []; + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + results.push(result.project.getName()); + dependencies.push(result.dependencies); + } + + t.deepEqual(results, [ + "library.d", + "library.b", + "library.c" + ], "Should visit dependencies in depth-first order"); + + const dIndex = results.indexOf("library.d"); + const bIndex = results.indexOf("library.b"); + const cIndex = results.indexOf("library.c"); + + t.deepEqual(dependencies[dIndex], [], "library.d should have no dependencies"); + t.deepEqual(dependencies[bIndex], ["library.d"], "library.b should have library.d as dependency"); + t.deepEqual(dependencies[cIndex], [], "library.c should have no dependencies"); +}); + +test("traverseDependenciesDepthFirst: Detect cycle", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.a"); + + const error = t.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const result of graph.traverseDependenciesDepthFirst("library.a")) { + // Should not complete iteration + } + }); + t.is(error.message, + "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*", + "Should throw with expected error message"); +}); + +test("traverseDependenciesDepthFirst: Dependency declaration order is followed", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + const results1 = []; + for (const result of graph1.traverseDependenciesDepthFirst("library.a")) { + results1.push(result.project.getName()); + } + + t.deepEqual(results1, [ + "library.b", + "library.c", + "library.d" + ], "First graph should visit in declaration order"); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph2.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.b")); + graph2.addProject(await createProject("library.c")); + graph2.addProject(await createProject("library.d")); + + graph2.declareDependency("library.a", "library.d"); + graph2.declareDependency("library.a", "library.c"); + graph2.declareDependency("library.a", "library.b"); + + const results2 = []; + for (const result of graph2.traverseDependenciesDepthFirst("library.a")) { + results2.push(result.project.getName()); + } + + t.deepEqual(results2, [ + "library.d", + "library.c", + "library.b" + ], "Second graph should visit in reverse declaration order"); +}); + +test("traverseDependents: Basic traversal without including start module", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.a", "library.b"); + + const results = []; + for (const result of graph.traverseDependents("library.c")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.b", + "library.a" + ], "Should traverse dependents in correct order, excluding start module"); +}); + +test("traverseDependents: Basic traversal including start module", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.a", "library.b"); + + const results = []; + for (const result of graph.traverseDependents("library.c", true)) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.c", + "library.b", + "library.a" + ], "Should traverse dependents in correct order, including start module"); +}); + +test("traverseDependents: Using boolean as first parameter", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.c" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + const results = []; + for (const result of graph.traverseDependents(true)) { + results.push(result.project.getName()); + } + + t.deepEqual(results.sort(), [ + "library.a", + "library.b", + "library.c" + ].sort(), "Should traverse from root and include root when boolean is passed as first parameter"); +}); + +test("traverseDependents: No dependents", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + const results = []; + for (const result of graph.traverseDependents("library.a")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [], "Should return empty results when project has no dependents"); +}); + +test("traverseDependents: Multiple dependents", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.d"); + graph.declareDependency("library.b", "library.d"); + graph.declareDependency("library.c", "library.d"); + + const results = []; + for (const result of graph.traverseDependents("library.d")) { + results.push(result.project.getName()); + } + + t.deepEqual(results.sort(), [ + "library.a", + "library.b", + "library.c" + ].sort(), "Should return all projects that depend on the target project"); +}); + +test("traverseDependents: Complex chain", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + graph.addProject(await createProject("library.e")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.d"); + graph.declareDependency("library.d", "library.e"); + + const results = []; + for (const result of graph.traverseDependents("library.e")) { + results.push(result.project.getName()); + } + + t.deepEqual(results, [ + "library.d", + "library.c", + "library.b", + "library.a" + ], "Should traverse entire dependent chain"); +}); + +test("traverseDependents: No project visited twice", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.a", "library.d"); + graph.declareDependency("library.b", "library.d"); + graph.declareDependency("library.c", "library.d"); + + const results = []; + for (const result of graph.traverseDependents("library.d")) { + results.push(result.project.getName()); + } + + t.deepEqual(results.sort(), [ + "library.a", + "library.b", + "library.c" + ].sort(), "Should visit each project exactly once"); +}); + +test("traverseDependents: Can't find start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + + const error = t.throws(() => { + // Consume the generator to trigger the error + // eslint-disable-next-line no-unused-vars + for (const result of graph.traverseDependents("library.nonexistent")) { + // Should not reach here + } + }); + t.is(error.message, + "Failed to start graph traversal: Could not find project library.nonexistent in project graph", + "Should throw with expected error message"); +}); + +test("traverseDependents: dependents parameter", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.d"); + graph.declareDependency("library.b", "library.d"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.d"); + + const results = []; + const dependents = []; + for (const result of graph.traverseDependents("library.d")) { + results.push(result.project.getName()); + dependents.push(result.dependents); + } + + t.deepEqual(results.sort(), [ + "library.a", + "library.b", + "library.c" + ].sort(), "Should visit all dependents"); + + // Check that dependents information is provided correctly + const aIndex = results.indexOf("library.a"); + const bIndex = results.indexOf("library.b"); + const cIndex = results.indexOf("library.c"); + + t.deepEqual(dependents[aIndex], [], "library.a should have no dependents"); + t.deepEqual(dependents[bIndex], [], "library.b should have no dependents"); + t.deepEqual(dependents[cIndex], ["library.b"], "library.c should have library.b as dependent"); +}); + +test("traverseDependents: Detect cycle", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.a"); + + const error = t.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const result of graph.traverseDependents("library.a")) { + // Should not complete iteration + } + }); + t.is(error.message, + "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*", + "Should throw with expected error message"); +}); + test("join", async (t) => { const {ProjectGraph} = t.context; const graph1 = new ProjectGraph({ diff --git a/packages/project/test/lib/graph/graph.integration.js b/packages/project/test/lib/graph/graph.integration.js index 9b459a0f823..fcff4e57957 100644 --- a/packages/project/test/lib/graph/graph.integration.js +++ b/packages/project/test/lib/graph/graph.integration.js @@ -3,7 +3,7 @@ import path from "node:path"; import sinonGlobal from "sinon"; import esmock from "esmock"; import Workspace from "../../../lib/graph/Workspace.js"; -import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js"; +import SnapshotCache from "../../../lib/ui5Framework/maven/SnapshotCache.js"; const __dirname = import.meta.dirname; const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); @@ -254,7 +254,7 @@ test.serial("graphFromPackageDependencies with inactive workspace file at custom versionOverride: "versionOverride", workspaceName: "default", workspaceConfigPath: path.join(libraryHPath, "custom-ui5-workspace.yaml"), - cacheMode: CacheMode.Force + snapshotCache: SnapshotCache.Force }); t.is(res, "graph"); @@ -278,6 +278,6 @@ test.serial("graphFromPackageDependencies with inactive workspace file at custom t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: null, - cacheMode: "Force" + snapshotCache: "Force" }, "enrichProjectGraph got called with correct options"); }); diff --git a/packages/project/test/lib/graph/graph.js b/packages/project/test/lib/graph/graph.js index 4cc4c1386c0..799033a59db 100644 --- a/packages/project/test/lib/graph/graph.js +++ b/packages/project/test/lib/graph/graph.js @@ -2,7 +2,7 @@ import test from "ava"; import path from "node:path"; import sinonGlobal from "sinon"; import esmock from "esmock"; -import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js"; +import SnapshotCache from "../../../lib/ui5Framework/maven/SnapshotCache.js"; const __dirname = import.meta.dirname; const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); @@ -58,7 +58,7 @@ test.serial("graphFromPackageDependencies", async (t) => { rootConfiguration: "rootConfiguration", rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", - cacheMode: CacheMode.Off, + snapshotCache: SnapshotCache.Off, workspaceName: null }); @@ -84,7 +84,7 @@ test.serial("graphFromPackageDependencies", async (t) => { t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: undefined, - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -101,7 +101,7 @@ test.serial("graphFromPackageDependencies with workspace name", async (t) => { rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", workspaceName: "dolphin", - cacheMode: CacheMode.Off + snapshotCache: SnapshotCache.Off }); t.is(res, "graph"); @@ -133,7 +133,7 @@ test.serial("graphFromPackageDependencies with workspace name", async (t) => { t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: "workspace", - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -231,7 +231,7 @@ test.serial("graphFromPackageDependencies with empty workspace", async (t) => { rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", workspaceName: "dolphin", - cacheMode: CacheMode.Off + snapshotCache: SnapshotCache.Off }); t.is(res, "graph"); @@ -263,7 +263,7 @@ test.serial("graphFromPackageDependencies with empty workspace", async (t) => { t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: null, - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -317,7 +317,7 @@ test.serial("graphFromStaticFile", async (t) => { rootConfiguration: "rootConfiguration", rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", - cacheMode: CacheMode.Off + snapshotCache: SnapshotCache.Off }); t.is(res, "graph"); @@ -344,7 +344,7 @@ test.serial("graphFromStaticFile", async (t) => { "enrichProjectGraph got called with graph"); t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -380,7 +380,7 @@ test.serial("usingObject", async (t) => { rootConfiguration: "rootConfiguration", rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", - cacheMode: "Off" + snapshotCache: "Off" }); t.is(res, "graph"); @@ -401,7 +401,7 @@ test.serial("usingObject", async (t) => { "enrichProjectGraph got called with graph"); t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.js b/packages/project/test/lib/graph/helpers/ui5Framework.js index ceae8c52e54..b134ac187ac 100644 --- a/packages/project/test/lib/graph/helpers/ui5Framework.js +++ b/packages/project/test/lib/graph/helpers/ui5Framework.js @@ -5,7 +5,7 @@ import esmock from "esmock"; import DependencyTreeProvider from "../../../../lib/graph/providers/DependencyTree.js"; import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js"; import Specification from "../../../../lib/specifications/Specification.js"; -import CacheMode from "../../../../lib/ui5Framework/maven/CacheMode.js"; +import SnapshotCache from "../../../../lib/ui5Framework/maven/SnapshotCache.js"; const __dirname = import.meta.dirname; @@ -128,7 +128,7 @@ test.serial("enrichProjectGraph", async (t) => { t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: undefined, @@ -239,7 +239,7 @@ test.serial("enrichProjectGraph SNAPSHOT", async (t) => { const projectGraph = await projectGraphBuilder(provider); await ui5Framework.enrichProjectGraph(projectGraph, { - cacheMode: CacheMode.Force + snapshotCache: SnapshotCache.Force }); t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); @@ -341,7 +341,7 @@ test.serial("enrichProjectGraph: With versionOverride", async (t) => { t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9", ui5DataDir: undefined, @@ -404,7 +404,7 @@ test.serial("enrichProjectGraph: With versionOverride containing snapshot versio t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, "Sapui5MavenSnapshotResolverStub#constructor should be called once"); t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9-SNAPSHOT", ui5DataDir: undefined, @@ -467,7 +467,7 @@ test.serial("enrichProjectGraph: With versionOverride containing latest-snapshot t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, "Sapui5MavenSnapshotResolverStub#constructor should be called once"); t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9-SNAPSHOT", ui5DataDir: undefined, @@ -627,7 +627,7 @@ test.serial("enrichProjectGraph should resolve framework project with version an t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGrap should be called once"); t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.2.3", ui5DataDir: undefined, @@ -732,7 +732,7 @@ test.serial("enrichProjectGraph should resolve framework project " + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9", ui5DataDir: undefined, @@ -997,7 +997,7 @@ test.serial("enrichProjectGraph should use framework library metadata from works t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.111.1", ui5DataDir: undefined, @@ -1056,7 +1056,7 @@ test.serial("enrichProjectGraph should allow omitting framework version in case t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, ui5DataDir: undefined, version: undefined, @@ -1113,7 +1113,7 @@ test.serial("enrichProjectGraph should use UI5 data dir from env var", async (t) t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: expectedUi5DataDir, @@ -1169,7 +1169,7 @@ test.serial("enrichProjectGraph should use UI5 data dir from configuration", asy t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: expectedUi5DataDir, @@ -1225,7 +1225,7 @@ test.serial("enrichProjectGraph should use absolute UI5 data dir from configurat t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: expectedUi5DataDir, diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index 305cdbb04b1..437d29d3287 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -24,7 +24,7 @@ test("check number of exports", (t) => { "ui5Framework/Openui5Resolver", "ui5Framework/Sapui5Resolver", "ui5Framework/Sapui5MavenSnapshotResolver", - "ui5Framework/maven/CacheMode", + "ui5Framework/maven/SnapshotCache", "validation/validator", "validation/ValidationError", "graph/ProjectGraph", diff --git a/packages/project/test/lib/resources/ProjectResources.js b/packages/project/test/lib/resources/ProjectResources.js new file mode 100644 index 00000000000..69aa964531e --- /dev/null +++ b/packages/project/test/lib/resources/ProjectResources.js @@ -0,0 +1,165 @@ +import test from "ava"; +import sinon from "sinon"; +import {createProxy, createResource} from "@ui5/fs/resourceFactory"; +import ProjectResources from "../../../lib/resources/ProjectResources.js"; + +function createProjectResources({frozenSourceReader} = {}) { + const sourceReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null), + }; + const writer = { + byGlob: sinon.stub().resolves([]), + write: sinon.stub().resolves(), + }; + const pr = new ProjectResources({ + getName: () => "test.project", + getStyledReader: sinon.stub().returns(sourceReader), + createWriter: sinon.stub().returns(writer), + addReadersForWriter: sinon.stub(), + buildManifest: null, + }); + if (frozenSourceReader) { + pr.setFrozenSourceReader(frozenSourceReader); + } + return {pr, sourceReader, writer}; +} + +test.afterEach.always(() => { + sinon.restore(); +}); + +test("setFrozenSourceReader: frozen reader is included in getReader chain", (t) => { + const frozenReader = {name: "frozen-cas-reader"}; + const {pr} = createProjectResources({frozenSourceReader: frozenReader}); + + // getReader returns a prioritized collection; we can't easily inspect internals, + // but we verify it returns a reader without errors and that the frozen reader was set. + const reader = pr.getReader(); + t.truthy(reader, "Reader returned successfully"); +}); + +test("setFrozenSourceReader: invalidates cached readers", (t) => { + const {pr} = createProjectResources(); + + // Access the reader to populate the cache + const reader1 = pr.getReader(); + + // Set a frozen source reader — this should invalidate the cache + const frozenReader = {name: "frozen-cas-reader"}; + pr.setFrozenSourceReader(frozenReader); + + // Getting the reader again should produce a new instance (not the cached one) + const reader2 = pr.getReader(); + t.not(reader1, reader2, "Cached reader was invalidated; new reader instance returned"); +}); + +test("setFrozenSourceReader: frozen reader is between stages and source reader", (t) => { + const frozenReader = {name: "frozen-cas-reader"}; + const sourceReader = { + byGlob: sinon.stub().resolves([]), + byPath: sinon.stub().resolves(null), + }; + const writer = { + byGlob: sinon.stub().resolves([]), + write: sinon.stub().resolves(), + }; + const getStyledReader = sinon.stub().returns(sourceReader); + const addReadersForWriter = sinon.stub(); + const pr = new ProjectResources({ + getName: () => "test.project", + getStyledReader, + createWriter: sinon.stub().returns(writer), + addReadersForWriter, + buildManifest: null, + }); + pr.setFrozenSourceReader(frozenReader); + + // Initialize stages and switch to one to exercise #addReaderForStage + pr.initStages(["stage1"]); + pr.useStage("stage1"); + + // Calling getWorkspace triggers #getReaders (buildtime style) for the workspace reader + const workspace = pr.getWorkspace(); + t.truthy(workspace, "Workspace created successfully with frozen reader in chain"); +}); + +test("getReader without frozen source reader works normally", (t) => { + const {pr} = createProjectResources(); + + const reader = pr.getReader(); + t.truthy(reader, "Reader returned without frozen source reader"); +}); + +test("initStages clears frozen source reader", (t) => { + const frozenReader = {name: "frozen-cas-reader"}; + const {pr} = createProjectResources({frozenSourceReader: frozenReader}); + + // Verify frozen reader is active + const reader1 = pr.getReader(); + t.truthy(reader1, "Reader with frozen source reader"); + + // initStages resets all stage state including the frozen reader + pr.initStages(["stage1"]); + + // After initStages, a new reader should be created without the frozen reader + // (cache was invalidated). We can't directly inspect the chain, but we verify + // that it doesn't throw and returns a fresh reader. + const reader2 = pr.getReader(); + t.truthy(reader2, "Reader returned after initStages"); + t.not(reader1, reader2, "Reader was recreated after initStages"); +}); + +test("Frozen source reader takes priority over filesystem source reader", async (t) => { + const resourcePath = "/resources/test/some.js"; + const filesystemContent = "filesystem content"; + const frozenCASContent = "frozen CAS content"; + + // Create a source reader that simulates the filesystem + const filesystemResource = createResource({path: resourcePath, string: filesystemContent}); + const sourceReader = createProxy({ + name: "Filesystem source reader", + listResourcePaths: () => [resourcePath], + getResource: async (virPath) => { + if (virPath === resourcePath) { + return filesystemResource; + } + return null; + } + }); + + // Create a frozen CAS reader with different content + const frozenResource = createResource({path: resourcePath, string: frozenCASContent}); + const frozenReader = createProxy({ + name: "Frozen CAS reader", + listResourcePaths: () => [resourcePath], + getResource: async (virPath) => { + if (virPath === resourcePath) { + return frozenResource; + } + return null; + } + }); + + const writer = { + byGlob: sinon.stub().resolves([]), + write: sinon.stub().resolves(), + }; + + const pr = new ProjectResources({ + getName: () => "test.project", + getStyledReader: sinon.stub().returns(sourceReader), + createWriter: sinon.stub().returns(writer), + addReadersForWriter: sinon.stub(), + buildManifest: null, + }); + + pr.setFrozenSourceReader(frozenReader); + + const reader = pr.getReader(); + const result = await reader.byPath(resourcePath); + t.truthy(result, "Resource found via reader"); + const content = await result.getString(); + t.is(content, frozenCASContent, + "Frozen CAS reader takes priority over filesystem source reader"); +}); diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js index 0a53ae309b0..cf27337910d 100644 --- a/packages/project/test/lib/specifications/types/Application.js +++ b/packages/project/test/lib/specifications/types/Application.js @@ -357,7 +357,8 @@ test("Read and write resources outside of app namespace", async (t) => { const workspace = project.getWorkspace(); await workspace.write(createResource({ - path: "/resources/my-custom-bundle.js" + path: "/resources/my-custom-bundle.js", + string: "// some custom bundle content" })); const buildtimeReader = project.getReader({style: "buildtime"}); diff --git a/packages/project/test/lib/specifications/types/Component.js b/packages/project/test/lib/specifications/types/Component.js index f5713be9d95..25bac64d823 100644 --- a/packages/project/test/lib/specifications/types/Component.js +++ b/packages/project/test/lib/specifications/types/Component.js @@ -358,7 +358,8 @@ test("Read and write resources outside of app namespace", async (t) => { const workspace = project.getWorkspace(); await workspace.write(createResource({ - path: "/resources/my-custom-bundle.js" + path: "/resources/my-custom-bundle.js", + string: "// some custom bundle content" })); const buildtimeReader = project.getReader({style: "buildtime"}); diff --git a/packages/project/test/lib/specifications/types/Library.js b/packages/project/test/lib/specifications/types/Library.js index aaeed466701..fc7f4d339b3 100644 --- a/packages/project/test/lib/specifications/types/Library.js +++ b/packages/project/test/lib/specifications/types/Library.js @@ -480,7 +480,7 @@ test("_parseConfiguration: Get copyright", async (t) => { const {projectInput} = t.context; const project = await (new Library().init(projectInput)); - t.is(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly"); + t.is(project.getCopyright(), "${copyright}", "Copyright was read correctly"); }); test("_parseConfiguration: Copyright already configured", async (t) => { diff --git a/packages/project/test/lib/ui5framework/maven/Installer.js b/packages/project/test/lib/ui5framework/maven/Installer.js index 86b00754cdb..c07e9e204bc 100644 --- a/packages/project/test/lib/ui5framework/maven/Installer.js +++ b/packages/project/test/lib/ui5framework/maven/Installer.js @@ -683,7 +683,7 @@ test.serial("_fetchArtifactMetadata: Cache available but disabled", async (t) => cwd: "/cwd/", ui5DataDir: "/ui5Data/", snapshotEndpointUrlCb: () => {}, - cacheMode: "Off" + snapshotCache: "Off" }); sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); @@ -719,7 +719,7 @@ test.serial("_fetchArtifactMetadata: Cache outdated but enforced", async (t) => cwd: "/cwd/", ui5DataDir: "/ui5Data/", snapshotEndpointUrlCb: () => {}, - cacheMode: "Force" + snapshotCache: "Force" }); sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); @@ -756,7 +756,7 @@ test.serial("_fetchArtifactMetadata throws", async (t) => { cwd: "/cwd/", ui5DataDir: "/ui5Data/", snapshotEndpointUrlCb: () => {}, - cacheMode: "Force" + snapshotCache: "Force" }); sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); diff --git a/packages/server/lib/middleware/MiddlewareManager.js b/packages/server/lib/middleware/MiddlewareManager.js index 36894892e4d..10348b82154 100644 --- a/packages/server/lib/middleware/MiddlewareManager.js +++ b/packages/server/lib/middleware/MiddlewareManager.js @@ -21,17 +21,19 @@ const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty); * @alias @ui5/server/internal/MiddlewareManager */ class MiddlewareManager { - constructor({graph, rootProject, resources, options = { + constructor({graph, rootProject, sources, resources, buildReader, options = { sendSAPTargetCSP: false, serveCSPReports: false }}) { - if (!graph || !rootProject || !resources || !resources.all || + if (!graph || !rootProject || !sources || !resources || !resources.all || !resources.rootProject || !resources.dependencies) { throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided"); } this.graph = graph; this.rootProject = rootProject; + this.sources = sources; this.resources = resources; + this.buildReader = buildReader; this.options = options; this.middleware = Object.create(null); @@ -218,7 +220,8 @@ class MiddlewareManager { }); await this.addMiddleware("serveResources"); await this.addMiddleware("testRunner"); - await this.addMiddleware("serveThemes"); + // TODO: Allow to still reference 'serveThemes' middleware in custom middleware + // await this.addMiddleware("serveThemes"); await this.addMiddleware("versionInfo", { mountPath: "/resources/sap-ui-version.json" }); diff --git a/packages/server/lib/middleware/serveResources.js b/packages/server/lib/middleware/serveResources.js index 3e6c1d0ac63..e0a96ce2441 100644 --- a/packages/server/lib/middleware/serveResources.js +++ b/packages/server/lib/middleware/serveResources.js @@ -1,15 +1,5 @@ -import {getLogger} from "@ui5/logger"; -const log = getLogger("server:middleware:serveResources"); -import replaceStream from "replacestream"; import etag from "etag"; import fresh from "fresh"; -import fsInterface from "@ui5/fs/fsInterface"; - -const rProperties = /\.properties$/i; -const rReplaceVersion = /\.(library|js|json)$/i; -const rManifest = /\/manifest\.json$/i; -const rResourcesPrefix = /^\/resources\//i; -const rTestResourcesPrefix = /^\/test-resources\//i; function isFresh(req, res) { return fresh(req.headers, { @@ -18,7 +8,7 @@ function isFresh(req, res) { } /** - * Creates and returns the middleware to serve application resources. + * Creates and returns the middleware to serve project resources. * * @module @ui5/server/middleware/serveResources * @param {object} parameters Parameters @@ -30,90 +20,23 @@ function createMiddleware({resources, middlewareUtil}) { return async function serveResources(req, res, next) { try { const pathname = middlewareUtil.getPathname(req); - let resource = await resources.all.byPath(pathname); - if (!resource) { // Not found - if (!rManifest.test(pathname) || !rResourcesPrefix.test(pathname)) { - next(); - return; - } - log.verbose(`Could not find manifest.json for ${pathname}. ` + - `Checking for .library file to generate manifest.json from.`); - const {default: generateLibraryManifest} = await import("./helper/generateLibraryManifest.js"); - // Attempt to find a .library file, which is required for generating a manifest.json - const dotLibraryPath = pathname.replace(rManifest, "/.library"); - const dotLibraryResource = await resources.all.byPath(dotLibraryPath); - if (dotLibraryResource && dotLibraryResource.getProject()?.getType() === "library") { - resource = await generateLibraryManifest(middlewareUtil, dotLibraryResource); - } - if (!resource) { - // Not a library project, missing .library file or other reason for failed manifest.json generation - next(); - return; - } - } else if ( - rManifest.test(pathname) && !rTestResourcesPrefix.test(pathname) && - resource.getProject()?.getNamespace() - ) { - // Special handling for manifest.json file by adding additional content to the served manifest.json - // NOTE: This should only be done for manifest.json files that exist in the sources, - // not in test-resources. - // Files created by generateLibraryManifest (see above) should not be handled in here. - // Only manifest.json files in library / application projects should be handled. - // resource.getProject.getNamespace() returns null for all other kind of projects. - const {default: manifestEnhancer} = await import("@ui5/builder/processors/manifestEnhancer"); - await manifestEnhancer({ - resources: [resource], - // Ensure that only files within the manifest's project are accessible - // Using the "runtime" style to match the style used by the UI5 server - fs: fsInterface(resource.getProject().getReader({style: "runtime"})) - }); + const resource = await resources.all.byPath(pathname); + if (!resource) { + // Not found + next(); + return; } const resourcePath = resource.getPath(); - if (rProperties.test(resourcePath)) { - // Special handling for *.properties files escape non ascii characters. - const {default: nonAsciiEscaper} = await import("@ui5/builder/processors/nonAsciiEscaper"); - const project = resource.getProject(); - let propertiesFileSourceEncoding = project?.getPropertiesFileSourceEncoding?.(); - - if (!propertiesFileSourceEncoding) { - if (project && project.getSpecVersion().lte("1.1")) { - // default encoding to "ISO-8859-1" for old specVersions - propertiesFileSourceEncoding = "ISO-8859-1"; - } else { - // default encoding to "UTF-8" for all projects starting with specVersion 2.0 - propertiesFileSourceEncoding = "UTF-8"; - } - } - const encoding = nonAsciiEscaper.getEncodingFromAlias(propertiesFileSourceEncoding); - await nonAsciiEscaper({ - resources: [resource], options: { - encoding - } - }); - } - const {contentType, charset} = middlewareUtil.getMimeInfo(resourcePath); + const {contentType} = middlewareUtil.getMimeInfo(resourcePath); if (!res.getHeader("Content-Type")) { res.setHeader("Content-Type", contentType); } // Enable ETag caching - const statInfo = resource.getStatInfo(); - if (statInfo?.size !== undefined && !resource.isModified()) { - let etagHeader = etag(statInfo); - if (resource.getProject()) { - // Add project version to ETag to invalidate cache when project version changes. - // This is necessary to invalidate files with ${version} placeholders. - etagHeader = etagHeader.slice(0, -1) + `-${resource.getProject().getVersion()}"`; - } - res.setHeader("ETag", etagHeader); - } else { - // Fallback to buffer if stats are not available or insufficient or resource is modified. - // Modified resources must use the buffer for cache invalidation so that UI5 CLI changes - // invalidate the cache even when the original resource is not modified. - res.setHeader("ETag", etag(await resource.getBuffer())); - } + const resourceIntegrity = await resource.getIntegrity(); + res.setHeader("ETag", etag(resourceIntegrity)); if (isFresh(req, res)) { // client has a fresh copy of the resource @@ -122,22 +45,9 @@ function createMiddleware({resources, middlewareUtil}) { return; } - let stream = resource.getStream(); - - // Only execute version replacement for UTF-8 encoded resources because replaceStream will always output - // UTF-8 anyways. - // Also, only process .library, *.js and *.json files. Just like it's done in Application- - // and LibraryBuilder - if ((!charset || charset === "UTF-8") && rReplaceVersion.test(resourcePath)) { - if (resource.getProject()) { - stream.setEncoding("utf8"); - stream = stream.pipe(replaceStream("${version}", resource.getProject().getVersion())); - } else { - log.verbose(`Project missing from resource ${pathname}"`); - } - } - - stream.pipe(res); + // Pipe resource stream to response + // TODO: Check whether we can optimize this for small or even all resources by using getBuffer() + res.send(await resource.getBuffer()); } catch (err) { next(err); } diff --git a/packages/server/lib/server.js b/packages/server/lib/server.js index 5ea5ff8812d..c27de5b2f73 100644 --- a/packages/server/lib/server.js +++ b/packages/server/lib/server.js @@ -4,6 +4,7 @@ import MiddlewareManager from "./middleware/MiddlewareManager.js"; import {createReaderCollection} from "@ui5/fs/resourceFactory"; import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; import {getLogger} from "@ui5/logger"; +import Cache from "@ui5/project/build/cache/Cache"; const log = getLogger("server"); /** @@ -128,6 +129,8 @@ async function _addSsl({app, key, cert}) { * are send for any requested *.html file * @param {boolean} [options.serveCSPReports=false] Enable CSP reports serving for request url * '/.ui5/csp/csp-reports.json' + * @param {string} [options.cache="Default"] Cache mode to use for building UI5 projects. + * @param {Function} error Error callback. Will be called when an error occurs outside of request handling. * @returns {Promise} Promise resolving once the server is listening. * It resolves with an object containing the port, * h2-flag and a close function, @@ -135,8 +138,9 @@ async function _addSsl({app, key, cert}) { */ export async function serve(graph, { port: requestedPort, changePortIfInUse = false, h2 = false, key, cert, - acceptRemoteConnections = false, sendSAPTargetCSP = false, simpleIndex = false, serveCSPReports = false -}) { + acceptRemoteConnections = false, sendSAPTargetCSP = false, + simpleIndex = false, serveCSPReports = false, cache = Cache.Default +}, error) { const rootProject = graph.getRoot(); const readers = []; @@ -145,30 +149,52 @@ export async function serve(graph, { // Ignore root project return; } - readers.push(dep.getReader({style: "runtime"})); + readers.push(dep.getSourceReader("runtime")); }); const dependencies = createReaderCollection({ - name: `Dependency reader collection for project ${rootProject.getName()}`, + name: `Dependency reader collection for sources of project ${rootProject.getName()}`, readers }); - const rootReader = rootProject.getReader({style: "runtime"}); + const rootReader = rootProject.getSourceReader("runtime"); // TODO change to ReaderCollection once duplicates are sorted out const combo = new ReaderCollectionPrioritized({ - name: "server - prioritize workspace over dependencies", + name: "Server: Reader for sources of all projects", readers: [rootReader, dependencies] }); - const resources = { + const sources = { rootProject: rootReader, dependencies: dependencies, all: combo }; + const initialBuildIncludedDependencies = []; + if (graph.getProject("sap.ui.core")) { + // Ensure sap.ui.core is always built initially (if present in the graph) + initialBuildIncludedDependencies.push("sap.ui.core"); + } + const buildServer = await graph.serve({ + initialBuildIncludedDependencies, + excludedTasks: ["minify", "generateLibraryPreload", "generateComponentPreload", "generateBundle"], + cache, + }); + + const resources = { + rootProject: buildServer.getRootReader(), + dependencies: buildServer.getDependenciesReader(), + all: buildServer.getReader(), + }; + + buildServer.on("error", async (err) => { + error(err); + }); + const middlewareManager = new MiddlewareManager({ graph, rootProject, + sources, resources, options: { sendSAPTargetCSP, diff --git a/packages/server/package.json b/packages/server/package.json index 86c44da9949..b06670dc8bd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -101,7 +101,6 @@ "mime-types": "^2.1.35", "parseurl": "^1.3.3", "portscanner": "^2.2.0", - "replacestream": "^4.0.3", "router": "^2.2.0", "spdy": "^4.0.2", "yesno": "^0.4.0"