@lavamoat/aa
Advanced tools
Comparing version 3.1.5 to 4.0.0
# Changelog | ||
## [4.0.0](https://github.com/LavaMoat/LavaMoat/compare/aa-v3.1.5...aa-v4.0.0) (2023-10-18) | ||
### ⚠ BREAKING CHANGES | ||
* The minimum supported Node.js version is now v16.20.0. | ||
### Features | ||
* **aa:** ship generated types ([#638](https://github.com/LavaMoat/LavaMoat/issues/638)) ([d8d5996](https://github.com/LavaMoat/LavaMoat/commit/d8d5996c82c3bca21bd3091bc1f7b3af8db5f591)) | ||
### Bug Fixes | ||
* drop Node.js v14 ([#729](https://github.com/LavaMoat/LavaMoat/issues/729)) ([10c667b](https://github.com/LavaMoat/LavaMoat/commit/10c667bd88eaabf60a8fd8e4493cc7676848b201)) | ||
## [3.1.5](https://github.com/LavaMoat/LavaMoat/compare/aa-v3.1.4...aa-v3.1.5) (2023-09-14) | ||
@@ -4,0 +20,0 @@ |
{ | ||
"name": "@lavamoat/aa", | ||
"version": "3.1.5", | ||
"version": "4.0.0", | ||
"main": "src/index.js", | ||
@@ -17,2 +17,5 @@ "bin": { | ||
}, | ||
"devDependencies": { | ||
"@types/resolve": "^1.20.2" | ||
}, | ||
"dependencies": { | ||
@@ -27,3 +30,3 @@ "resolve": "^1.22.3" | ||
"engines": { | ||
"node": ">=14.0.0" | ||
"node": "^16.20.0 || ^18.0.0 || ^20.0.0" | ||
}, | ||
@@ -36,3 +39,3 @@ "ava": { | ||
}, | ||
"gitHead": "5fb4a8824b4100150f891fabf17448a77de48498" | ||
"types": "./types/index.d.ts" | ||
} |
@@ -11,3 +11,3 @@ ### @lavamoat/aa | ||
- consistent across platform and package manager | ||
- consistent whether only prod dependencies or dev and prod dependencies are installed | ||
- consistent whether only prod dependencies or dev and prod dependencies are installed | ||
@@ -21,2 +21,3 @@ #### scheme | ||
with the logical paths: | ||
- `abc>ijk>xyz` | ||
@@ -23,0 +24,0 @@ - `abc>xyz` |
@@ -5,3 +5,2 @@ #!/usr/bin/env node | ||
start().catch((err) => { | ||
@@ -12,6 +11,9 @@ console.error(err) | ||
async function start () { | ||
async function start() { | ||
// log all package names, optionally filtered by a package self-name | ||
const [,,filterKey] = process.argv | ||
const canonicalNameMap = await loadCanonicalNameMap({ rootDir: process.cwd(), includeDevDeps: true }) | ||
const [, , filterKey] = process.argv | ||
const canonicalNameMap = await loadCanonicalNameMap({ | ||
rootDir: process.cwd(), | ||
includeDevDeps: true, | ||
}) | ||
for (const logicalName of canonicalNameMap.values()) { | ||
@@ -18,0 +20,0 @@ if (filterKey !== undefined) { |
156
src/index.js
@@ -0,1 +1,3 @@ | ||
'use strict' | ||
const { readFileSync } = require('fs') | ||
@@ -13,3 +15,10 @@ const path = require('path') | ||
function createPerformantResolve () { | ||
/** | ||
* @returns {Resolver} | ||
*/ | ||
function createPerformantResolve() { | ||
/** | ||
* @param {string} self | ||
* @returns {(readFileSync: (file: string) => (string|{toString(): string}), pkgfile: string) => Record<string,unknown>} | ||
*/ | ||
const readPackageWithout = (self) => (readFileSync, pkgfile) => { | ||
@@ -23,5 +32,6 @@ // avoid loading the package.json we're just trying to resolve | ||
try { | ||
// @ts-expect-error - JSON.parse calls toString() on its parameter if not given a string | ||
var pkg = JSON.parse(body) | ||
return pkg | ||
} catch (jsonErr) { } | ||
} catch (jsonErr) {} | ||
} | ||
@@ -39,7 +49,7 @@ | ||
/** | ||
* @param {object} options | ||
* @returns {Promise<Map<string, string>>} | ||
* @param {LoadCanonicalNameMapOpts} options | ||
* @returns {Promise<CanonicalNameMap>} | ||
*/ | ||
async function loadCanonicalNameMap({ rootDir, includeDevDeps, resolve } = {}) { | ||
const canonicalNameMap = new Map() | ||
async function loadCanonicalNameMap({ rootDir, includeDevDeps, resolve }) { | ||
const canonicalNameMap = /** @type {CanonicalNameMap} */ (new Map()) | ||
// performant resolve avoids loading package.jsons if their path is what's being resolved, | ||
@@ -50,3 +60,7 @@ // offering 2x performance improvement compared to using original resolve | ||
// walk tree | ||
const logicalPathMap = walkDependencyTreeForBestLogicalPaths({ packageDir: rootDir, includeDevDeps, resolve, canonicalNameMap }) | ||
const logicalPathMap = walkDependencyTreeForBestLogicalPaths({ | ||
packageDir: rootDir, | ||
includeDevDeps, | ||
resolve, | ||
}) | ||
//convert dependency paths to canonical package names | ||
@@ -63,2 +77,8 @@ for (const [packageDir, logicalPathParts] of logicalPathMap.entries()) { | ||
/** | ||
* @param {Resolver} resolve | ||
* @param {string} depName | ||
* @param {string} packageDir | ||
* @returns {string|undefined} | ||
*/ | ||
function wrappedResolveSync(resolve, depName, packageDir) { | ||
@@ -69,3 +89,3 @@ const depRelativePackageJsonPath = path.join(depName, 'package.json') | ||
} catch (err) { | ||
if (!err.message.includes('Cannot find module')) { | ||
if (!(/** @type {Error} */ (err).message.includes('Cannot find module'))) { | ||
throw err | ||
@@ -77,2 +97,8 @@ } | ||
} | ||
/** | ||
* @param {string} packageDir | ||
* @param {boolean} includeDevDeps | ||
* @returns {string[]} | ||
*/ | ||
function getDependencies(packageDir, includeDevDeps) { | ||
@@ -91,13 +117,26 @@ const packageJsonPath = path.join(packageDir, 'package.json') | ||
/** @type {WalkDepTreeOpts[]} */ | ||
let currentLevelTodos | ||
/** @type {WalkDepTreeOpts[]} */ | ||
let nextLevelTodos | ||
/** | ||
* @param {object} options | ||
* @returns {Map<{packageDir: string, logicalPathParts: string[]}>} | ||
* @param {WalkDepTreeOpts} options | ||
* @returns {Map<string, string[]>} | ||
*/ | ||
function walkDependencyTreeForBestLogicalPaths({ packageDir, logicalPath = [], includeDevDeps = false, visited = new Set(), resolve }) { | ||
function walkDependencyTreeForBestLogicalPaths({ | ||
packageDir, | ||
logicalPath = [], | ||
includeDevDeps = false, | ||
visited = new Set(), | ||
resolve, | ||
}) { | ||
resolve = resolve ?? createPerformantResolve() | ||
/** @type {Map<string, string[]>} */ | ||
const preferredPackageLogicalPathMap = new Map() | ||
// add the entry package as the first work unit | ||
currentLevelTodos = [{ packageDir, logicalPath, includeDevDeps, visited, resolve }] | ||
currentLevelTodos = [ | ||
{ packageDir, logicalPath, includeDevDeps, visited, resolve }, | ||
] | ||
nextLevelTodos = [] | ||
@@ -116,6 +155,17 @@ // drain work queue until empty, avoid going depth-first by prioritizing the current depth level | ||
function processOnePackageInLogicalTree(preferredPackageLogicalPathMap, resolve) { | ||
const { packageDir, logicalPath = [], includeDevDeps = false, visited = new Set() } = currentLevelTodos.pop() | ||
/** | ||
* @param {Map<string, string[]>} preferredPackageLogicalPathMap | ||
* @param {Resolver} resolve | ||
*/ | ||
function processOnePackageInLogicalTree( | ||
preferredPackageLogicalPathMap, | ||
resolve | ||
) { | ||
const { | ||
packageDir, | ||
logicalPath = [], | ||
includeDevDeps = false, | ||
visited = new Set(), | ||
} = /** @type {WalkDepTreeOpts} */ (currentLevelTodos.pop()) | ||
const depsToWalk = getDependencies(packageDir, includeDevDeps) | ||
const results = [] | ||
@@ -148,3 +198,8 @@ // deps are already sorted by preference for paths | ||
// continue walking children, adding them to the end of the queue | ||
nextLevelTodos.push({ packageDir: childPackageDir, logicalPath: childLogicalPath, includeDevDeps: false, visited: childVisited }) | ||
nextLevelTodos.push({ | ||
packageDir: childPackageDir, | ||
logicalPath: childLogicalPath, | ||
includeDevDeps: false, | ||
visited: childVisited, | ||
}) | ||
} else { | ||
@@ -157,5 +212,9 @@ // debug: log skipped path traversals | ||
} | ||
return results | ||
} | ||
/** | ||
* @param {CanonicalNameMap} canonicalNameMap | ||
* @param {string} modulePath | ||
* @returns {string} | ||
*/ | ||
function getPackageNameForModulePath(canonicalNameMap, modulePath) { | ||
@@ -167,7 +226,9 @@ const packageDir = getPackageDirForModulePath(canonicalNameMap, modulePath) | ||
} | ||
const packageName = canonicalNameMap.get(packageDir) | ||
const packageName = /** @type {string} */ (canonicalNameMap.get(packageDir)) | ||
const relativeToPackageDir = path.relative(packageDir, modulePath) | ||
// files should never be associated with a package directory across a package boundary (as tested via the presense of "node_modules" in the path) | ||
if (relativeToPackageDir.includes('node_modules')) { | ||
throw new Error(`LavaMoat - Encountered unknown package directory for file "${modulePath}"`) | ||
throw new Error( | ||
`LavaMoat - Encountered unknown package directory for file "${modulePath}"` | ||
) | ||
} | ||
@@ -177,6 +238,12 @@ return packageName | ||
/** | ||
* @param {CanonicalNameMap} canonicalNameMap | ||
* @param {string} modulePath | ||
* @returns {string|undefined} | ||
*/ | ||
function getPackageDirForModulePath(canonicalNameMap, modulePath) { | ||
// find which of these directories the module is in | ||
const matchingPackageDirs = Array.from(canonicalNameMap.keys()) | ||
.filter(packageDir => modulePath.startsWith(packageDir)) | ||
const matchingPackageDirs = Array.from(canonicalNameMap.keys()).filter( | ||
(packageDir) => modulePath.startsWith(packageDir) | ||
) | ||
if (matchingPackageDirs.length === 0) { | ||
@@ -190,3 +257,8 @@ return undefined | ||
// for comparing string lengths | ||
/** | ||
* for comparing string lengths | ||
* @param {string} a | ||
* @param {string} b | ||
* @returns {string} | ||
*/ | ||
function takeLongest(a, b) { | ||
@@ -196,3 +268,8 @@ return a.length > b.length ? a : b | ||
// for package logical path names | ||
/** | ||
* for package logical path names | ||
* @param {string} a | ||
* @param {string} b | ||
* @returns {0|1|-1} | ||
*/ | ||
function comparePreferredPackageName(a, b) { | ||
@@ -215,3 +292,8 @@ // prefer shorter package names | ||
// for comparing package logical path arrays (shorter is better) | ||
/** | ||
* for comparing package logical path arrays (shorter is better) | ||
* @param {string[]} [aPath] | ||
* @param {string[]} [bPath] | ||
* @returns {0|1|-1} | ||
*/ | ||
function comparePackageLogicalPaths(aPath, bPath) { | ||
@@ -251,1 +333,27 @@ // undefined is not preferred | ||
} | ||
/** | ||
* @typedef Resolver | ||
* @property {(path: string, opts: {basedir: string}) => string} sync | ||
*/ | ||
/** | ||
* @typedef LoadCanonicalNameMapOpts | ||
* @property {string} rootDir | ||
* @property {boolean} [includeDevDeps] | ||
* @property {Resolver} [resolve] | ||
*/ | ||
/** | ||
* @internal | ||
* @typedef WalkDepTreeOpts | ||
* @property {string} packageDir | ||
* @property {string[]} [logicalPath] | ||
* @property {boolean} [includeDevDeps] | ||
* @property {Set<string>} [visited] | ||
* @property {Resolver} [resolve] | ||
*/ | ||
/** | ||
* @typedef {Map<string, string> & {rootDir: string}} CanonicalNameMap | ||
*/ |
@@ -5,83 +5,59 @@ const path = require('path') | ||
test('project 1', async t => { | ||
const canonicalNameMap = await loadCanonicalNameMap({ rootDir: path.join(__dirname, 'projects', '1') }) | ||
test('project 1', async (t) => { | ||
const canonicalNameMap = await loadCanonicalNameMap({ | ||
rootDir: path.join(__dirname, 'projects', '1'), | ||
}) | ||
// normalize results to be relative | ||
const normalizedMapEntries = Array.from(canonicalNameMap.entries()).sort() | ||
.map(([packagePath,canonicalName]) => [path.relative(__dirname,packagePath), canonicalName]) | ||
const normalizedMapEntries = Array.from(canonicalNameMap.entries()) | ||
.sort() | ||
.map(([packagePath, canonicalName]) => [ | ||
path.relative(__dirname, packagePath), | ||
canonicalName, | ||
]) | ||
t.deepEqual(normalizedMapEntries, [ | ||
[ | ||
'projects/1', | ||
'$root$', | ||
], | ||
[ | ||
'projects/1/node_modules/aaa', | ||
'aaa', | ||
], | ||
[ | ||
'projects/1/node_modules/bbb', | ||
'bbb', | ||
], | ||
[ | ||
'projects/1/node_modules/bbb/node_modules/evil_dep', | ||
'bbb>evil_dep', | ||
], | ||
['projects/1', '$root$'], | ||
['projects/1/node_modules/aaa', 'aaa'], | ||
['projects/1/node_modules/bbb', 'bbb'], | ||
['projects/1/node_modules/bbb/node_modules/evil_dep', 'bbb>evil_dep'], | ||
]) | ||
}) | ||
test('project 2', async t => { | ||
const canonicalNameMap = await loadCanonicalNameMap({ rootDir: path.join(__dirname, 'projects', '2') }) | ||
test('project 2', async (t) => { | ||
const canonicalNameMap = await loadCanonicalNameMap({ | ||
rootDir: path.join(__dirname, 'projects', '2'), | ||
}) | ||
// normalize results to be relative | ||
const normalizedMapEntries = Array.from(canonicalNameMap.entries()).sort() | ||
.map(([packagePath,canonicalName]) => [path.relative(__dirname,packagePath), canonicalName]) | ||
const normalizedMapEntries = Array.from(canonicalNameMap.entries()) | ||
.sort() | ||
.map(([packagePath, canonicalName]) => [ | ||
path.relative(__dirname, packagePath), | ||
canonicalName, | ||
]) | ||
t.deepEqual(normalizedMapEntries, [ | ||
[ | ||
'projects/2', | ||
'$root$', | ||
], | ||
[ | ||
'projects/2/node_modules/aaa', | ||
'aaa', | ||
], | ||
[ | ||
'projects/2/node_modules/bbb', | ||
'bbb', | ||
], | ||
[ | ||
'projects/2/node_modules/bbb/node_modules/evil_dep', | ||
'bbb>evil_dep', | ||
], | ||
[ | ||
'projects/2/node_modules/good_dep', | ||
'good_dep', | ||
], | ||
['projects/2', '$root$'], | ||
['projects/2/node_modules/aaa', 'aaa'], | ||
['projects/2/node_modules/bbb', 'bbb'], | ||
['projects/2/node_modules/bbb/node_modules/evil_dep', 'bbb>evil_dep'], | ||
['projects/2/node_modules/good_dep', 'good_dep'], | ||
]) | ||
}) | ||
test('project 3', async t => { | ||
const canonicalNameMap = await loadCanonicalNameMap({ rootDir: path.join(__dirname, 'projects', '3') }) | ||
test('project 3', async (t) => { | ||
const canonicalNameMap = await loadCanonicalNameMap({ | ||
rootDir: path.join(__dirname, 'projects', '3'), | ||
}) | ||
// normalize results to be relative | ||
const normalizedMapEntries = Array.from(canonicalNameMap.entries()).sort() | ||
.map(([packagePath,canonicalName]) => [path.relative(__dirname,packagePath), canonicalName]) | ||
const normalizedMapEntries = Array.from(canonicalNameMap.entries()) | ||
.sort() | ||
.map(([packagePath, canonicalName]) => [ | ||
path.relative(__dirname, packagePath), | ||
canonicalName, | ||
]) | ||
t.deepEqual(normalizedMapEntries, [ | ||
[ | ||
'projects/3', | ||
'$root$', | ||
], | ||
[ | ||
'projects/3/node_modules/aaa', | ||
'aaa', | ||
], | ||
[ | ||
'projects/3/node_modules/bbb', | ||
'bbb', | ||
], | ||
[ | ||
'projects/3/node_modules/bbb/node_modules/good_dep', | ||
'bbb>good_dep', | ||
], | ||
[ | ||
'projects/3/node_modules/evil_dep', | ||
'evil_dep', | ||
], | ||
['projects/3', '$root$'], | ||
['projects/3/node_modules/aaa', 'aaa'], | ||
['projects/3/node_modules/bbb', 'bbb'], | ||
['projects/3/node_modules/bbb/node_modules/good_dep', 'bbb>good_dep'], | ||
['projects/3/node_modules/evil_dep', 'evil_dep'], | ||
]) | ||
}) |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
21601
26
458
25
0
1