Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wicked-aliens-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/scanner": minor
---

Implement cache lookup for from and workingDir APIs
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,22 @@ type WorkingDirOptions = Options & {
* It is optionally used to fetch registry authentication tokens
*/
npmRcConfig?: Config;
/**
* Optional cache lookup called after reading the local package.json.
*/
cacheLookup?: (
packageJSON: PackageJSON
) => Promise<Payload | null>;
};

type FromOptions = Omit<Options, "includeDevDeps">;
type FromOptions = Omit<Options, "includeDevDeps"> & {
/**
* Optional cache lookup called after fetching the remote manifest.
*/
cacheLookup?: (
manifest: pacote.AbbreviatedManifest & pacote.ManifestResult
) => Promise<Payload | null>;
};

interface Options {
/**
Expand Down
32 changes: 32 additions & 0 deletions workspaces/scanner/docs/from.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,38 @@ It is important here to dig and learn some vocabulary related to npm:

To simplify it, the first step is to check the package's existence on the remote registry and to get a structure similar to the `package.json`.

## Steps 1.5: Cache Lookup (optional)

After fetching the manifest, if a `cacheLookup` function was provided in the options, it is called with the resolved manifest. If it returns a non-null `Payload`, that value is returned immediately and the dependency walker is **never executed**. This is useful to avoid redundant network I/O when a fresh result is already available.

```ts
const payload = await scanner.from("fastify", {
cacheLookup: async(manifest) => {
const cached = await myCache.get(`${manifest.name}@${manifest.version}`);
return cached ?? null;
}
});
```

The same `cacheLookup` mechanism is available on `workingDir`. In that case, the callback receives the parsed `package.json` object instead of the npm manifest:

```ts
const payload = await scanner.workingDir(process.cwd(), {
cacheLookup: async(packageJSON) => {
const cached = await myCache.get(`${packageJSON.name}@${packageJSON.version}`);
return cached ?? null;
}
});
```

```mermaid
graph LR;
A[From API]-->|Spec|B[Fetching Manifest];
B-->C{cacheLookup?};
C-->|Payload returned|D[Return cached result];
C-->|null returned|E[Dependency Walker];
```

## Steps 2: Dependency Walker

This step aims to identify and walk through the package dependencies (that's why we call this the dependency walker). To do this, we retrieve the dependencies from the root of the Manifest in step 1 and start a recursive mechanism.
Expand Down
42 changes: 35 additions & 7 deletions workspaces/scanner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ import type Config from "@npmcli/config";

// Import Internal Dependencies
import { depWalker } from "./depWalker.ts";
import { NPM_TOKEN, urlToString, readNpmRc } from "./utils/index.ts";
import { Logger, ScannerLoggerEvents } from "./class/logger.class.ts";
import {
NPM_TOKEN,
urlToString,
readNpmRc
} from "./utils/index.ts";
import {
Logger,
ScannerLoggerEvents
} from "./class/logger.class.ts";
import { TempDirectory } from "./class/TempDirectory.class.ts";
import { comparePayloads } from "./comparePayloads.ts";
import type { Options } from "./types.ts";
import type {
Options,
Payload
} from "./types.ts";

// CONSTANTS
const kDefaultWorkingDirOptions = {
Expand All @@ -33,13 +43,16 @@ export type WorkingDirOptions = Options & {
* It is optionally used to fetch registry authentication tokens
*/
npmRcConfig?: Config;
cacheLookup?: (
packageJSON: PackageJSON
) => Promise<Payload | null>;
};

export async function workingDir(
location = process.cwd(),
options: WorkingDirOptions = {},
logger = new Logger()
) {
): Promise<Payload> {
const registry = options.registry ?
urlToString(options.registry) :
getLocalRegistryURL();
Expand All @@ -66,20 +79,30 @@ export async function workingDir(
const str = await fs.readFile(packagePath, "utf-8");
logger.end(ScannerLoggerEvents.manifest.read);

const packageJSON = JSON.parse(str) as PackageJSON;
const cachedPayload = await options.cacheLookup?.(packageJSON);
if (cachedPayload) {
return cachedPayload;
}

return depWalker(
JSON.parse(str) as PackageJSON,
packageJSON,
finalizedOptions,
logger
);
}

export type FromOptions = Omit<Options, "includeDevDeps">;
export type FromOptions = Omit<Options, "includeDevDeps"> & {
cacheLookup?: (
manifest: pacote.AbbreviatedManifest & pacote.ManifestResult
) => Promise<Payload | null>;
};

export async function from(
packageName: string,
options: FromOptions = {},
logger = new Logger()
) {
): Promise<Payload> {
const registry = options.registry ?
urlToString(options.registry) :
getLocalRegistryURL();
Expand All @@ -91,6 +114,11 @@ export async function from(
});
logger.end(ScannerLoggerEvents.manifest.fetch);

const cachedPayload = await options.cacheLookup?.(manifest);
if (cachedPayload) {
return cachedPayload;
}

return depWalker(
// FIX: find a way to merge pacote & registry interfaces
manifest as pacote.AbbreviatedManifest,
Expand Down
Loading
Loading