From 32b8379144035a28a7a97980f7416f5d3d5d387c Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Wed, 18 Feb 2026 16:25:03 -0700 Subject: [PATCH 01/13] Add bun.lock analysis support with JSONC parser Adds support for analyzing bun.lock lockfiles, which use JSONC format (JSON with Comments). Includes a megaparsec-based JSONC stripper, bun lockfile parser with workspace and transitive dependency support, and documentation. Co-Authored-By: Claude Opus 4.6 --- .../strategies/languages/nodejs/bun.md | 75 ++++ .../strategies/languages/nodejs/nodejs.md | 3 +- spectrometer.cabal | 4 + src/Data/Text/Jsonc.hs | 110 ++++++ src/Effect/ReadFS.hs | 15 +- src/Strategy/Node.hs | 25 +- src/Strategy/Node/Bun/BunLock.hs | 246 ++++++++++++ src/Strategy/Node/Errors.hs | 1 + src/Types.hs | 2 + test/Bun/BunLockSpec.hs | 233 ++++++++++++ test/Bun/testdata/bun-project/bun.lock | 351 ++++++++++++++++++ test/Bun/testdata/dependencies/bun.lock | 24 ++ test/Bun/testdata/jsonc/bun.lock | 16 + test/Bun/testdata/workspaces/bun.lock | 30 ++ test/Data/Text/JsoncSpec.hs | 133 +++++++ 15 files changed, 1260 insertions(+), 8 deletions(-) create mode 100644 docs/references/strategies/languages/nodejs/bun.md create mode 100644 src/Data/Text/Jsonc.hs create mode 100644 src/Strategy/Node/Bun/BunLock.hs create mode 100644 test/Bun/BunLockSpec.hs create mode 100644 test/Bun/testdata/bun-project/bun.lock create mode 100644 test/Bun/testdata/dependencies/bun.lock create mode 100644 test/Bun/testdata/jsonc/bun.lock create mode 100644 test/Bun/testdata/workspaces/bun.lock create mode 100644 test/Data/Text/JsoncSpec.hs diff --git a/docs/references/strategies/languages/nodejs/bun.md b/docs/references/strategies/languages/nodejs/bun.md new file mode 100644 index 0000000000..1550be74ff --- /dev/null +++ b/docs/references/strategies/languages/nodejs/bun.md @@ -0,0 +1,75 @@ +# Bun + +[Bun](https://bun.sh/) is a fast JavaScript runtime and package manager. +Bun uses a `bun.lock` lockfile in JSONC format (JSON with Comments). + +Reference: https://bun.sh/docs/install/lockfile + +## Project Discovery + +Find files named `bun.lock` with a corresponding `package.json` file. + +## Analysis + +Only `bun.lock` is used for analysis. The lockfile is in JSONC format, +meaning it may contain single-line comments (`//`), block comments (`/* */`), +and trailing commas. These are stripped before parsing. + +### Lockfile Structure + +The `bun.lock` file has the following top-level structure: + +```jsonc +{ + "lockfileVersion": 1, + "workspaces": { + "": { "name": "my-project", "dependencies": {...}, "devDependencies": {...} }, + "packages/a": { "name": "pkg-a", "dependencies": {...} } + }, + "packages": { + "lodash": ["lodash@4.17.21", "", {}, "sha512-..."], + "cross-spawn/which": ["which@2.0.2", "", {...}, "sha512-..."] + } +} +``` + +### Workspaces + +Workspace entries are keyed by their relative path from the root, with `""` +representing the root workspace. Each workspace declares its own +`dependencies`, `devDependencies`, and `optionalDependencies`. + +### Packages + +Package keys use a slash-delimited path for nested `node_modules`: + +- `"lodash"` — top-level package +- `"cross-spawn/which"` — `which` nested under `cross-spawn` + +Package values are variable-length arrays depending on the resolution type: + +- **npm:** `["name@version", "registry", {deps}, "integrity"]` +- **file:** `["name@file:path", {deps}]` +- **workspace:** `["name@workspace:path"]` +- **git:** `["name@git+url", {deps}, "hash", "integrity"]` + +### Environment Labeling + +- Dependencies declared in `devDependencies` of any workspace are labeled as development dependencies. +- Dependencies declared in `dependencies` or `optionalDependencies` of any workspace are labeled as production dependencies. +- Workspace packages themselves (those with `workspace:` resolutions) are excluded from the final dependency graph. + +## FAQ + +### How do I perform analysis only for bun projects? + +You can explicitly specify an analysis target in `.fossa.yml` file. The example below will exclude all analysis targets except for bun. + +```yaml +# .fossa.yml + +version: 3 +targets: + only: + - type: bun +``` diff --git a/docs/references/strategies/languages/nodejs/nodejs.md b/docs/references/strategies/languages/nodejs/nodejs.md index b27cf1e4fc..959562a40f 100644 --- a/docs/references/strategies/languages/nodejs/nodejs.md +++ b/docs/references/strategies/languages/nodejs/nodejs.md @@ -1,10 +1,11 @@ # NodeJS Analysis -The nodejs buildtool ecosystem consists of three major toolchains: the `npm` cli, `pnpm` and `yarn`. +The nodejs buildtool ecosystem consists of four major toolchains: the `npm` cli, `pnpm`, `yarn`, and `bun`. | Strategy | Direct Deps | Transitive Deps | Edges | Container Scanning | | ----------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | | [yarnlock](yarn.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | [npmlock](npm-lockfile.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | [pnpmlock](pnpm.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| [bunlock](bun.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | | [packagejson](packagejson.md) | :white_check_mark: | :x: | :x: | :white_check_mark: | diff --git a/spectrometer.cabal b/spectrometer.cabal index 6b60b2e70b..e161503be5 100644 --- a/spectrometer.cabal +++ b/spectrometer.cabal @@ -370,6 +370,7 @@ library Control.Timeout Control.Timeout.Internal Data.Aeson.Extra + Data.Text.Jsonc Data.Conduit.Extra Data.Error Data.FileEmbed.Extra @@ -480,6 +481,7 @@ library Strategy.Nim Strategy.Nim.NimbleLock Strategy.Node + Strategy.Node.Bun.BunLock Strategy.Node.Errors Strategy.Node.Npm.PackageLock Strategy.Node.Npm.PackageLockV3 @@ -611,6 +613,7 @@ test-suite unit-tests App.Fossa.VSI.TypesSpec App.Fossa.VSIDepsSpec BerkeleyDB.BerkeleyDBSpec + Bun.BunLockSpec BundlerSpec Cargo.CargoTomlSpec Cargo.MetadataSpec @@ -644,6 +647,7 @@ test-suite unit-tests Dart.PubSpecSpec Data.IndexFileTreeSpec Data.RpmDbHeaderBlobSpec + Data.Text.JsoncSpec Discovery.ArchiveSpec Discovery.FiltersSpec Discovery.WalkSpec diff --git a/src/Data/Text/Jsonc.hs b/src/Data/Text/Jsonc.hs new file mode 100644 index 0000000000..6f589c1f79 --- /dev/null +++ b/src/Data/Text/Jsonc.hs @@ -0,0 +1,110 @@ +module Data.Text.Jsonc ( + stripJsonc, +) where + +import Data.Functor (($>)) +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Void (Void) +import Text.Megaparsec ( + Parsec, + anySingle, + eof, + lookAhead, + manyTill, + runParser, + satisfy, + takeWhileP, + try, + (<|>), + ) +import Text.Megaparsec.Char (char, string) +import Text.Megaparsec.Error (errorBundlePretty) + +type Parser = Parsec Void Text + +-- | Strip JSONC artifacts (single-line comments, block comments, +-- and trailing commas) from text, producing valid JSON. +-- +-- Handles: +-- +-- * @\/\/@ single-line comments (to end of line) +-- * @\/\* ... \*\/@ block comments +-- * Trailing commas before @}@ or @]@ +-- +-- Strings (delimited by @"@) are preserved verbatim, including +-- any comment-like or comma characters they contain. +stripJsonc :: Text -> Either String Text +stripJsonc input = case runParser jsoncParser "jsonc" input of + Left err -> Left (errorBundlePretty err) + Right chunks -> Right (Text.concat chunks) + +-- | Parse the entire JSONC input into chunks of valid JSON text. +jsoncParser :: Parser [Text] +jsoncParser = manyTill chunk eof + where + chunk :: Parser Text + chunk = + quotedString + <|> lineComment + <|> blockComment + <|> trailingComma + <|> plainText + +-- | Parse a JSON string literal, preserving its contents verbatim. +quotedString :: Parser Text +quotedString = do + _ <- char '"' + contents <- manyTill stringChar (char '"') + pure $ "\"" <> Text.concat contents <> "\"" + where + stringChar :: Parser Text + stringChar = escapedChar <|> (Text.singleton <$> anySingle) + + escapedChar :: Parser Text + escapedChar = do + _ <- char '\\' + c <- anySingle + pure $ "\\" <> Text.singleton c + +-- | Parse a @\/\/@ line comment and discard it. +lineComment :: Parser Text +lineComment = do + _ <- try (string "//") + _ <- takeWhileP Nothing (/= '\n') + _ <- (char '\n' $> ()) <|> eof + pure "" + +-- | Parse a @\/\* ... \*\/@ block comment and discard it. +blockComment :: Parser Text +blockComment = do + _ <- try (string "/*") + _ <- manyTill anySingle (string "*/") + pure "" + +-- | Parse a trailing comma (comma followed by optional whitespace +-- then @}@ or @]@) and discard only the comma, preserving whitespace. +trailingComma :: Parser Text +trailingComma = do + _ <- try $ do + _ <- char ',' + _ <- lookAhead (takeWhileP Nothing isJsonWhitespace *> satisfy isClosingBracket) + pure () + pure "" + where + isClosingBracket :: Char -> Bool + isClosingBracket c = c == '}' || c == ']' + +-- | Parse one or more characters that aren't special +-- (not a quote, slash, or comma). +plainText :: Parser Text +plainText = do + c <- anySingle + if c == '/' + then pure (Text.singleton c) + else do + rest <- takeWhileP Nothing (\x -> x /= '"' && x /= '/' && x /= ',') + pure $ Text.singleton c <> rest + +isJsonWhitespace :: Char -> Bool +isJsonWhitespace c = c == ' ' || c == '\t' || c == '\n' || c == '\r' diff --git a/src/Effect/ReadFS.hs b/src/Effect/ReadFS.hs index 11e1852d7e..d6cce36527 100644 --- a/src/Effect/ReadFS.hs +++ b/src/Effect/ReadFS.hs @@ -41,6 +41,7 @@ module Effect.ReadFS ( readContentsParser, readContentsParserBS, readContentsJson, + readContentsJsonc, readContentsToml, readContentsYaml, readContentsXML, @@ -89,9 +90,10 @@ import Data.Bifunctor (first) import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.Either.Combinators (mapRight) -import Data.String.Conversion (decodeUtf8, toString, toText) +import Data.String.Conversion (decodeUtf8, encodeUtf8, toString, toText) import Data.Text (Text) import Data.Text.Extra (showT) +import Data.Text.Jsonc (stripJsonc) import Data.Void (Void) import Data.Yaml (decodeEither', prettyPrintParseException) import Effect.Logger (renderIt) @@ -366,6 +368,17 @@ readContentsJson file = context ("Parsing JSON file '" <> toText (toString file) Left err -> errSupport (fileParseErrorSupportMsg file) $ fatal $ FileParseError (toString file) (toText err) Right a -> pure a +-- | Read JSONC (JSON with Comments) from a file. +-- Strips single-line comments, block comments, and trailing commas before parsing as JSON. +readContentsJsonc :: (FromJSON a, Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> m a +readContentsJsonc file = context ("Parsing JSONC file '" <> toText (toString file) <> "'") $ do + contents <- readContentsText file + case stripJsonc contents of + Left err -> errSupport (fileParseErrorSupportMsg file) $ fatal $ FileParseError (toString file) (toText err) + Right stripped -> case eitherDecodeStrict (encodeUtf8 stripped) of + Left err -> errSupport (fileParseErrorSupportMsg file) $ fatal $ FileParseError (toString file) (toText err) + Right a -> pure a + readContentsToml :: (Toml.Schema.FromValue a, Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> m a readContentsToml file = context ("Parsing TOML file '" <> toText (toString file) <> "'") $ do contents <- readContentsText file diff --git a/src/Strategy/Node.hs b/src/Strategy/Node.hs index 38b59fb432..61bdb94d28 100644 --- a/src/Strategy/Node.hs +++ b/src/Strategy/Node.hs @@ -73,6 +73,7 @@ import Path ( toFilePath, (), ) +import Strategy.Node.Bun.BunLock qualified as BunLock import Strategy.Node.Errors (CyclicPackageJson (CyclicPackageJson), MissingNodeLockFile (..), fossaNodeDocUrl, npmLockFileDocUrl, yarnLockfileDocUrl, yarnV2LockfileDocUrl) import Strategy.Node.Npm.PackageLock qualified as PackageLock import Strategy.Node.Npm.PackageLockV3 qualified as PackageLockV3 @@ -98,7 +99,7 @@ import Strategy.Node.YarnV2.YarnLock qualified as V2 import Types ( DependencyResults (DependencyResults), DiscoveredProject (..), - DiscoveredProjectType (NpmProjectType, PnpmProjectType, YarnProjectType), + DiscoveredProjectType (BunProjectType, NpmProjectType, PnpmProjectType, YarnProjectType), FoundTargets (ProjectWithoutTargets), GraphBreadth (Complete, Partial), License (License), @@ -118,7 +119,7 @@ discover :: ) => Path Abs Dir -> m [DiscoveredProject NodeProject] -discover dir = withMultiToolFilter [YarnProjectType, NpmProjectType, PnpmProjectType] $ +discover dir = withMultiToolFilter [YarnProjectType, NpmProjectType, PnpmProjectType, BunProjectType] $ context "NodeJS" $ do manifestList <- context "Finding nodejs/pnpm projects" $ collectManifests dir manifestMap <- context "Reading manifest files" $ (Map.fromList . catMaybes) <$> traverse loadPackage manifestList @@ -146,6 +147,7 @@ mkProject project = do Yarn _ g -> (g, YarnProjectType) NPMLock _ g -> (g, NpmProjectType) NPM g -> (g, NpmProjectType) + Bun _ g -> (g, BunProjectType) Pnpm _ g -> (g, PnpmProjectType) Manifest rootManifest <- fromEitherShow $ findWorkspaceRootManifest graph pure $ @@ -172,6 +174,7 @@ getDeps :: getDeps (Yarn yarnLockFile graph) = analyzeYarn yarnLockFile graph getDeps (NPMLock packageLockFile graph) = analyzeNpmLock packageLockFile graph getDeps (Pnpm pnpmLockFile _) = analyzePnpmLock pnpmLockFile +getDeps (Bun bunLockFile _) = analyzeBunLock bunLockFile getDeps (NPM graph) = analyzeNpm graph analyzePnpmLock :: (Has Diagnostics sig m, Has ReadFS sig m, Has Logger sig m) => Manifest -> m DependencyResults @@ -179,6 +182,11 @@ analyzePnpmLock (Manifest pnpmLockFile) = do result <- PnpmLock.analyze pnpmLockFile pure $ DependencyResults result Complete [pnpmLockFile] +analyzeBunLock :: (Has Diagnostics sig m, Has ReadFS sig m) => Manifest -> m DependencyResults +analyzeBunLock (Manifest bunLockFile) = do + result <- BunLock.analyze bunLockFile + pure $ DependencyResults result Complete [bunLockFile] + analyzeNpmLock :: (Has Diagnostics sig m, Has ReadFS sig m) => Manifest -> PkgJsonGraph -> m DependencyResults analyzeNpmLock (Manifest npmLockFile) graph = do npmLockVersion <- detectNpmLockVersion npmLockFile @@ -368,19 +376,23 @@ identifyProjectType graph = do let yarnFilePath = parent manifest Path. $(mkRelFile "yarn.lock") packageLockPath = parent manifest Path. $(mkRelFile "package-lock.json") pnpmLockPath = parent manifest Path. $(mkRelFile "pnpm-lock.yaml") + bunLockPath = parent manifest Path. $(mkRelFile "bun.lock") yarnExists <- doesFileExist yarnFilePath pkgLockExists <- doesFileExist packageLockPath pnpmLockExists <- doesFileExist pnpmLockPath - pure $ case (yarnExists, pkgLockExists, pnpmLockExists) of - (True, _, _) -> Yarn (Manifest yarnFilePath) graph - (_, True, _) -> NPMLock (Manifest packageLockPath) graph - (_, _, True) -> Pnpm (Manifest pnpmLockPath) graph + bunLockExists <- doesFileExist bunLockPath + pure $ case (yarnExists, pkgLockExists, pnpmLockExists, bunLockExists) of + (True, _, _, _) -> Yarn (Manifest yarnFilePath) graph + (_, True, _, _) -> NPMLock (Manifest packageLockPath) graph + (_, _, True, _) -> Pnpm (Manifest pnpmLockPath) graph + (_, _, _, True) -> Bun (Manifest bunLockPath) graph _ -> NPM graph data NodeProject = Yarn Manifest PkgJsonGraph | NPMLock Manifest PkgJsonGraph | NPM PkgJsonGraph + | Bun Manifest PkgJsonGraph | Pnpm Manifest PkgJsonGraph deriving (Eq, Ord, Show, Generic) @@ -413,6 +425,7 @@ pkgGraph :: NodeProject -> PkgJsonGraph pkgGraph (Yarn _ pjg) = pjg pkgGraph (NPMLock _ pjg) = pjg pkgGraph (NPM pjg) = pjg +pkgGraph (Bun _ pjg) = pjg pkgGraph (Pnpm _ pjg) = pjg findWorkspaceRootManifest :: PkgJsonGraph -> Either String Manifest diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs new file mode 100644 index 0000000000..c2084c1c0c --- /dev/null +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -0,0 +1,246 @@ +{-# LANGUAGE OverloadedRecordDot #-} + +module Strategy.Node.Bun.BunLock ( + analyze, + buildGraph, + + -- * for testing + BunLockfile (..), + BunWorkspace (..), + BunPackage (..), + BunPackageDeps (..), + parseResolution, +) where + +import Control.Algebra (Has) +import Control.Effect.Diagnostics (Diagnostics, context) +import Control.Monad (unless) +import Data.Aeson ( + FromJSON (parseJSON), + Value (Object), + withArray, + withObject, + (.!=), + (.:), + (.:?), + ) +import Data.Foldable (for_) +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Set qualified as Set +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Vector qualified as V +import DepTypes ( + DepEnvironment (..), + DepType (NodeJSType), + Dependency (..), + VerConstraint (CEq), + ) +import Effect.Grapher (Grapher, deep, direct, edge, evalGrapher, run) +import Effect.ReadFS (ReadFS, readContentsJsonc) +import Graphing (Graphing) +import Graphing qualified +import Path (Abs, File, Path) + +-- | Bun lockfile (bun.lock) in JSONC format. +-- +-- See @docs/references/strategies/languages/nodejs/bun.md@ for the full +-- lockfile format documentation. +data BunLockfile = BunLockfile + { lockfileVersion :: Int + , workspaces :: Map Text BunWorkspace + , packages :: Map Text BunPackage + } + deriving (Show, Eq) + +data BunWorkspace = BunWorkspace + { wsName :: Text + , wsDependencies :: Map Text Text + , wsDevDependencies :: Map Text Text + , wsOptionalDependencies :: Map Text Text + , wsPeerDependencies :: Map Text Text + } + deriving (Show, Eq, Ord) + +-- | Resolved dependencies extracted from a package's info object. +data BunPackageDeps = BunPackageDeps + { pkgDepsDependencies :: Map Text Text + , pkgDepsOptionalDependencies :: Map Text Text + , pkgDepsPeerDependencies :: Map Text Text + } + deriving (Show, Eq) + +-- | A resolved package entry in the lockfile. +data BunPackage = BunPackage + { pkgResolution :: Text + -- ^ e.g. @"lodash\@4.17.21"@, @"pkg\@file:../local"@, @"pkg\@workspace:packages/a"@ + , pkgDeps :: BunPackageDeps + -- ^ Transitive dependency info extracted from the package array. + } + deriving (Show, Eq) + +instance FromJSON BunLockfile where + parseJSON = withObject "BunLockfile" $ \obj -> + BunLockfile + <$> obj .: "lockfileVersion" + <*> obj .:? "workspaces" .!= mempty + <*> obj .:? "packages" .!= mempty + +instance FromJSON BunWorkspace where + parseJSON = withObject "BunWorkspace" $ \obj -> + BunWorkspace + <$> obj .:? "name" .!= "" + <*> obj .:? "dependencies" .!= mempty + <*> obj .:? "devDependencies" .!= mempty + <*> obj .:? "optionalDependencies" .!= mempty + <*> obj .:? "peerDependencies" .!= mempty + +instance FromJSON BunPackageDeps where + parseJSON = withObject "BunPackageDeps" $ \obj -> + BunPackageDeps + <$> obj .:? "dependencies" .!= mempty + <*> obj .:? "optionalDependencies" .!= mempty + <*> obj .:? "peerDependencies" .!= mempty + +-- | Package arrays are variable-length. The first element is always +-- the resolution string. We find the first JSON object in the +-- remaining elements (the dependency metadata object). +instance FromJSON BunPackage where + parseJSON = withArray "BunPackage" $ \arr -> + case V.toList arr of + [] -> fail "Expected non-empty package array" + (resVal : rest) -> do + resolution <- parseJSON resVal + deps <- case filter isObject rest of + (obj : _) -> parseJSON obj + [] -> pure emptyDeps + pure $ BunPackage resolution deps + where + isObject (Object _) = True + isObject _ = False + + emptyDeps :: BunPackageDeps + emptyDeps = BunPackageDeps mempty mempty mempty + +-- | Parse a resolution string into (name, version). +-- +-- >>> parseResolution "lodash@4.17.21" +-- ("lodash", "4.17.21") +-- +-- >>> parseResolution "@scope/pkg@1.0.0" +-- ("@scope/pkg", "1.0.0") +-- +-- >>> parseResolution "pkg@file:../local" +-- ("pkg", "file:../local") +-- +-- >>> parseResolution "pkg@workspace:packages/a" +-- ("pkg", "workspace:packages/a") +parseResolution :: Text -> (Text, Text) +parseResolution res + | "@" `Text.isPrefixOf` res = + -- Scoped package: @scope/name@version + let withoutAt = Text.drop 1 res + (scopeAndName, rest) = Text.breakOn "@" withoutAt + in ("@" <> scopeAndName, Text.drop 1 rest) + | otherwise = + let (name, rest) = Text.breakOn "@" res + in (name, Text.drop 1 rest) + +-- | Analyze a bun.lock file and produce a dependency graph. +analyze :: + (Has ReadFS sig m, Has Diagnostics sig m) => + Path Abs File -> + m (Graphing Dependency) +analyze file = do + lockfile <- context "Parsing bun.lock" $ readContentsJsonc file + context "Building dependency graph" $ pure $ buildGraph lockfile + +-- | Build a dependency graph from a parsed bun lockfile. +-- +-- Strategy: +-- 1. Collect all dev dependency names across all workspaces. +-- 2. For each workspace, mark its declared dependencies as direct. +-- 3. For each non-workspace package, add it as a deep dependency +-- and create edges to its transitive dependencies. +-- 4. Filter out workspace packages from the final graph. +buildGraph :: BunLockfile -> Graphing Dependency +buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do + for_ allWorkspaces $ \workspace -> do + markDirectDeps EnvProduction workspace.wsDependencies + markDirectDeps EnvDevelopment workspace.wsDevDependencies + markDirectDeps EnvProduction workspace.wsOptionalDependencies + + for_ (Map.toList $ packages lockfile) $ \(_, pkg) -> + unless (isWorkspaceRef $ pkgResolution pkg) $ do + let parentDep = toDependency pkg + deep parentDep + for_ (transitiveDepNames pkg) $ \childName -> + case Map.lookup childName (packages lockfile) of + Nothing -> pure () + Just childPkg + | isWorkspaceRef (pkgResolution childPkg) -> pure () + | otherwise -> edge parentDep (toDependency childPkg) + where + allWorkspaces :: [BunWorkspace] + allWorkspaces = Map.elems $ workspaces lockfile + + devDepNames :: Set.Set Text + devDepNames = Set.fromList $ concatMap (Map.keys . wsDevDependencies) allWorkspaces + + markDirectDeps :: (Has (Grapher Dependency) sig m) => DepEnvironment -> Map Text Text -> m () + markDirectDeps env deps = + for_ (Map.keys deps) $ \depName -> + case Map.lookup depName (packages lockfile) of + Nothing -> pure () + Just pkg + | isWorkspaceRef (pkgResolution pkg) -> pure () + | otherwise -> direct $ toDependencyAs env pkg + + transitiveDepNames :: BunPackage -> [Text] + transitiveDepNames pkg = + Map.keys (pkgDepsDependencies $ pkgDeps pkg) + <> Map.keys (pkgDepsOptionalDependencies $ pkgDeps pkg) + <> Map.keys (pkgDepsPeerDependencies $ pkgDeps pkg) + + -- \| Convert a package to a Dependency, inferring environment from workspace declarations. + toDependency :: BunPackage -> Dependency + toDependency pkg = toDependencyAs (envOf pkg) pkg + + toDependencyAs :: DepEnvironment -> BunPackage -> Dependency + toDependencyAs env pkg = + Dependency + { dependencyType = NodeJSType + , dependencyName = name + , dependencyVersion = if Text.null version then Nothing else Just (CEq version) + , dependencyLocations = mempty + , dependencyEnvironments = Set.singleton env + , dependencyTags = mempty + } + where + (name, version) = parseResolution (pkgResolution pkg) + + -- \| Determine the environment for a package based on workspace declarations. + envOf :: BunPackage -> DepEnvironment + envOf pkg + | Set.member name devDepNames = EnvDevelopment + | otherwise = EnvProduction + where + (name, _) = parseResolution (pkgResolution pkg) + + -- \| Check if a resolution string refers to a workspace package. + isWorkspaceRef :: Text -> Bool + isWorkspaceRef = Text.isInfixOf "workspace:" + + -- \| Collect workspace package names and filter them from the graph. + withoutWorkspacePackages :: Graphing Dependency -> Graphing Dependency + withoutWorkspacePackages = Graphing.shrink (\dep -> not $ Set.member (dependencyName dep) wsPackageNames) + + wsPackageNames :: Set.Set Text + wsPackageNames = + Set.fromList + [ name + | pkg <- Map.elems (packages lockfile) + , isWorkspaceRef (pkgResolution pkg) + , let (name, _) = parseResolution (pkgResolution pkg) + ] diff --git a/src/Strategy/Node/Errors.hs b/src/Strategy/Node/Errors.hs index d920ecabe7..bedf3951d0 100644 --- a/src/Strategy/Node/Errors.hs +++ b/src/Strategy/Node/Errors.hs @@ -46,5 +46,6 @@ instance ToDiagnostic MissingNodeLockFile where [ "Ensure valid lockfile exist and is readable prior to running fossa." , indent 2 "For yarn package manager, you can perform: `yarn install` to install dependencies and generate lockfile." , indent 2 "For node package manager, you can perform: `npm install` to install dependencies and generate lockfile." + , indent 2 "For bun package manager, you can perform: `bun install` to install dependencies and generate lockfile." ] Errata (Just header) [] Nothing diff --git a/src/Types.hs b/src/Types.hs index 416face720..69c82e0299 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -67,6 +67,7 @@ data DiscoveredProjectType = AlpineDatabaseProjectType | BerkeleyDBProjectType | BinaryDepsProjectType + | BunProjectType | BundlerProjectType | CabalProjectType | CargoProjectType @@ -119,6 +120,7 @@ projectTypeToText = \case AlpineDatabaseProjectType -> "apkdb" BerkeleyDBProjectType -> "berkeleydb" BinaryDepsProjectType -> "binary-deps" + BunProjectType -> "bun" BundlerProjectType -> "bundler" CabalProjectType -> "cabal" CargoProjectType -> "cargo" diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs new file mode 100644 index 0000000000..8026aae5bf --- /dev/null +++ b/test/Bun/BunLockSpec.hs @@ -0,0 +1,233 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Bun.BunLockSpec ( + spec, +) where + +import Control.Algebra (Has) +import Control.Effect.Diagnostics (Diagnostics) +import Data.Aeson (eitherDecodeStrict) +import Data.Map.Strict qualified as Map +import Data.Set qualified as Set +import Data.String.Conversion (encodeUtf8, toText) +import Data.Text (Text) +import Data.Text.Jsonc (stripJsonc) +import DepTypes ( + DepEnvironment (EnvDevelopment, EnvProduction), + DepType (NodeJSType), + Dependency (..), + VerConstraint (CEq), + ) +import Effect.ReadFS (ReadFS, readContentsJsonc) +import GraphUtil (expectEdge) +import Graphing (Graphing) +import Graphing qualified +import Path (Abs, File, Path, fromAbsFile, mkRelDir, mkRelFile, ()) +import Path.IO (getCurrentDir) +import Strategy.Node.Bun.BunLock ( + BunLockfile (..), + BunPackage (..), + BunPackageDeps (..), + BunWorkspace (..), + buildGraph, + parseResolution, + ) +import Test.Effect (it', shouldBe') +import Test.Hspec (Expectation, Spec, describe, expectationFailure, it, runIO, shouldBe) + +spec :: Spec +spec = do + currentDir <- runIO getCurrentDir + let testdata = currentDir $(mkRelDir "test/Bun/testdata") + let jsoncPath = testdata $(mkRelFile "jsonc/bun.lock") + let depsPath = testdata $(mkRelFile "dependencies/bun.lock") + let wsPath = testdata $(mkRelFile "workspaces/bun.lock") + let bunProjectPath = testdata $(mkRelFile "bun-project/bun.lock") + + parseResolutionSpec + jsoncSpec jsoncPath + dependenciesSpec depsPath + workspacesSpec wsPath + bunProjectSpec bunProjectPath + +parseResolutionSpec :: Spec +parseResolutionSpec = describe "parseResolution" $ do + it "parses unscoped packages" $ + parseResolution "lodash@4.17.21" `shouldBe` ("lodash", "4.17.21") + + it "parses scoped packages" $ + parseResolution "@scope/pkg@1.0.0" `shouldBe` ("@scope/pkg", "1.0.0") + + it "parses file references" $ + parseResolution "pkg@file:../local" `shouldBe` ("pkg", "file:../local") + + it "parses workspace references" $ + parseResolution "pkg@workspace:packages/a" `shouldBe` ("pkg", "workspace:packages/a") + + it "parses git references" $ + parseResolution "pkg@github:user/repo#abc123" `shouldBe` ("pkg", "github:user/repo#abc123") + +-- | JSONC: verifies that line comments, block comments, and trailing commas +-- are stripped correctly and the result parses as a valid bun lockfile. +jsoncSpec :: Path Abs File -> Spec +jsoncSpec path = + describe "jsonc" $ do + it' "parses bun.lock with comments and trailing commas" $ do + lockfile <- parseBunLockEffect path + lockfileVersion lockfile `shouldBe'` 1 + wsName (workspaces lockfile Map.! "") `shouldBe'` "jsonc-test" + wsDependencies (workspaces lockfile Map.! "") `shouldBe'` Map.fromList [("lodash", "^4.17.21")] + +-- | Dependencies: all dependency types on a single root workspace, +-- transitive dependency chains, and environment labeling. +dependenciesSpec :: Path Abs File -> Spec +dependenciesSpec path = + describe "dependencies" $ do + it' "parses production, dev, and optional dependencies" $ do + lockfile <- parseBunLockEffect path + let rootWs = workspaces lockfile Map.! "" + wsDependencies rootWs `shouldBe'` Map.fromList [("express", "^4.18.2")] + wsDevDependencies rootWs `shouldBe'` Map.fromList [("typescript", "^5.0.0")] + wsOptionalDependencies rootWs `shouldBe'` Map.fromList [("fsevents", "^2.3.3")] + + it' "parses transitive dependency info" $ do + lockfile <- parseBunLockEffect path + let expressPkg = packages lockfile Map.! "express" + pkgDepsDependencies (pkgDeps expressPkg) `shouldBe'` Map.fromList [("accepts", "~1.3.8")] + + describe "graph" $ do + checkGraph path $ \graph -> do + let directDeps = Graphing.directList graph + + it "marks production dependencies as direct" $ + directDeps `shouldContainDep` mkProdDep "express" "4.18.2" + + it "marks dev dependencies as direct" $ + directDeps `shouldContainDep` mkDevDep "typescript" "5.3.3" + + it "marks optional dependencies as direct" $ + directDeps `shouldContainDep` mkProdDep "fsevents" "2.3.3" + + it "creates transitive edges" $ do + expectEdge graph (mkProdDep "express" "4.18.2") (mkProdDep "accepts" "1.3.8") + expectEdge graph (mkProdDep "accepts" "1.3.8") (mkProdDep "mime-types" "2.1.35") + +-- | Workspaces: multiple workspaces, workspace refs, and workspace +-- package filtering from the final graph. +workspacesSpec :: Path Abs File -> Spec +workspacesSpec path = + describe "workspaces" $ do + it' "parses multiple workspaces" $ do + lockfile <- parseBunLockEffect path + let ws = workspaces lockfile + Map.size ws `shouldBe'` 3 + wsName (ws Map.! "") `shouldBe'` "workspace-root" + wsName (ws Map.! "packages/app") `shouldBe'` "@types/app" + wsName (ws Map.! "packages/utils") `shouldBe'` "utils" + + it' "parses workspace package resolutions" $ do + lockfile <- parseBunLockEffect path + pkgResolution (packages lockfile Map.! "@types/app") `shouldBe'` "@types/app@workspace:packages/app" + pkgResolution (packages lockfile Map.! "utils") `shouldBe'` "utils@workspace:packages/utils" + + describe "graph" $ do + checkGraph path $ \graph -> do + let directDeps = Graphing.directList graph + + it "marks sub-workspace dependencies as direct" $ + directDeps `shouldContainDep` mkProdDep "lodash" "4.17.21" + + it "excludes workspace packages from graph" $ do + let names = map dependencyName (Graphing.vertexList graph) + names `shouldNotContain` "@types/app" + names `shouldNotContain` "utils" + +-- | Bun project: a real-world bun.lock from the bun project itself. +-- Covers scoped packages, git refs, nested package keys, optional/peer deps +-- in packages, and large dependency counts. +bunProjectSpec :: Path Abs File -> Spec +bunProjectSpec path = + describe "bun-project" $ do + it' "parses workspaces" $ do + lockfile <- parseBunLockEffect path + Map.size (workspaces lockfile) `shouldBe'` 3 + wsName (workspaces lockfile Map.! "") `shouldBe'` "bun" + + it' "parses scoped and workspace package resolutions" $ do + lockfile <- parseBunLockEffect path + let pkgs = packages lockfile + pkgResolution (pkgs Map.! "@types/bun") `shouldBe'` "@types/bun@workspace:packages/@types/bun" + pkgResolution (pkgs Map.! "esbuild") `shouldBe'` "esbuild@0.21.5" + + it' "parses transitive dependency info" $ do + lockfile <- parseBunLockEffect path + let lezerCpp = packages lockfile Map.! "@lezer/cpp" + Map.member "@lezer/common" (pkgDepsDependencies $ pkgDeps lezerCpp) `shouldBe'` True + + describe "graph" $ do + checkGraph path $ \graph -> do + it "marks dev dependencies as direct" $ do + let directDeps = Graphing.directList graph + directDeps `shouldContainDep` mkDevDep "esbuild" "0.21.5" + directDeps `shouldContainDep` mkDevDep "typescript" "5.9.2" + + it "creates transitive edges" $ + expectEdge graph (mkDevDep "@lezer/cpp" "1.1.3") (mkDevDep "@lezer/common" "1.3.0") + + it "excludes workspace packages from graph" $ do + let names = map dependencyName (Graphing.vertexList graph) + names `shouldNotContain` "@types/bun" + names `shouldNotContain` "bun-types" + +-- | Parse a bun.lock in the effect stack (for it' tests). +parseBunLockEffect :: + ( Has ReadFS sig m + , Has Diagnostics sig m + ) => + Path Abs File -> + m BunLockfile +parseBunLockEffect = readContentsJsonc + +-- | Parse a bun.lock in IO for graph tests (outside the effect stack). +checkGraph :: Path Abs File -> (Graphing Dependency -> Spec) -> Spec +checkGraph path graphSpec = do + result <- runIO $ parseBunLockIO path + case result of + Left err -> + describe (fromAbsFile path) $ + it "should parse" (expectationFailure err) + Right lockfile -> graphSpec (buildGraph lockfile) + +parseBunLockIO :: Path Abs File -> IO (Either String BunLockfile) +parseBunLockIO path = do + contents <- readFile (fromAbsFile path) + case stripJsonc (toText contents) of + Left err -> pure $ Left err + Right stripped -> pure $ eitherDecodeStrict (encodeUtf8 stripped) + +shouldContainDep :: [Dependency] -> Dependency -> Expectation +shouldContainDep deps dep + | dep `elem` deps = pure () + | otherwise = expectationFailure $ show (dependencyName dep) ++ " not found in direct dependencies" + +shouldNotContain :: (Eq a, Show a) => [a] -> a -> Expectation +shouldNotContain xs x + | x `elem` xs = expectationFailure $ show x ++ " should not be in " ++ show xs + | otherwise = pure () + +mkProdDep :: Text -> Text -> Dependency +mkProdDep name version = mkDep name version EnvProduction + +mkDevDep :: Text -> Text -> Dependency +mkDevDep name version = mkDep name version EnvDevelopment + +mkDep :: Text -> Text -> DepEnvironment -> Dependency +mkDep name version env = + Dependency + { dependencyType = NodeJSType + , dependencyName = name + , dependencyVersion = Just (CEq version) + , dependencyLocations = mempty + , dependencyEnvironments = Set.singleton env + , dependencyTags = mempty + } diff --git a/test/Bun/testdata/bun-project/bun.lock b/test/Bun/testdata/bun-project/bun.lock new file mode 100644 index 0000000000..121ee86803 --- /dev/null +++ b/test/Bun/testdata/bun-project/bun.lock @@ -0,0 +1,351 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "bun", + "devDependencies": { + "@lezer/common": "^1.2.3", + "@lezer/cpp": "^1.1.3", + "@types/bun": "workspace:*", + "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", + "peechy": "0.4.34", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "source-map-js": "^1.2.1", + "typescript": "5.9.2", + }, + }, + "packages/@types/bun": { + "name": "@types/bun", + "version": "1.2.2", + "dependencies": { + "bun-types": "workspace:", + }, + }, + "packages/bun-types": { + "name": "bun-types", + "dependencies": { + "@types/node": "*", + }, + }, + }, + "overrides": { + "@types/bun": "workspace:packages/@types/bun", + "@types/node": "25.0.0", + "bun-types": "workspace:packages/bun-types", + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@lezer/common": ["@lezer/common@1.3.0", "", {}, "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ=="], + + "@lezer/cpp": ["@lezer/cpp@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/lr": ["@lezer/lr@1.4.3", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA=="], + + "@octokit/app": ["@octokit/app@14.1.0", "", { "dependencies": { "@octokit/auth-app": "^6.0.0", "@octokit/auth-unauthenticated": "^5.0.0", "@octokit/core": "^5.0.0", "@octokit/oauth-app": "^6.0.0", "@octokit/plugin-paginate-rest": "^9.0.0", "@octokit/types": "^12.0.0", "@octokit/webhooks": "^12.0.4" } }, "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw=="], + + "@octokit/auth-app": ["@octokit/auth-app@6.1.4", "", { "dependencies": { "@octokit/auth-oauth-app": "^7.1.0", "@octokit/auth-oauth-user": "^4.1.0", "@octokit/request": "^8.3.1", "@octokit/request-error": "^5.1.0", "@octokit/types": "^13.1.0", "deprecation": "^2.3.1", "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", "universal-github-app-jwt": "^1.1.2", "universal-user-agent": "^6.0.0" } }, "sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ=="], + + "@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@7.1.0", "", { "dependencies": { "@octokit/auth-oauth-device": "^6.1.0", "@octokit/auth-oauth-user": "^4.1.0", "@octokit/request": "^8.3.1", "@octokit/types": "^13.0.0", "@types/btoa-lite": "^1.0.0", "btoa-lite": "^1.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA=="], + + "@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@6.1.0", "", { "dependencies": { "@octokit/oauth-methods": "^4.1.0", "@octokit/request": "^8.3.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw=="], + + "@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@4.1.0", "", { "dependencies": { "@octokit/auth-oauth-device": "^6.1.0", "@octokit/oauth-methods": "^4.1.0", "@octokit/request": "^8.3.1", "@octokit/types": "^13.0.0", "btoa-lite": "^1.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ=="], + + "@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], + + "@octokit/auth-unauthenticated": ["@octokit/auth-unauthenticated@5.0.1", "", { "dependencies": { "@octokit/request-error": "^5.0.0", "@octokit/types": "^12.0.0" } }, "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg=="], + + "@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], + + "@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], + + "@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], + + "@octokit/oauth-app": ["@octokit/oauth-app@6.1.0", "", { "dependencies": { "@octokit/auth-oauth-app": "^7.0.0", "@octokit/auth-oauth-user": "^4.0.0", "@octokit/auth-unauthenticated": "^5.0.0", "@octokit/core": "^5.0.0", "@octokit/oauth-authorization-url": "^6.0.2", "@octokit/oauth-methods": "^4.0.0", "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^6.0.0" } }, "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g=="], + + "@octokit/oauth-authorization-url": ["@octokit/oauth-authorization-url@6.0.2", "", {}, "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA=="], + + "@octokit/oauth-methods": ["@octokit/oauth-methods@4.1.0", "", { "dependencies": { "@octokit/oauth-authorization-url": "^6.0.2", "@octokit/request": "^8.3.1", "@octokit/request-error": "^5.1.0", "@octokit/types": "^13.0.0", "btoa-lite": "^1.0.0" } }, "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/plugin-paginate-graphql": ["@octokit/plugin-paginate-graphql@4.0.1", "", { "peerDependencies": { "@octokit/core": ">=5" } }, "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@11.4.4-cjs.2", "", { "dependencies": { "@octokit/types": "^13.7.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1", "", { "dependencies": { "@octokit/types": "^13.8.0" }, "peerDependencies": { "@octokit/core": "^5" } }, "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ=="], + + "@octokit/plugin-retry": ["@octokit/plugin-retry@6.1.0", "", { "dependencies": { "@octokit/request-error": "^5.0.0", "@octokit/types": "^13.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig=="], + + "@octokit/plugin-throttling": ["@octokit/plugin-throttling@8.2.0", "", { "dependencies": { "@octokit/types": "^12.2.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^5.0.0" } }, "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ=="], + + "@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], + + "@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], + + "@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + + "@octokit/webhooks": ["@octokit/webhooks@12.3.2", "", { "dependencies": { "@octokit/request-error": "^5.0.0", "@octokit/webhooks-methods": "^4.1.0", "@octokit/webhooks-types": "7.6.1", "aggregate-error": "^3.1.0" } }, "sha512-exj1MzVXoP7xnAcAB3jZ97pTvVPkQF9y6GA/dvYC47HV7vLv+24XRS6b/v/XnyikpEuvMhugEXdGtAlU086WkQ=="], + + "@octokit/webhooks-methods": ["@octokit/webhooks-methods@5.1.1", "", {}, "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg=="], + + "@octokit/webhooks-types": ["@octokit/webhooks-types@7.6.1", "", {}, "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw=="], + + "@sentry/types": ["@sentry/types@7.120.4", "", {}, "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q=="], + + "@types/aws-lambda": ["@types/aws-lambda@8.10.159", "", {}, "sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg=="], + + "@types/btoa-lite": ["@types/btoa-lite@1.0.2", "", {}, "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg=="], + + "@types/bun": ["@types/bun@workspace:packages/@types/bun"], + + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@25.0.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew=="], + + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + + "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], + + "btoa-lite": ["btoa-lite@1.0.0", "", {}, "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-tracestrings": ["bun-tracestrings@github:oven-sh/bun.report#912ca63", { "dependencies": { "@octokit/webhooks-methods": "^5.1.0", "@sentry/types": "^7.112.2", "@types/bun": "^1.2.6", "html-minifier": "^4.0.0", "lightningcss": "^1.24.1", "marked": "^12.0.1", "octokit": "^3.2.0", "prettier": "^3.2.5", "typescript": "^5.0.0" }, "bin": { "ci-remap-server": "./bin/ci-remap-server.ts" } }, "oven-sh-bun.report-912ca63"], + + "bun-types": ["bun-types@workspace:packages/bun-types"], + + "camel-case": ["camel-case@3.0.0", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.1.1" } }, "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w=="], + + "capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="], + + "change-case": ["change-case@4.1.2", "", { "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", "constant-case": "^3.0.4", "dot-case": "^3.0.4", "header-case": "^2.0.4", "no-case": "^3.0.4", "param-case": "^3.0.4", "pascal-case": "^3.1.2", "path-case": "^3.0.4", "sentence-case": "^3.0.4", "snake-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A=="], + + "clean-css": ["clean-css@4.2.4", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A=="], + + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "constant-case": ["constant-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case": "^2.0.2" } }, "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ=="], + + "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "header-case": ["header-case@2.0.4", "", { "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" } }, "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q=="], + + "html-minifier": ["html-minifier@4.0.0", "", { "dependencies": { "camel-case": "^3.0.0", "clean-css": "^4.2.1", "commander": "^2.19.0", "he": "^1.2.0", "param-case": "^2.1.1", "relateurl": "^0.2.7", "uglify-js": "^3.5.1" }, "bin": { "html-minifier": "./cli.js" } }, "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "lru-cache": ["@wolfy1339/lru-cache@11.0.2-patch.1", "", {}, "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA=="], + + "marked": ["marked@12.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q=="], + + "mitata": ["mitata@0.1.14", "", {}, "sha512-8kRs0l636eT4jj68PFXOR2D5xl4m56T478g16SzUPOYgkzQU+xaw62guAQxzBPm+SXb15GQi1cCpDxJfkr4CSA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "octokit": ["octokit@3.2.2", "", { "dependencies": { "@octokit/app": "^14.0.2", "@octokit/core": "^5.0.0", "@octokit/oauth-app": "^6.0.0", "@octokit/plugin-paginate-graphql": "^4.0.0", "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", "@octokit/plugin-retry": "^6.0.0", "@octokit/plugin-throttling": "^8.0.0", "@octokit/request-error": "^5.0.0", "@octokit/types": "^13.0.0", "@octokit/webhooks": "^12.3.1" } }, "sha512-7Abo3nADdja8l/aglU6Y3lpnHSfv0tw7gFPiqzry/yCU+2gTAX7R1roJ8hJrxIK+S1j+7iqRJXtmuHJ/UDsBhQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "param-case": ["param-case@2.1.1", "", { "dependencies": { "no-case": "^2.2.0" } }, "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w=="], + + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + + "path-case": ["path-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg=="], + + "peechy": ["peechy@0.4.34", "", { "dependencies": { "change-case": "^4.1.2" }, "bin": { "peechy": "cli.js" } }, "sha512-Cpke/cCqqZHhkyxz7mdqS8ZAGJFUi5icu3ZGqxm9GC7g2VrhH0tmjPhZoWHAN5ghw1m1wq5+2YvfbDSqgC4+Zg=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.3.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "sentence-case": ["sentence-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "universal-github-app-jwt": ["universal-github-app-jwt@1.2.0", "", { "dependencies": { "@types/jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.2" } }, "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g=="], + + "universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "upper-case": ["upper-case@1.1.3", "", {}, "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA=="], + + "upper-case-first": ["upper-case-first@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "@octokit/app/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], + + "@octokit/app/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + + "@octokit/auth-unauthenticated/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + + "@octokit/plugin-throttling/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + + "@octokit/webhooks/@octokit/webhooks-methods": ["@octokit/webhooks-methods@4.1.0", "", {}, "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ=="], + + "camel-case/no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], + + "change-case/camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + + "change-case/param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], + + "constant-case/upper-case": ["upper-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg=="], + + "param-case/no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], + + "@octokit/app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "@octokit/auth-unauthenticated/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "@octokit/plugin-throttling/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "camel-case/no-case/lower-case": ["lower-case@1.1.4", "", {}, "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA=="], + + "param-case/no-case/lower-case": ["lower-case@1.1.4", "", {}, "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA=="], + } +} diff --git a/test/Bun/testdata/dependencies/bun.lock b/test/Bun/testdata/dependencies/bun.lock new file mode 100644 index 0000000000..ad81a953b2 --- /dev/null +++ b/test/Bun/testdata/dependencies/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "deps-test", + "dependencies": { + "express": "^4.18.2", + }, + "devDependencies": { + "typescript": "^5.0.0", + }, + "optionalDependencies": { + "fsevents": "^2.3.3", + }, + }, + }, + "packages": { + "express": ["express@4.18.2", "", { "dependencies": { "accepts": "~1.3.8" } }, "sha512-fake=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34" } }, "sha512-fake=="], + "mime-types": ["mime-types@2.1.35", "", {}, "sha512-fake=="], + "typescript": ["typescript@5.3.3", "", { "bin": { "tsc": "bin/tsc" } }, "sha512-fake=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-fake=="], + }, +} diff --git a/test/Bun/testdata/jsonc/bun.lock b/test/Bun/testdata/jsonc/bun.lock new file mode 100644 index 0000000000..dee5da1e33 --- /dev/null +++ b/test/Bun/testdata/jsonc/bun.lock @@ -0,0 +1,16 @@ +{ + // Single-line comment + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "jsonc-test", + /* Block comment */ + "dependencies": { + "lodash": "^4.17.21", + }, + }, + }, + "packages": { + "lodash": ["lodash@4.17.21", "", {}, "sha512-fake=="], + }, +} diff --git a/test/Bun/testdata/workspaces/bun.lock b/test/Bun/testdata/workspaces/bun.lock new file mode 100644 index 0000000000..65a695d636 --- /dev/null +++ b/test/Bun/testdata/workspaces/bun.lock @@ -0,0 +1,30 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "workspace-root", + "devDependencies": { + "@types/app": "workspace:*", + "typescript": "^5.0.0", + }, + }, + "packages/app": { + "name": "@types/app", + "dependencies": { + "lodash": "^4.17.21", + }, + }, + "packages/utils": { + "name": "utils", + "dependencies": { + "lodash": "^4.17.21", + }, + }, + }, + "packages": { + "@types/app": ["@types/app@workspace:packages/app"], + "utils": ["utils@workspace:packages/utils"], + "lodash": ["lodash@4.17.21", "", {}, "sha512-fake=="], + "typescript": ["typescript@5.3.3", "", { "bin": { "tsc": "bin/tsc" } }, "sha512-fake=="], + }, +} diff --git a/test/Data/Text/JsoncSpec.hs b/test/Data/Text/JsoncSpec.hs new file mode 100644 index 0000000000..512d8a4aff --- /dev/null +++ b/test/Data/Text/JsoncSpec.hs @@ -0,0 +1,133 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Data.Text.JsoncSpec ( + spec, +) where + +import Data.Aeson (eitherDecodeStrict) +import Data.Aeson qualified as Aeson +import Data.String.Conversion (encodeUtf8) +import Data.Text (Text) +import Data.Text.Jsonc (stripJsonc) +import Test.Hspec (Expectation, Spec, describe, expectationFailure, it, shouldBe) +import Text.RawString.QQ (r) + +spec :: Spec +spec = describe "stripJsonc" $ do + describe "stripping" $ do + it "strips single-line comments" $ + stripJsonc + [r|{ +// comment +"a": 1 +}|] + `shouldBeRight` [r|{ +"a": 1 +}|] + + it "strips line comments at end of file" $ + stripJsonc + [r|{"a": 1} +// trailing|] + `shouldBeRight` [r|{"a": 1} +|] + + it "strips block comments" $ + stripJsonc [r|{"a": /* inline */ 1}|] + `shouldBeRight` [r|{"a": 1}|] + + it "strips multi-line block comments" $ + stripJsonc + [r|{ +/* line 1 + line 2 */ +"a": 1 +}|] + `shouldBeRight` [r|{ + +"a": 1 +}|] + + it "strips trailing commas before }" $ + stripJsonc [r|{"a": 1,}|] + `shouldBeRight` [r|{"a": 1}|] + + it "strips trailing commas before ]" $ + stripJsonc [r|[1, 2,]|] + `shouldBeRight` [r|[1, 2]|] + + -- Uses escaped strings because trailing spaces are invisible in raw strings. + it "strips trailing commas with whitespace" $ + stripJsonc "{\"a\": 1 , \n}" + `shouldBeRight` "{\"a\": 1 \n}" + + it "preserves commas between elements" $ + stripJsonc [r|{"a": 1, "b": 2}|] + `shouldBeRight` [r|{"a": 1, "b": 2}|] + + it "preserves // inside strings" $ + stripJsonc [r|{"url": "https://example.com"}|] + `shouldBeRight` [r|{"url": "https://example.com"}|] + + it "preserves commas inside strings" $ + stripJsonc [r|{"msg": "hello, world"}|] + `shouldBeRight` [r|{"msg": "hello, world"}|] + + it "preserves escaped quotes inside strings" $ + stripJsonc [r|{"msg": "say \"hello\""}|] + `shouldBeRight` [r|{"msg": "say \"hello\""}|] + + it "preserves /* */ inside strings" $ + stripJsonc [r|{"a": "/* not a comment */"}|] + `shouldBeRight` [r|{"a": "/* not a comment */"}|] + + it "preserves trailing comma patterns inside strings" $ + stripJsonc [r|{"a": "value,}"}|] + `shouldBeRight` [r|{"a": "value,}"}|] + + it "handles comments and trailing commas together" $ + stripJsonc + [r|{ +// comment +"a": 1, +"b": 2, +}|] + `shouldBeRight` [r|{ +"a": 1, +"b": 2 +}|] + + describe "aeson round-trip" $ do + it "parses JSONC with comments and trailing commas" $ + shouldRoundTrip + [r|{ +// comment +"name": "test", +"version": 1, +/* block */ +"items": [1, 2, 3,], +}|] + [r|{"name": "test", "version": 1, "items": [1, 2, 3]}|] + + it "parses JSONC with comment-like strings" $ + shouldRoundTrip + [r|{"url": "https://example.com/path", "pattern": "/* glob */",}|] + [r|{"url": "https://example.com/path", "pattern": "/* glob */"}|] + +shouldRoundTrip :: Text -> Text -> Expectation +shouldRoundTrip input expectedJson = + case stripJsonc input of + Left err -> expectationFailure $ "stripJsonc failed: " ++ err + Right stripped -> do + let actual = eitherDecodeStrict @Aeson.Value (encodeUtf8 stripped) + let expected = eitherDecodeStrict @Aeson.Value (encodeUtf8 expectedJson) + case (actual, expected) of + (Right a, Right e) -> a `shouldBe` e + (Left err, _) -> expectationFailure $ "Failed to parse stripped output: " ++ err + (_, Left err) -> expectationFailure $ "Failed to parse expected JSON: " ++ err + +shouldBeRight :: (Show a, Show b, Eq b) => Either a b -> b -> Expectation +shouldBeRight (Left err) _ = expectationFailure $ "Expected Right, got Left: " ++ show err +shouldBeRight (Right actual) expected + | actual == expected = pure () + | otherwise = expectationFailure $ "Expected: " ++ show expected ++ "\nGot: " ++ show actual From 0c19687b2f234465e80967af331da7c665eb418b Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Thu, 19 Feb 2026 16:22:18 -0700 Subject: [PATCH 02/13] Remove workspace-level peerDependencies from bun.lock parser Bun does not extract peerDependencies from workspace package.json files, so the field is never populated in bun.lock. Package-level peerDependencies (used for transitive edges) are unaffected. Co-Authored-By: Claude Opus 4.6 --- .../references/strategies/languages/nodejs/bun.md | 15 --------------- src/Strategy/Node/Bun/BunLock.hs | 2 -- 2 files changed, 17 deletions(-) diff --git a/docs/references/strategies/languages/nodejs/bun.md b/docs/references/strategies/languages/nodejs/bun.md index 1550be74ff..5f94dd289e 100644 --- a/docs/references/strategies/languages/nodejs/bun.md +++ b/docs/references/strategies/languages/nodejs/bun.md @@ -58,18 +58,3 @@ Package values are variable-length arrays depending on the resolution type: - Dependencies declared in `devDependencies` of any workspace are labeled as development dependencies. - Dependencies declared in `dependencies` or `optionalDependencies` of any workspace are labeled as production dependencies. - Workspace packages themselves (those with `workspace:` resolutions) are excluded from the final dependency graph. - -## FAQ - -### How do I perform analysis only for bun projects? - -You can explicitly specify an analysis target in `.fossa.yml` file. The example below will exclude all analysis targets except for bun. - -```yaml -# .fossa.yml - -version: 3 -targets: - only: - - type: bun -``` diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index c2084c1c0c..3ac3fabf2f 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -59,7 +59,6 @@ data BunWorkspace = BunWorkspace , wsDependencies :: Map Text Text , wsDevDependencies :: Map Text Text , wsOptionalDependencies :: Map Text Text - , wsPeerDependencies :: Map Text Text } deriving (Show, Eq, Ord) @@ -94,7 +93,6 @@ instance FromJSON BunWorkspace where <*> obj .:? "dependencies" .!= mempty <*> obj .:? "devDependencies" .!= mempty <*> obj .:? "optionalDependencies" .!= mempty - <*> obj .:? "peerDependencies" .!= mempty instance FromJSON BunPackageDeps where parseJSON = withObject "BunPackageDeps" $ \obj -> From 7476747233771d993290e86adb76c6e2b3817feb Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 09:40:43 -0700 Subject: [PATCH 03/13] Fix alphabetical ordering of Data.Text.Jsonc in spectrometer.cabal Co-Authored-By: Claude Opus 4.6 --- spectrometer.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spectrometer.cabal b/spectrometer.cabal index e161503be5..bd3b5d4573 100644 --- a/spectrometer.cabal +++ b/spectrometer.cabal @@ -370,7 +370,6 @@ library Control.Timeout Control.Timeout.Internal Data.Aeson.Extra - Data.Text.Jsonc Data.Conduit.Extra Data.Error Data.FileEmbed.Extra @@ -387,6 +386,7 @@ library Data.String.Conversion Data.Tagged Data.Text.Extra + Data.Text.Jsonc Data.Tracing.Instrument DepTypes Diag.Common From 20740751a660228229b95ea42ad738323f57493a Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 09:57:10 -0700 Subject: [PATCH 04/13] Refactor wsPackageNames to use mapMaybe instead of list comprehension Co-Authored-By: Claude Opus 4.6 --- src/Strategy/Node/Bun/BunLock.hs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 3ac3fabf2f..bfe33e6abb 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -27,6 +27,7 @@ import Data.Aeson ( import Data.Foldable (for_) import Data.Map (Map) import Data.Map qualified as Map +import Data.Maybe (mapMaybe) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as Text @@ -237,8 +238,10 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do wsPackageNames :: Set.Set Text wsPackageNames = Set.fromList - [ name - | pkg <- Map.elems (packages lockfile) - , isWorkspaceRef (pkgResolution pkg) - , let (name, _) = parseResolution (pkgResolution pkg) - ] + . mapMaybe wsPackageName + $ Map.elems (packages lockfile) + + wsPackageName :: BunPackage -> Maybe Text + wsPackageName pkg + | isWorkspaceRef (pkgResolution pkg) = Just . fst $ parseResolution (pkgResolution pkg) + | otherwise = Nothing From e7c7583b67ab1bfef3b5b255486820ef9c9b514c Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 10:56:41 -0700 Subject: [PATCH 05/13] Simplify dependency environment inference and add transitive dev dep test Consolidate toDependency/toDependencyAs/envOf into toDependency, toDependencyWithEnv, and mkDep so parseResolution is called once per package. Add test verifying transitive deps of dev deps are labeled as production, consistent with npm v3 and pnpm. Co-Authored-By: Claude Opus 4.6 --- src/Strategy/Node/Bun/BunLock.hs | 32 +++++++++++++------------ test/Bun/BunLockSpec.hs | 3 +++ test/Bun/testdata/dependencies/bun.lock | 3 ++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index bfe33e6abb..de6d39af4b 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -194,7 +194,7 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do Nothing -> pure () Just pkg | isWorkspaceRef (pkgResolution pkg) -> pure () - | otherwise -> direct $ toDependencyAs env pkg + | otherwise -> direct $ toDependencyWithEnv env pkg transitiveDepNames :: BunPackage -> [Text] transitiveDepNames pkg = @@ -203,11 +203,23 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do <> Map.keys (pkgDepsPeerDependencies $ pkgDeps pkg) -- \| Convert a package to a Dependency, inferring environment from workspace declarations. + -- Only packages directly declared in a workspace's devDependencies are + -- labeled EnvDevelopment. Transitive deps of dev deps get EnvProduction. + -- This is consistent with npm v3 and pnpm, which use per-package flags + -- rather than propagating environment from parents. toDependency :: BunPackage -> Dependency - toDependency pkg = toDependencyAs (envOf pkg) pkg - - toDependencyAs :: DepEnvironment -> BunPackage -> Dependency - toDependencyAs env pkg = + toDependency pkg = + let (name, version) = parseResolution (pkgResolution pkg) + env = if Set.member name devDepNames then EnvDevelopment else EnvProduction + in mkDep name version env + + toDependencyWithEnv :: DepEnvironment -> BunPackage -> Dependency + toDependencyWithEnv env pkg = + let (name, version) = parseResolution (pkgResolution pkg) + in mkDep name version env + + mkDep :: Text -> Text -> DepEnvironment -> Dependency + mkDep name version env = Dependency { dependencyType = NodeJSType , dependencyName = name @@ -216,16 +228,6 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do , dependencyEnvironments = Set.singleton env , dependencyTags = mempty } - where - (name, version) = parseResolution (pkgResolution pkg) - - -- \| Determine the environment for a package based on workspace declarations. - envOf :: BunPackage -> DepEnvironment - envOf pkg - | Set.member name devDepNames = EnvDevelopment - | otherwise = EnvProduction - where - (name, _) = parseResolution (pkgResolution pkg) -- \| Check if a resolution string refers to a workspace package. isWorkspaceRef :: Text -> Bool diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index 8026aae5bf..ac1c0382bf 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -112,6 +112,9 @@ dependenciesSpec path = expectEdge graph (mkProdDep "express" "4.18.2") (mkProdDep "accepts" "1.3.8") expectEdge graph (mkProdDep "accepts" "1.3.8") (mkProdDep "mime-types" "2.1.35") + it "labels transitive deps of dev deps as production" $ do + expectEdge graph (mkDevDep "typescript" "5.3.3") (mkProdDep "semver" "7.6.0") + -- | Workspaces: multiple workspaces, workspace refs, and workspace -- package filtering from the final graph. workspacesSpec :: Path Abs File -> Spec diff --git a/test/Bun/testdata/dependencies/bun.lock b/test/Bun/testdata/dependencies/bun.lock index ad81a953b2..03207c0c15 100644 --- a/test/Bun/testdata/dependencies/bun.lock +++ b/test/Bun/testdata/dependencies/bun.lock @@ -18,7 +18,8 @@ "express": ["express@4.18.2", "", { "dependencies": { "accepts": "~1.3.8" } }, "sha512-fake=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34" } }, "sha512-fake=="], "mime-types": ["mime-types@2.1.35", "", {}, "sha512-fake=="], - "typescript": ["typescript@5.3.3", "", { "bin": { "tsc": "bin/tsc" } }, "sha512-fake=="], + "typescript": ["typescript@5.3.3", "", { "dependencies": { "semver": "^7.5.0" } }, "sha512-fake=="], + "semver": ["semver@7.6.0", "", {}, "sha512-fake=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-fake=="], }, } From 6323f6fd3ba6729e01fd7456c4f5ca90d946e895 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 10:58:09 -0700 Subject: [PATCH 06/13] Clarify env inference comment to explain mechanism Co-Authored-By: Claude Opus 4.6 --- src/Strategy/Node/Bun/BunLock.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index de6d39af4b..602fd91de6 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -203,10 +203,10 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do <> Map.keys (pkgDepsPeerDependencies $ pkgDeps pkg) -- \| Convert a package to a Dependency, inferring environment from workspace declarations. - -- Only packages directly declared in a workspace's devDependencies are - -- labeled EnvDevelopment. Transitive deps of dev deps get EnvProduction. - -- This is consistent with npm v3 and pnpm, which use per-package flags - -- rather than propagating environment from parents. + -- Environment is based on whether the package name appears in any workspace's + -- devDependencies, not on how the package is reached in the dependency graph. + -- This means transitive deps of dev deps get EnvProduction (since they aren't + -- themselves declared in devDependencies). This matches npm v3 and pnpm behavior. toDependency :: BunPackage -> Dependency toDependency pkg = let (name, version) = parseResolution (pkgResolution pkg) From b0c199d4422dcd0281da3caff8ec7bd29d4e6732 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 11:40:03 -0700 Subject: [PATCH 07/13] Use Text IO instead of String IO in BunLockSpec Replace readFile (String) + toText with TextIO.readFile (Text) to match the convention used by other test files in the codebase. Co-Authored-By: Claude Opus 4.6 --- test/Bun/BunLockSpec.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index ac1c0382bf..1e3e8b5c58 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -9,8 +9,9 @@ import Control.Effect.Diagnostics (Diagnostics) import Data.Aeson (eitherDecodeStrict) import Data.Map.Strict qualified as Map import Data.Set qualified as Set -import Data.String.Conversion (encodeUtf8, toText) +import Data.String.Conversion (encodeUtf8) import Data.Text (Text) +import Data.Text.IO qualified as TextIO import Data.Text.Jsonc (stripJsonc) import DepTypes ( DepEnvironment (EnvDevelopment, EnvProduction), @@ -203,8 +204,8 @@ checkGraph path graphSpec = do parseBunLockIO :: Path Abs File -> IO (Either String BunLockfile) parseBunLockIO path = do - contents <- readFile (fromAbsFile path) - case stripJsonc (toText contents) of + contents <- TextIO.readFile (fromAbsFile path) + case stripJsonc contents of Left err -> pure $ Left err Right stripped -> pure $ eitherDecodeStrict (encodeUtf8 stripped) From d0c07af77503e529e2a16cf34e0cf6f076429877 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 13:04:20 -0700 Subject: [PATCH 08/13] Filter dependency types: only include npm and git packages Exclude file, link, workspace, root, and module resolution types from the dependency graph, consistent with how npm, pnpm, and yarn handle local packages. Git/github packages are now reported as GitType. Co-Authored-By: Claude Opus 4.6 --- .../strategies/languages/nodejs/bun.md | 8 +- src/Strategy/Node/Bun/BunLock.hs | 80 +++++++++---------- test/Bun/BunLockSpec.hs | 19 +++-- 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/docs/references/strategies/languages/nodejs/bun.md b/docs/references/strategies/languages/nodejs/bun.md index 5f94dd289e..7da0b923a3 100644 --- a/docs/references/strategies/languages/nodejs/bun.md +++ b/docs/references/strategies/languages/nodejs/bun.md @@ -49,12 +49,16 @@ Package keys use a slash-delimited path for nested `node_modules`: Package values are variable-length arrays depending on the resolution type: - **npm:** `["name@version", "registry", {deps}, "integrity"]` +- **git:** `["name@git+url", {deps}, "hash", "integrity"]` +- **github:** `["name@github:user/repo#ref", {deps}, "resolved"]` - **file:** `["name@file:path", {deps}]` +- **link:** `["name@link:path", {deps}]` - **workspace:** `["name@workspace:path"]` -- **git:** `["name@git+url", {deps}, "hash", "integrity"]` + +Only **npm** and **git/github** packages are included in the dependency graph. +File, link, workspace, root, and module resolutions are excluded. ### Environment Labeling - Dependencies declared in `devDependencies` of any workspace are labeled as development dependencies. - Dependencies declared in `dependencies` or `optionalDependencies` of any workspace are labeled as production dependencies. -- Workspace packages themselves (those with `workspace:` resolutions) are excluded from the final dependency graph. diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 602fd91de6..1ff00d62b5 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -14,7 +14,6 @@ module Strategy.Node.Bun.BunLock ( import Control.Algebra (Has) import Control.Effect.Diagnostics (Diagnostics, context) -import Control.Monad (unless) import Data.Aeson ( FromJSON (parseJSON), Value (Object), @@ -27,21 +26,19 @@ import Data.Aeson ( import Data.Foldable (for_) import Data.Map (Map) import Data.Map qualified as Map -import Data.Maybe (mapMaybe) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as Text import Data.Vector qualified as V import DepTypes ( DepEnvironment (..), - DepType (NodeJSType), + DepType (GitType, NodeJSType), Dependency (..), VerConstraint (CEq), ) import Effect.Grapher (Grapher, deep, direct, edge, evalGrapher, run) import Effect.ReadFS (ReadFS, readContentsJsonc) import Graphing (Graphing) -import Graphing qualified import Path (Abs, File, Path) -- | Bun lockfile (bun.lock) in JSONC format. @@ -160,26 +157,25 @@ analyze file = do -- Strategy: -- 1. Collect all dev dependency names across all workspaces. -- 2. For each workspace, mark its declared dependencies as direct. --- 3. For each non-workspace package, add it as a deep dependency +-- 3. For each supported package (npm, git), add it as a deep dependency -- and create edges to its transitive dependencies. --- 4. Filter out workspace packages from the final graph. +-- Unsupported types (workspace, file, link, root, module) are excluded. buildGraph :: BunLockfile -> Graphing Dependency -buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do +buildGraph lockfile = run . evalGrapher $ do for_ allWorkspaces $ \workspace -> do markDirectDeps EnvProduction workspace.wsDependencies markDirectDeps EnvDevelopment workspace.wsDevDependencies markDirectDeps EnvProduction workspace.wsOptionalDependencies for_ (Map.toList $ packages lockfile) $ \(_, pkg) -> - unless (isWorkspaceRef $ pkgResolution pkg) $ do - let parentDep = toDependency pkg + for_ (toDependency pkg) $ \parentDep -> do deep parentDep for_ (transitiveDepNames pkg) $ \childName -> case Map.lookup childName (packages lockfile) of Nothing -> pure () - Just childPkg - | isWorkspaceRef (pkgResolution childPkg) -> pure () - | otherwise -> edge parentDep (toDependency childPkg) + Just childPkg -> + for_ (toDependency childPkg) $ \childDep -> + edge parentDep childDep where allWorkspaces :: [BunWorkspace] allWorkspaces = Map.elems $ workspaces lockfile @@ -192,9 +188,7 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do for_ (Map.keys deps) $ \depName -> case Map.lookup depName (packages lockfile) of Nothing -> pure () - Just pkg - | isWorkspaceRef (pkgResolution pkg) -> pure () - | otherwise -> direct $ toDependencyWithEnv env pkg + Just pkg -> for_ (toDependencyWithEnv env pkg) direct transitiveDepNames :: BunPackage -> [Text] transitiveDepNames pkg = @@ -207,43 +201,45 @@ buildGraph lockfile = withoutWorkspacePackages . run . evalGrapher $ do -- devDependencies, not on how the package is reached in the dependency graph. -- This means transitive deps of dev deps get EnvProduction (since they aren't -- themselves declared in devDependencies). This matches npm v3 and pnpm behavior. - toDependency :: BunPackage -> Dependency + -- + -- Returns Nothing for unsupported resolution types (workspace, file, link, + -- tarball, root, module). Only npm and git/github packages are included. + toDependency :: BunPackage -> Maybe Dependency toDependency pkg = let (name, version) = parseResolution (pkgResolution pkg) env = if Set.member name devDepNames then EnvDevelopment else EnvProduction - in mkDep name version env + in resolutionToDep name version env - toDependencyWithEnv :: DepEnvironment -> BunPackage -> Dependency + toDependencyWithEnv :: DepEnvironment -> BunPackage -> Maybe Dependency toDependencyWithEnv env pkg = let (name, version) = parseResolution (pkgResolution pkg) - in mkDep name version env - - mkDep :: Text -> Text -> DepEnvironment -> Dependency - mkDep name version env = + in resolutionToDep name version env + + -- \| Convert a parsed resolution to a Dependency based on the version prefix. + -- Only npm (no prefix) and git/github resolutions produce dependencies. + resolutionToDep :: Text -> Text -> DepEnvironment -> Maybe Dependency + resolutionToDep name version env + | "github:" `Text.isPrefixOf` version = Just $ mkDep GitType name (Text.drop 7 version) env + | "git+" `Text.isPrefixOf` version = Just $ mkDep GitType name (Text.drop 4 version) env + | isLocalRef version = Nothing + | otherwise = Just $ mkDep NodeJSType name version env + + -- \| Check if a version string refers to a local/unsupported resolution type. + isLocalRef :: Text -> Bool + isLocalRef v = + "workspace:" `Text.isPrefixOf` v + || "file:" `Text.isPrefixOf` v + || "link:" `Text.isPrefixOf` v + || "root:" `Text.isPrefixOf` v + || "module:" `Text.isPrefixOf` v + + mkDep :: DepType -> Text -> Text -> DepEnvironment -> Dependency + mkDep depType name version env = Dependency - { dependencyType = NodeJSType + { dependencyType = depType , dependencyName = name , dependencyVersion = if Text.null version then Nothing else Just (CEq version) , dependencyLocations = mempty , dependencyEnvironments = Set.singleton env , dependencyTags = mempty } - - -- \| Check if a resolution string refers to a workspace package. - isWorkspaceRef :: Text -> Bool - isWorkspaceRef = Text.isInfixOf "workspace:" - - -- \| Collect workspace package names and filter them from the graph. - withoutWorkspacePackages :: Graphing Dependency -> Graphing Dependency - withoutWorkspacePackages = Graphing.shrink (\dep -> not $ Set.member (dependencyName dep) wsPackageNames) - - wsPackageNames :: Set.Set Text - wsPackageNames = - Set.fromList - . mapMaybe wsPackageName - $ Map.elems (packages lockfile) - - wsPackageName :: BunPackage -> Maybe Text - wsPackageName pkg - | isWorkspaceRef (pkgResolution pkg) = Just . fst $ parseResolution (pkgResolution pkg) - | otherwise = Nothing diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index 1e3e8b5c58..ebbab874a9 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -15,7 +15,7 @@ import Data.Text.IO qualified as TextIO import Data.Text.Jsonc (stripJsonc) import DepTypes ( DepEnvironment (EnvDevelopment, EnvProduction), - DepType (NodeJSType), + DepType (GitType, NodeJSType), Dependency (..), VerConstraint (CEq), ) @@ -178,6 +178,10 @@ bunProjectSpec path = it "creates transitive edges" $ expectEdge graph (mkDevDep "@lezer/cpp" "1.1.3") (mkDevDep "@lezer/common" "1.3.0") + it "includes git dependencies as GitType" $ do + let gitDeps = filter (\d -> dependencyType d == GitType) (Graphing.vertexList graph) + gitDeps `shouldContainDep` mkGitDep "bun-tracestrings" "oven-sh/bun.report#912ca63" + it "excludes workspace packages from graph" $ do let names = map dependencyName (Graphing.vertexList graph) names `shouldNotContain` "@types/bun" @@ -220,15 +224,18 @@ shouldNotContain xs x | otherwise = pure () mkProdDep :: Text -> Text -> Dependency -mkProdDep name version = mkDep name version EnvProduction +mkProdDep name version = mkDep NodeJSType name version EnvProduction mkDevDep :: Text -> Text -> Dependency -mkDevDep name version = mkDep name version EnvDevelopment +mkDevDep name version = mkDep NodeJSType name version EnvDevelopment + +mkGitDep :: Text -> Text -> Dependency +mkGitDep name version = mkDep GitType name version EnvDevelopment -mkDep :: Text -> Text -> DepEnvironment -> Dependency -mkDep name version env = +mkDep :: DepType -> Text -> Text -> DepEnvironment -> Dependency +mkDep depType name version env = Dependency - { dependencyType = NodeJSType + { dependencyType = depType , dependencyName = name , dependencyVersion = Just (CEq version) , dependencyLocations = mempty From c7261200299ad066a81e2c6479fac60481d2b6ea Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 13:51:01 -0700 Subject: [PATCH 09/13] Refactor BunLock to use LabeledGrapher for environment merging Switch from plain Grapher to LabeledGrapher so that vertices are environment-agnostic and environments accumulate as labels. This fixes duplicate vertices when the same package appears in both prod and dev across different workspaces. Also adds git dependency support (github:/git+ resolutions), a mixed-envs test fixture verifying environment merging, simplifies readContentsJsonc per review feedback, and updates changelog/docs. Co-Authored-By: Claude Opus 4.6 --- Changelog.md | 4 + .../strategies/languages/nodejs/bun.md | 3 + src/Effect/ReadFS.hs | 6 +- src/Strategy/Node/Bun/BunLock.hs | 175 ++++++++++++------ test/Bun/BunLockSpec.hs | 90 ++++++--- test/Bun/testdata/git-deps/bun.lock | 18 ++ test/Bun/testdata/mixed-envs/bun.lock | 21 +++ 7 files changed, 234 insertions(+), 83 deletions(-) create mode 100644 test/Bun/testdata/git-deps/bun.lock create mode 100644 test/Bun/testdata/mixed-envs/bun.lock diff --git a/Changelog.md b/Changelog.md index 5072b6981a..1504bd8bce 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # FOSSA CLI Changelog +## Unreleased + +- nodejs: Add support for Bun lockfiles (`bun.lock`). Analyzes npm and git dependencies, workspaces, and environment labeling. ([#1648](https://github.com/fossas/fossa-cli/pull/1648)) + ## 3.15.8 - Snippet scanning: Fix bug where proxies cause POSTs to get redirected to GETs ([#1645](https://github.com/fossas/fossa-cli/pull/1645)) diff --git a/docs/references/strategies/languages/nodejs/bun.md b/docs/references/strategies/languages/nodejs/bun.md index 7da0b923a3..ca6a4571b6 100644 --- a/docs/references/strategies/languages/nodejs/bun.md +++ b/docs/references/strategies/languages/nodejs/bun.md @@ -62,3 +62,6 @@ File, link, workspace, root, and module resolutions are excluded. - Dependencies declared in `devDependencies` of any workspace are labeled as development dependencies. - Dependencies declared in `dependencies` or `optionalDependencies` of any workspace are labeled as production dependencies. +- When the same package appears in both `dependencies` and `devDependencies` across + different workspaces, both environments are recorded on a single graph vertex + (environments accumulate rather than creating duplicate entries). diff --git a/src/Effect/ReadFS.hs b/src/Effect/ReadFS.hs index d6cce36527..10253a6948 100644 --- a/src/Effect/ReadFS.hs +++ b/src/Effect/ReadFS.hs @@ -373,11 +373,9 @@ readContentsJson file = context ("Parsing JSON file '" <> toText (toString file) readContentsJsonc :: (FromJSON a, Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> m a readContentsJsonc file = context ("Parsing JSONC file '" <> toText (toString file) <> "'") $ do contents <- readContentsText file - case stripJsonc contents of + case eitherDecodeStrict . encodeUtf8 =<< stripJsonc contents of Left err -> errSupport (fileParseErrorSupportMsg file) $ fatal $ FileParseError (toString file) (toText err) - Right stripped -> case eitherDecodeStrict (encodeUtf8 stripped) of - Left err -> errSupport (fileParseErrorSupportMsg file) $ fatal $ FileParseError (toString file) (toText err) - Right a -> pure a + Right a -> pure a readContentsToml :: (Toml.Schema.FromValue a, Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> m a readContentsToml file = context ("Parsing TOML file '" <> toText (toString file) <> "'") $ do diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 1ff00d62b5..2fb9f91a4c 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -9,10 +9,13 @@ module Strategy.Node.Bun.BunLock ( BunWorkspace (..), BunPackage (..), BunPackageDeps (..), + BunDepVertex (..), + BunDepLabel (..), parseResolution, ) where import Control.Algebra (Has) +import Control.Applicative ((<|>)) import Control.Effect.Diagnostics (Diagnostics, context) import Data.Aeson ( FromJSON (parseJSON), @@ -35,36 +38,75 @@ import DepTypes ( DepType (GitType, NodeJSType), Dependency (..), VerConstraint (CEq), + insertEnvironment, ) -import Effect.Grapher (Grapher, deep, direct, edge, evalGrapher, run) +import Effect.Grapher (LabeledGrapher, deep, direct, edge, label, run, withLabeling) import Effect.ReadFS (ReadFS, readContentsJsonc) import Graphing (Graphing) import Path (Abs, File, Path) -- | Bun lockfile (bun.lock) in JSONC format. -- +-- Implemented against lockfile version 1 (bun v1.2.x). +-- -- See @docs/references/strategies/languages/nodejs/bun.md@ for the full -- lockfile format documentation. data BunLockfile = BunLockfile { lockfileVersion :: Int - , workspaces :: Map Text BunWorkspace - , packages :: Map Text BunPackage + , workspaces :: Map WorkspacePath BunWorkspace + , packages :: Map PackageName BunPackage } deriving (Show, Eq) +-- | Relative path from the project root to the workspace directory. +-- The root workspace uses an empty string @""@. +type WorkspacePath = Text + +-- | Package name as it appears in lockfile keys, e.g. @"lodash"@ or @"@scope/pkg"@. +type PackageName = Text + +-- | Version constraint as declared in package.json, e.g. @"^4.17.21"@. +type VersionConstraint = Text + +-- | Graph vertex: identifies a package without environment info. +data BunDepVertex = BunDepVertex + { bunDepType :: DepType + , bunDepName :: Text + , bunDepVersion :: Maybe VerConstraint + } + deriving (Eq, Ord, Show) + +newtype BunDepLabel = BunDepEnv DepEnvironment + deriving (Eq, Ord, Show) + +-- | Convert a vertex and its accumulated labels into a 'Dependency'. +vertexToDependency :: BunDepVertex -> Set.Set BunDepLabel -> Dependency +vertexToDependency vertex = foldr applyLabel base + where + base = + Dependency + { dependencyType = bunDepType vertex + , dependencyName = bunDepName vertex + , dependencyVersion = bunDepVersion vertex + , dependencyLocations = mempty + , dependencyEnvironments = mempty + , dependencyTags = mempty + } + applyLabel (BunDepEnv env) = insertEnvironment env + data BunWorkspace = BunWorkspace - { wsName :: Text - , wsDependencies :: Map Text Text - , wsDevDependencies :: Map Text Text - , wsOptionalDependencies :: Map Text Text + { wsName :: PackageName + , wsDependencies :: Map PackageName VersionConstraint + , wsDevDependencies :: Map PackageName VersionConstraint + , wsOptionalDependencies :: Map PackageName VersionConstraint } deriving (Show, Eq, Ord) -- | Resolved dependencies extracted from a package's info object. data BunPackageDeps = BunPackageDeps - { pkgDepsDependencies :: Map Text Text - , pkgDepsOptionalDependencies :: Map Text Text - , pkgDepsPeerDependencies :: Map Text Text + { pkgDepsDependencies :: Map PackageName VersionConstraint + , pkgDepsOptionalDependencies :: Map PackageName VersionConstraint + , pkgDepsPeerDependencies :: Map PackageName VersionConstraint } deriving (Show, Eq) @@ -156,90 +198,107 @@ analyze file = do -- -- Strategy: -- 1. Collect all dev dependency names across all workspaces. --- 2. For each workspace, mark its declared dependencies as direct. --- 3. For each supported package (npm, git), add it as a deep dependency --- and create edges to its transitive dependencies. +-- 2. For each workspace, mark its declared dependencies as direct +-- and label them with their environment. +-- 3. For each supported package (npm, git), add it as a deep dependency, +-- label it with its inferred environment, and create edges to its +-- transitive dependencies. -- Unsupported types (workspace, file, link, root, module) are excluded. +-- +-- Uses 'LabeledGrapher' so that vertices are environment-agnostic and +-- environments accumulate as labels, avoiding duplicate vertices when +-- the same package appears in both prod and dev across workspaces. buildGraph :: BunLockfile -> Graphing Dependency -buildGraph lockfile = run . evalGrapher $ do +buildGraph lockfile = run . withLabeling vertexToDependency $ do for_ allWorkspaces $ \workspace -> do markDirectDeps EnvProduction workspace.wsDependencies markDirectDeps EnvDevelopment workspace.wsDevDependencies markDirectDeps EnvProduction workspace.wsOptionalDependencies - for_ (Map.toList $ packages lockfile) $ \(_, pkg) -> - for_ (toDependency pkg) $ \parentDep -> do - deep parentDep + for_ (packages lockfile) $ \pkg -> + for_ (toVertex pkg) $ \parentVertex -> do + let (name, _) = parseResolution (pkgResolution pkg) + inferredEnv = if Set.member name devDepNames then EnvDevelopment else EnvProduction + deep parentVertex + label parentVertex (BunDepEnv inferredEnv) for_ (transitiveDepNames pkg) $ \childName -> case Map.lookup childName (packages lockfile) of Nothing -> pure () Just childPkg -> - for_ (toDependency childPkg) $ \childDep -> - edge parentDep childDep + for_ (toVertex childPkg) $ \childVertex -> + edge parentVertex childVertex where allWorkspaces :: [BunWorkspace] allWorkspaces = Map.elems $ workspaces lockfile - devDepNames :: Set.Set Text + devDepNames :: Set.Set PackageName devDepNames = Set.fromList $ concatMap (Map.keys . wsDevDependencies) allWorkspaces - markDirectDeps :: (Has (Grapher Dependency) sig m) => DepEnvironment -> Map Text Text -> m () + markDirectDeps :: (Has (LabeledGrapher BunDepVertex BunDepLabel) sig m) => DepEnvironment -> Map PackageName VersionConstraint -> m () markDirectDeps env deps = for_ (Map.keys deps) $ \depName -> case Map.lookup depName (packages lockfile) of Nothing -> pure () - Just pkg -> for_ (toDependencyWithEnv env pkg) direct + Just pkg -> for_ (toVertex pkg) $ \vertex -> do + direct vertex + label vertex (BunDepEnv env) - transitiveDepNames :: BunPackage -> [Text] + transitiveDepNames :: BunPackage -> [PackageName] transitiveDepNames pkg = Map.keys (pkgDepsDependencies $ pkgDeps pkg) <> Map.keys (pkgDepsOptionalDependencies $ pkgDeps pkg) <> Map.keys (pkgDepsPeerDependencies $ pkgDeps pkg) - -- \| Convert a package to a Dependency, inferring environment from workspace declarations. - -- Environment is based on whether the package name appears in any workspace's - -- devDependencies, not on how the package is reached in the dependency graph. - -- This means transitive deps of dev deps get EnvProduction (since they aren't - -- themselves declared in devDependencies). This matches npm v3 and pnpm behavior. + -- | Convert a package to a vertex (environment-agnostic). -- -- Returns Nothing for unsupported resolution types (workspace, file, link, -- tarball, root, module). Only npm and git/github packages are included. - toDependency :: BunPackage -> Maybe Dependency - toDependency pkg = + toVertex :: BunPackage -> Maybe BunDepVertex + toVertex pkg = let (name, version) = parseResolution (pkgResolution pkg) - env = if Set.member name devDepNames then EnvDevelopment else EnvProduction - in resolutionToDep name version env + in resolutionToVertex name version - toDependencyWithEnv :: DepEnvironment -> BunPackage -> Maybe Dependency - toDependencyWithEnv env pkg = - let (name, version) = parseResolution (pkgResolution pkg) - in resolutionToDep name version env - - -- \| Convert a parsed resolution to a Dependency based on the version prefix. - -- Only npm (no prefix) and git/github resolutions produce dependencies. - resolutionToDep :: Text -> Text -> DepEnvironment -> Maybe Dependency - resolutionToDep name version env - | "github:" `Text.isPrefixOf` version = Just $ mkDep GitType name (Text.drop 7 version) env - | "git+" `Text.isPrefixOf` version = Just $ mkDep GitType name (Text.drop 4 version) env - | isLocalRef version = Nothing - | otherwise = Just $ mkDep NodeJSType name version env - - -- \| Check if a version string refers to a local/unsupported resolution type. - isLocalRef :: Text -> Bool - isLocalRef v = + -- | Convert a parsed resolution to a vertex based on the version prefix. + -- Only npm (no prefix) and git resolutions produce vertices. + resolutionToVertex :: Text -> Text -> Maybe BunDepVertex + resolutionToVertex name version = case stripGitPrefix version of + Just ref -> Just $ mkGitVertex ref + Nothing + | isUnsupportedRef version -> Nothing + | otherwise -> Just $ mkVertex NodeJSType name version + + -- | Strip a known git hosting prefix from a version string. + stripGitPrefix :: Text -> Maybe Text + stripGitPrefix v = + Text.stripPrefix "github:" v + <|> Text.stripPrefix "gitlab:" v + <|> Text.stripPrefix "bitbucket:" v + <|> Text.stripPrefix "git+" v + + -- | Build a GitType vertex from a git reference like @"user/repo#ref"@ + -- or @"https://github.com/user/repo.git#ref"@. + mkGitVertex :: Text -> BunDepVertex + mkGitVertex ref = + let (repo, refPart) = Text.breakOn "#" ref + in mkVertex GitType repo (Text.drop 1 refPart) + + -- | Check if a version string refers to a local/unsupported resolution type. + isUnsupportedRef :: Text -> Bool + isUnsupportedRef v = "workspace:" `Text.isPrefixOf` v || "file:" `Text.isPrefixOf` v || "link:" `Text.isPrefixOf` v || "root:" `Text.isPrefixOf` v || "module:" `Text.isPrefixOf` v + || "https://" `Text.isPrefixOf` v + || "http://" `Text.isPrefixOf` v + || "./" `Text.isPrefixOf` v + || "../" `Text.isPrefixOf` v - mkDep :: DepType -> Text -> Text -> DepEnvironment -> Dependency - mkDep depType name version env = - Dependency - { dependencyType = depType - , dependencyName = name - , dependencyVersion = if Text.null version then Nothing else Just (CEq version) - , dependencyLocations = mempty - , dependencyEnvironments = Set.singleton env - , dependencyTags = mempty + mkVertex :: DepType -> Text -> Text -> BunDepVertex + mkVertex depType name version = + BunDepVertex + { bunDepType = depType + , bunDepName = name + , bunDepVersion = if Text.null version then Nothing else Just (CEq version) } diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index ebbab874a9..ad23720505 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -1,11 +1,10 @@ {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} module Bun.BunLockSpec ( spec, ) where -import Control.Algebra (Has) -import Control.Effect.Diagnostics (Diagnostics) import Data.Aeson (eitherDecodeStrict) import Data.Map.Strict qualified as Map import Data.Set qualified as Set @@ -19,7 +18,7 @@ import DepTypes ( Dependency (..), VerConstraint (CEq), ) -import Effect.ReadFS (ReadFS, readContentsJsonc) +import Effect.ReadFS (readContentsJsonc) import GraphUtil (expectEdge) import Graphing (Graphing) import Graphing qualified @@ -44,12 +43,16 @@ spec = do let depsPath = testdata $(mkRelFile "dependencies/bun.lock") let wsPath = testdata $(mkRelFile "workspaces/bun.lock") let bunProjectPath = testdata $(mkRelFile "bun-project/bun.lock") + let gitDepsPath = testdata $(mkRelFile "git-deps/bun.lock") + let mixedEnvsPath = testdata $(mkRelFile "mixed-envs/bun.lock") parseResolutionSpec jsoncSpec jsoncPath dependenciesSpec depsPath workspacesSpec wsPath bunProjectSpec bunProjectPath + gitDepsSpec gitDepsPath + mixedEnvsSpec mixedEnvsPath parseResolutionSpec :: Spec parseResolutionSpec = describe "parseResolution" $ do @@ -65,16 +68,19 @@ parseResolutionSpec = describe "parseResolution" $ do it "parses workspace references" $ parseResolution "pkg@workspace:packages/a" `shouldBe` ("pkg", "workspace:packages/a") - it "parses git references" $ + it "parses github references" $ parseResolution "pkg@github:user/repo#abc123" `shouldBe` ("pkg", "github:user/repo#abc123") + it "parses git+ references" $ + parseResolution "pkg@git+https://github.com/user/repo#abc123" `shouldBe` ("pkg", "git+https://github.com/user/repo#abc123") + -- | JSONC: verifies that line comments, block comments, and trailing commas -- are stripped correctly and the result parses as a valid bun lockfile. jsoncSpec :: Path Abs File -> Spec jsoncSpec path = describe "jsonc" $ do it' "parses bun.lock with comments and trailing commas" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path lockfileVersion lockfile `shouldBe'` 1 wsName (workspaces lockfile Map.! "") `shouldBe'` "jsonc-test" wsDependencies (workspaces lockfile Map.! "") `shouldBe'` Map.fromList [("lodash", "^4.17.21")] @@ -85,14 +91,14 @@ dependenciesSpec :: Path Abs File -> Spec dependenciesSpec path = describe "dependencies" $ do it' "parses production, dev, and optional dependencies" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path let rootWs = workspaces lockfile Map.! "" wsDependencies rootWs `shouldBe'` Map.fromList [("express", "^4.18.2")] wsDevDependencies rootWs `shouldBe'` Map.fromList [("typescript", "^5.0.0")] wsOptionalDependencies rootWs `shouldBe'` Map.fromList [("fsevents", "^2.3.3")] it' "parses transitive dependency info" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path let expressPkg = packages lockfile Map.! "express" pkgDepsDependencies (pkgDeps expressPkg) `shouldBe'` Map.fromList [("accepts", "~1.3.8")] @@ -122,7 +128,7 @@ workspacesSpec :: Path Abs File -> Spec workspacesSpec path = describe "workspaces" $ do it' "parses multiple workspaces" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path let ws = workspaces lockfile Map.size ws `shouldBe'` 3 wsName (ws Map.! "") `shouldBe'` "workspace-root" @@ -130,7 +136,7 @@ workspacesSpec path = wsName (ws Map.! "packages/utils") `shouldBe'` "utils" it' "parses workspace package resolutions" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path pkgResolution (packages lockfile Map.! "@types/app") `shouldBe'` "@types/app@workspace:packages/app" pkgResolution (packages lockfile Map.! "utils") `shouldBe'` "utils@workspace:packages/utils" @@ -153,18 +159,18 @@ bunProjectSpec :: Path Abs File -> Spec bunProjectSpec path = describe "bun-project" $ do it' "parses workspaces" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path Map.size (workspaces lockfile) `shouldBe'` 3 wsName (workspaces lockfile Map.! "") `shouldBe'` "bun" it' "parses scoped and workspace package resolutions" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path let pkgs = packages lockfile pkgResolution (pkgs Map.! "@types/bun") `shouldBe'` "@types/bun@workspace:packages/@types/bun" pkgResolution (pkgs Map.! "esbuild") `shouldBe'` "esbuild@0.21.5" it' "parses transitive dependency info" $ do - lockfile <- parseBunLockEffect path + lockfile <- readContentsJsonc @BunLockfile path let lezerCpp = packages lockfile Map.! "@lezer/cpp" Map.member "@lezer/common" (pkgDepsDependencies $ pkgDeps lezerCpp) `shouldBe'` True @@ -180,21 +186,49 @@ bunProjectSpec path = it "includes git dependencies as GitType" $ do let gitDeps = filter (\d -> dependencyType d == GitType) (Graphing.vertexList graph) - gitDeps `shouldContainDep` mkGitDep "bun-tracestrings" "oven-sh/bun.report#912ca63" + gitDeps `shouldContainDep` mkGitDep "oven-sh/bun.report" "912ca63" it "excludes workspace packages from graph" $ do let names = map dependencyName (Graphing.vertexList graph) names `shouldNotContain` "@types/bun" names `shouldNotContain` "bun-types" --- | Parse a bun.lock in the effect stack (for it' tests). -parseBunLockEffect :: - ( Has ReadFS sig m - , Has Diagnostics sig m - ) => - Path Abs File -> - m BunLockfile -parseBunLockEffect = readContentsJsonc +-- | Git dependencies: github: and git+ resolution types. +gitDepsSpec :: Path Abs File -> Spec +gitDepsSpec path = + describe "git-deps" $ do + describe "graph" $ do + checkGraph path $ \graph -> do + let directDeps = Graphing.directList graph + + it "includes github: dependencies as GitType with repo as name and ref as version" $ + directDeps `shouldContainDep` mkGitDep' EnvProduction "user/repo" "abc123" + + it "includes git+ dependencies as GitType with URL as name and ref as version" $ + directDeps `shouldContainDep` mkGitDep' EnvProduction "https://github.com/other/project.git" "def456" + + it "creates edges from git+ deps to their transitive deps" $ + expectEdge + graph + (mkGitDep' EnvProduction "https://github.com/other/project.git" "def456") + (mkProdDep "lodash" "4.17.21") + +-- | Mixed environments: the same package appears as a production dependency +-- in one workspace and a dev dependency in another. With LabeledGrapher the +-- environments merge into a single vertex rather than creating duplicates. +mixedEnvsSpec :: Path Abs File -> Spec +mixedEnvsSpec path = + describe "mixed-envs" $ do + describe "graph" $ do + checkGraph path $ \graph -> do + let directDeps = Graphing.directList graph + + it "merges environments when a package is prod in one workspace and dev in another" $ do + directDeps `shouldContainDep` mkBothEnvsDep "lodash" "4.17.21" + + it "produces a single vertex for the package" $ do + let lodashVertices = filter (\d -> dependencyName d == "lodash") (Graphing.vertexList graph) + length lodashVertices `shouldBe` 1 -- | Parse a bun.lock in IO for graph tests (outside the effect stack). checkGraph :: Path Abs File -> (Graphing Dependency -> Spec) -> Spec @@ -232,6 +266,20 @@ mkDevDep name version = mkDep NodeJSType name version EnvDevelopment mkGitDep :: Text -> Text -> Dependency mkGitDep name version = mkDep GitType name version EnvDevelopment +mkGitDep' :: DepEnvironment -> Text -> Text -> Dependency +mkGitDep' env name version = mkDep GitType name version env + +mkBothEnvsDep :: Text -> Text -> Dependency +mkBothEnvsDep name version = + Dependency + { dependencyType = NodeJSType + , dependencyName = name + , dependencyVersion = Just (CEq version) + , dependencyLocations = mempty + , dependencyEnvironments = Set.fromList [EnvProduction, EnvDevelopment] + , dependencyTags = mempty + } + mkDep :: DepType -> Text -> Text -> DepEnvironment -> Dependency mkDep depType name version env = Dependency diff --git a/test/Bun/testdata/git-deps/bun.lock b/test/Bun/testdata/git-deps/bun.lock new file mode 100644 index 0000000000..67d2ab49d9 --- /dev/null +++ b/test/Bun/testdata/git-deps/bun.lock @@ -0,0 +1,18 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "git-deps-test", + "dependencies": { + "my-github-dep": "github:user/repo#abc123", + "my-git-dep": "git+https://github.com/other/project.git#def456", + "lodash": "^4.17.21", + }, + }, + }, + "packages": { + "my-github-dep": ["my-github-dep@github:user/repo#abc123", "", {}, ""], + "my-git-dep": ["my-git-dep@git+https://github.com/other/project.git#def456", "", { "dependencies": { "lodash": "^4.17.21" } }, ""], + "lodash": ["lodash@4.17.21", "", {}, "sha512-fake=="], + }, +} diff --git a/test/Bun/testdata/mixed-envs/bun.lock b/test/Bun/testdata/mixed-envs/bun.lock new file mode 100644 index 0000000000..9f097aa35b --- /dev/null +++ b/test/Bun/testdata/mixed-envs/bun.lock @@ -0,0 +1,21 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "mixed-envs-root", + "dependencies": { + "lodash": "^4.17.21", + }, + }, + "packages/tools": { + "name": "tools", + "devDependencies": { + "lodash": "^4.17.21", + }, + }, + }, + "packages": { + "tools": ["tools@workspace:packages/tools"], + "lodash": ["lodash@4.17.21", "", {}, "sha512-fake=="], + }, +} From 61a5ce9d4544b31bc72c124746085af6f7d9f420 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 13:54:05 -0700 Subject: [PATCH 10/13] Remove unused lockfileVersion field from BunLockfile The field was parsed but never used in analysis. Remove it per review feedback to avoid keeping dead code. Co-Authored-By: Claude Opus 4.6 --- src/Strategy/Node/Bun/BunLock.hs | 6 ++---- test/Bun/BunLockSpec.hs | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 2fb9f91a4c..996068459e 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -52,8 +52,7 @@ import Path (Abs, File, Path) -- See @docs/references/strategies/languages/nodejs/bun.md@ for the full -- lockfile format documentation. data BunLockfile = BunLockfile - { lockfileVersion :: Int - , workspaces :: Map WorkspacePath BunWorkspace + { workspaces :: Map WorkspacePath BunWorkspace , packages :: Map PackageName BunPackage } deriving (Show, Eq) @@ -122,8 +121,7 @@ data BunPackage = BunPackage instance FromJSON BunLockfile where parseJSON = withObject "BunLockfile" $ \obj -> BunLockfile - <$> obj .: "lockfileVersion" - <*> obj .:? "workspaces" .!= mempty + <$> obj .:? "workspaces" .!= mempty <*> obj .:? "packages" .!= mempty instance FromJSON BunWorkspace where diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index ad23720505..85600dd082 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -81,7 +81,6 @@ jsoncSpec path = describe "jsonc" $ do it' "parses bun.lock with comments and trailing commas" $ do lockfile <- readContentsJsonc @BunLockfile path - lockfileVersion lockfile `shouldBe'` 1 wsName (workspaces lockfile Map.! "") `shouldBe'` "jsonc-test" wsDependencies (workspaces lockfile Map.! "") `shouldBe'` Map.fromList [("lodash", "^4.17.21")] From be99f59ac82ef89970e240b82cef740b6253a495 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 13:55:33 -0700 Subject: [PATCH 11/13] Revert "Remove unused lockfileVersion field from BunLockfile" This reverts commit 61a5ce9d4544b31bc72c124746085af6f7d9f420. --- src/Strategy/Node/Bun/BunLock.hs | 6 ++++-- test/Bun/BunLockSpec.hs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 996068459e..2fb9f91a4c 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -52,7 +52,8 @@ import Path (Abs, File, Path) -- See @docs/references/strategies/languages/nodejs/bun.md@ for the full -- lockfile format documentation. data BunLockfile = BunLockfile - { workspaces :: Map WorkspacePath BunWorkspace + { lockfileVersion :: Int + , workspaces :: Map WorkspacePath BunWorkspace , packages :: Map PackageName BunPackage } deriving (Show, Eq) @@ -121,7 +122,8 @@ data BunPackage = BunPackage instance FromJSON BunLockfile where parseJSON = withObject "BunLockfile" $ \obj -> BunLockfile - <$> obj .:? "workspaces" .!= mempty + <$> obj .: "lockfileVersion" + <*> obj .:? "workspaces" .!= mempty <*> obj .:? "packages" .!= mempty instance FromJSON BunWorkspace where diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index 85600dd082..ad23720505 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -81,6 +81,7 @@ jsoncSpec path = describe "jsonc" $ do it' "parses bun.lock with comments and trailing commas" $ do lockfile <- readContentsJsonc @BunLockfile path + lockfileVersion lockfile `shouldBe'` 1 wsName (workspaces lockfile Map.! "") `shouldBe'` "jsonc-test" wsDependencies (workspaces lockfile Map.! "") `shouldBe'` Map.fromList [("lodash", "^4.17.21")] From 43cbfe441774bb65ef5342f6734f964614e06210 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 13:57:26 -0700 Subject: [PATCH 12/13] Add comment explaining lockfileVersion is kept for debug bundles Co-Authored-By: Claude Opus 4.6 --- src/Strategy/Node/Bun/BunLock.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 2fb9f91a4c..fcba92a9b8 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -53,6 +53,7 @@ import Path (Abs, File, Path) -- lockfile format documentation. data BunLockfile = BunLockfile { lockfileVersion :: Int + -- ^ Not used for analysis, but included in debug bundles. , workspaces :: Map WorkspacePath BunWorkspace , packages :: Map PackageName BunPackage } From f52103bdc4347dfef966b1ff9e8b274eb2b9561b Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 20 Feb 2026 14:13:38 -0700 Subject: [PATCH 13/13] Fix fourmolu formatting in BunLock.hs Co-Authored-By: Claude Opus 4.6 --- src/Strategy/Node/Bun/BunLock.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index fcba92a9b8..8574c6f14a 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -250,7 +250,7 @@ buildGraph lockfile = run . withLabeling vertexToDependency $ do <> Map.keys (pkgDepsOptionalDependencies $ pkgDeps pkg) <> Map.keys (pkgDepsPeerDependencies $ pkgDeps pkg) - -- | Convert a package to a vertex (environment-agnostic). + -- \| Convert a package to a vertex (environment-agnostic). -- -- Returns Nothing for unsupported resolution types (workspace, file, link, -- tarball, root, module). Only npm and git/github packages are included. @@ -259,7 +259,7 @@ buildGraph lockfile = run . withLabeling vertexToDependency $ do let (name, version) = parseResolution (pkgResolution pkg) in resolutionToVertex name version - -- | Convert a parsed resolution to a vertex based on the version prefix. + -- \| Convert a parsed resolution to a vertex based on the version prefix. -- Only npm (no prefix) and git resolutions produce vertices. resolutionToVertex :: Text -> Text -> Maybe BunDepVertex resolutionToVertex name version = case stripGitPrefix version of @@ -268,7 +268,7 @@ buildGraph lockfile = run . withLabeling vertexToDependency $ do | isUnsupportedRef version -> Nothing | otherwise -> Just $ mkVertex NodeJSType name version - -- | Strip a known git hosting prefix from a version string. + -- \| Strip a known git hosting prefix from a version string. stripGitPrefix :: Text -> Maybe Text stripGitPrefix v = Text.stripPrefix "github:" v @@ -276,14 +276,14 @@ buildGraph lockfile = run . withLabeling vertexToDependency $ do <|> Text.stripPrefix "bitbucket:" v <|> Text.stripPrefix "git+" v - -- | Build a GitType vertex from a git reference like @"user/repo#ref"@ + -- \| Build a GitType vertex from a git reference like @"user/repo#ref"@ -- or @"https://github.com/user/repo.git#ref"@. mkGitVertex :: Text -> BunDepVertex mkGitVertex ref = let (repo, refPart) = Text.breakOn "#" ref in mkVertex GitType repo (Text.drop 1 refPart) - -- | Check if a version string refers to a local/unsupported resolution type. + -- \| Check if a version string refers to a local/unsupported resolution type. isUnsupportedRef :: Text -> Bool isUnsupportedRef v = "workspace:" `Text.isPrefixOf` v