Compare commits

...
13 changed files with 496 additions and 45 deletions
@@ -4,10 +4,18 @@
// .github/workflows/blocking-labels.yaml via actions/github-script:
//
// const { default: checkBlockingLabels } =
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mjs`);
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mts`);
// await checkBlockingLabels({ github, context, core });
export default async function checkBlockingLabels({ context, core }) {
import type {
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
export default async function checkBlockingLabels({
context,
core,
}: GitHubScriptArgs<PullRequestPayload>) {
const blockingLabels = [
"wait for backend",
"Needs UX",
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env node
// Checks npm registry metadata for the direct dependencies in package.json:
// deprecation, source repository and provenance. On pull requests only newly
// added packages are checked, and a deprecated one fails the check. Scheduled
// and manual runs report on every direct dependency without failing. Invoked
// from the `npm-metadata` job in .github/workflows/dependency-review.yaml via
// actions/github-script:
//
// const { default: checkNpmMetadata } =
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-npm-metadata.mts`);
// await checkNpmMetadata({ github, context, core });
import type { Octokit } from "@octokit/rest";
import type {
Context,
Core,
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
// Scheduled and manual runs have no pull request
type Payload = Partial<PullRequestPayload>;
type Section =
| "dependencies"
| "devDependencies"
| "optionalDependencies"
| "peerDependencies";
type PackageJson = Partial<Record<Section, Record<string, string>>>;
interface Dependency {
name: string;
// The package npm installs, which differs from name for npm: aliases
registryName: string;
version: string;
section: Section;
}
// The fields read from a version document on registry.npmjs.org
interface NpmManifest {
deprecated?: string;
repository?: string | { url?: string };
dist?: { attestations?: object };
}
interface Result {
name: string;
version: string;
section: Section;
skipped?: string;
deprecated?: string;
repository?: string;
provenance?: boolean;
}
const SECTIONS: Section[] = [
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
];
const EXACT_VERSION =
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const NPM_ALIAS = /^npm:((?:@[^/]+\/)?[^@]+)@(.+)$/;
const CONCURRENCY = 8;
// Returns undefined when the registry reports 404, and throws on any other
// failure so a registry outage fails the job instead of skipping the check.
const fetchJson = async <T,>(url: string): Promise<T | undefined> => {
const response = await fetch(url);
if (response.status === 404) {
return undefined;
}
if (!response.ok) {
throw new Error(
`Request to ${url} failed: ${response.status} ${response.statusText}`
);
}
return JSON.parse(await response.text());
};
const readPackageJson = async (
github: Octokit,
context: Context<Payload>,
ref: string
): Promise<PackageJson> => {
const { data } = await github.rest.repos.getContent({
...context.repo,
path: "package.json",
ref,
mediaType: { format: "raw" },
});
// The raw media type returns the file contents as a string, but Octokit
// types the response as the JSON form
if (typeof data !== "string") {
throw new Error("Expected raw package.json contents");
}
return JSON.parse(data);
};
const listDependencies = (pkg: PackageJson): Dependency[] =>
SECTIONS.flatMap((section) =>
Object.entries(pkg[section] ?? {}).map(([name, spec]) => {
// npm: aliases install another package, e.g. npm:[email protected]
const alias = spec.match(NPM_ALIAS);
return {
name,
registryName: alias?.[1] ?? name,
version: alias?.[2] ?? spec,
section,
};
})
);
const repositoryUrl = (repository: NpmManifest["repository"]) => {
const url = typeof repository === "string" ? repository : repository?.url;
return url?.replace(/^git\+/, "").replace(/\.git$/, "");
};
const checkPackage = async ({
name,
registryName,
version,
section,
}: Dependency): Promise<Result> => {
const result = {
name: registryName === name ? name : `${name} (npm:${registryName})`,
version,
section,
};
if (!EXACT_VERSION.test(version)) {
return { ...result, skipped: "not an exact version" };
}
const manifest = await fetchJson<NpmManifest>(
`https://registry.npmjs.org/${encodeURIComponent(registryName)}/${version}`
);
if (!manifest) {
return { ...result, skipped: "not found on the npm registry" };
}
return {
...result,
deprecated: manifest.deprecated,
repository: repositoryUrl(manifest.repository),
provenance: Boolean(manifest.dist?.attestations),
};
};
const checkAll = async (dependencies: Dependency[]) => {
const results: Result[] = [];
for (let i = 0; i < dependencies.length; i += CONCURRENCY) {
results.push(
// eslint-disable-next-line no-await-in-loop
...(await Promise.all(
dependencies.slice(i, i + CONCURRENCY).map(checkPackage)
))
);
}
return results;
};
const writeSummary = async (core: Core, heading: string, results: Result[]) => {
await core.summary
.addHeading(heading, 2)
.addTable([
[
"Package",
"Version",
"Section",
"Deprecated",
"Provenance",
"Repository",
].map((data) => ({ data, header: true })),
...results.map((result) =>
result.skipped
? [
result.name,
result.version,
result.section,
`skipped: ${result.skipped}`,
"-",
"-",
]
: [
result.name,
result.version,
result.section,
result.deprecated ? "yes" : "no",
result.provenance ? "yes" : "no",
result.repository ?? "none",
]
),
])
.write();
};
export default async function checkNpmMetadata({
github,
context,
core,
}: GitHubScriptArgs<Payload>) {
const pr = context.payload.pull_request;
if (!pr) {
const pkg = await readPackageJson(github, context, context.sha);
const results = await checkAll(listDependencies(pkg));
const flagged = (result: Result) =>
Number(
Boolean(result.skipped || result.deprecated || !result.repository)
);
results.sort(
(a, b) => flagged(b) - flagged(a) || a.name.localeCompare(b.name)
);
await writeSummary(core, "Direct dependencies", results);
return;
}
const [base, head] = await Promise.all([
readPackageJson(github, context, pr.base.sha),
readPackageJson(github, context, pr.head.sha),
]);
// Compare the declared name together with the package npm installs, so a
// new or retargeted npm: alias counts as new, but moving a package between
// sections does not
const key = ({ name, registryName }: Dependency) =>
JSON.stringify([name, registryName]);
const existing = new Set(listDependencies(base).map(key));
const added = listDependencies(head).filter(
(dependency) => !existing.has(key(dependency))
);
if (added.length === 0) {
core.info("No new dependencies");
return;
}
const results = await checkAll(added);
const annotation = { file: "package.json" };
for (const result of results) {
const label = `${result.name}@${result.version}`;
if (result.skipped) {
core.warning(`${label}: ${result.skipped}, not checked`, annotation);
continue;
}
if (result.deprecated) {
core.error(`${label} is deprecated: ${result.deprecated}`, annotation);
}
if (!result.repository) {
core.warning(`${label} has no source repository`, annotation);
}
if (!result.provenance) {
core.notice(`${label} was published without provenance`, annotation);
}
}
await writeSummary(core, "New dependencies", results);
if (results.some((result) => result.deprecated)) {
core.setFailed("New dependencies must not be deprecated on npm");
}
}
@@ -6,14 +6,19 @@
// via actions/github-script:
//
// const { default: checkStandards } =
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`);
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mts`);
// await checkStandards({ github, context, core });
import type {
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
export default async function checkPullRequestStandards({
github,
context,
core,
}) {
}: GitHubScriptArgs<PullRequestPayload>) {
const pr = context.payload.pull_request;
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
@@ -52,11 +57,16 @@ export default async function checkPullRequestStandards({
const normalized = body.toLowerCase();
// Ignore 404s from mutations that race manual edits or cancelled runs.
const ignoreMissing = async (fn) => {
const ignoreMissing = async <T,>(fn: () => Promise<T>) => {
try {
await fn();
} catch (error) {
if (error.status === 404) {
if (
typeof error === "object" &&
error !== null &&
"status" in error &&
error.status === 404
) {
core.info("Target already removed, nothing to do");
return;
}
@@ -65,7 +75,7 @@ export default async function checkPullRequestStandards({
};
// Hide/restore our comment via GraphQL (REST cannot minimize).
const setMinimized = async (subjectId, minimized) => {
const setMinimized = async (subjectId: string, minimized: boolean) => {
const mutation = minimized
? `mutation($id: ID!) {
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
@@ -81,13 +91,13 @@ export default async function checkPullRequestStandards({
await github.graphql(mutation, { id: subjectId });
} catch (error) {
core.info(
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
`Could not ${minimized ? "minimize" : "restore"} comment: ${error instanceof Error ? error.message : error}`
);
}
};
// Content of a "## <name>" section, or null when the heading is absent.
const section = (name) => {
const section = (name: string) => {
const match = body.match(
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
);
@@ -128,7 +138,8 @@ export default async function checkPullRequestStandards({
issue_number,
per_page: 100,
});
const existing = comments.find((c) => c.body.includes(marker));
const existing = comments.find((c) => c.body?.includes(marker));
const hasLabel = pr.labels.some((l) => l.name === label);
if (isValid) {
@@ -5,14 +5,16 @@
// actions/github-script:
//
// const { default: checkTaskAuthorization } =
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mjs`);
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mts`);
// await checkTaskAuthorization({ github, context, core });
import type { GitHubScriptArgs, IssuePayload } from "./github-script.d.ts";
export default async function checkTaskAuthorization({
github,
context,
core,
}) {
}: GitHubScriptArgs<IssuePayload>) {
const issueAuthor = context.payload.issue.user.login;
// Check if user is an organization member
+61
View File
@@ -0,0 +1,61 @@
// The arguments actions/github-script passes to the scripts in this directory,
// limited to the parts they use.
import type { Octokit } from "@octokit/rest";
export type SummaryTableCell = string | { data: string; header?: boolean };
export interface Summary {
addHeading(text: string, level?: number): Summary;
addRaw(text: string): Summary;
addTable(rows: SummaryTableCell[][]): Summary;
write(): Promise<Summary>;
}
export interface AnnotationProperties {
file?: string;
}
export interface Core {
info(message: string): void;
notice(message: string, properties?: AnnotationProperties): void;
warning(message: string, properties?: AnnotationProperties): void;
error(message: string, properties?: AnnotationProperties): void;
setFailed(message: string): void;
summary: Summary;
}
export interface User {
login: string;
type: string;
}
export interface PullRequestPayload {
pull_request: {
number: number;
body: string | null;
draft: boolean;
user: User;
labels: { name: string }[];
base: { sha: string };
head: { sha: string };
};
}
export interface IssuePayload {
issue: {
user: User;
};
}
export interface Context<Payload> {
sha: string;
repo: { owner: string; repo: string };
issue: { owner: string; repo: string; number: number };
payload: Payload;
}
export interface GitHubScriptArgs<Payload> {
github: Octokit;
context: Context<Payload>;
core: Core;
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2024",
"lib": ["ES2024"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
// actions/github-script imports these directly, relying on Node's type stripping
"erasableSyntaxOnly": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["*.mts", "*.d.ts"]
}
+1 -1
View File
@@ -30,6 +30,6 @@ jobs:
with:
script: |
const { default: checkBlockingLabels } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mjs`
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mts`
);
await checkBlockingLabels({ github, context, core });
+57
View File
@@ -0,0 +1,57 @@
name: Dependency review
on:
pull_request:
branches:
- dev
paths:
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .github/workflows/dependency-review.yaml
- .github/scripts/check-npm-metadata.mts
schedule:
- cron: "0 6 1 * *" # Monthly report on all direct dependencies
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
dependency-review:
name: Review dependency changes
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read # To compare the dependency graph between base and head
steps:
- name: Dependency review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
license-check: false # Covered by pnpm lint:licenses
fail-on-severity: high
show-openssf-scorecard: true
warn-on-openssf-scorecard-level: 3
npm-metadata:
name: Check npm metadata
runs-on: ubuntu-latest
permissions:
contents: read # To read package.json
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: Check npm metadata
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { default: checkNpmMetadata } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-npm-metadata.mts`
);
await checkNpmMetadata({ github, context, core });
@@ -33,6 +33,6 @@ jobs:
with:
script: |
const { default: checkStandards } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mts`
);
await checkStandards({ github, context, core });
@@ -51,6 +51,6 @@ jobs:
with:
script: |
const { default: checkTaskAuthorization } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mjs`
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mts`
);
await checkTaskAuthorization({ github, context, core });
+1 -1
View File
@@ -290,7 +290,7 @@ export default tseslint.config(
},
},
{
files: [".github/scripts/*.mjs"],
files: [".github/scripts/*.mts"],
languageOptions: {
globals: globals.node,
},
+2 -1
View File
@@ -12,7 +12,7 @@
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:prettier": "prettier . --cache --check",
"format:prettier": "prettier . --cache --write",
"lint:types": "node ./node_modules/@typescript/native/bin/tsc",
"lint:types": "node ./node_modules/@typescript/native/bin/tsc && node ./node_modules/@typescript/native/bin/tsc -p .github/scripts",
"lint:lit": "lit-analyzer \"{.,*}/src/**/*.ts\"",
"lint:licenses": "node --no-deprecation script/check-licenses",
"lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:types && pnpm run lint:lit",
@@ -171,6 +171,7 @@
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.5",
"@types/node": "24.19.0",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
+31 -28
View File
@@ -415,6 +415,9 @@ importers:
'@types/luxon':
specifier: 3.7.5
version: 3.7.5
'@types/node':
specifier: 24.19.0
version: 24.19.0
'@types/qrcode':
specifier: 1.5.6
version: 1.5.6
@@ -564,13 +567,13 @@ importers:
version: 8.70.1([email protected]([email protected]))([email protected])([email protected])
vite:
specifier: 8.3.1
version: 8.3.1(@types/node@26.2.0)([email protected])([email protected])
version: 8.3.1(@types/node@24.19.0)([email protected])([email protected])
vite-tsconfig-paths:
specifier: 6.1.1
version: 6.1.1([email protected])([email protected])([email protected](@types/node@26.2.0)([email protected])([email protected]))
version: 6.1.1([email protected])([email protected])([email protected](@types/node@24.19.0)([email protected])([email protected]))
vitest:
specifier: 5.0.2
version: 5.0.2(@types/node@26.2.0)(@vitest/[email protected])([email protected])([email protected](@types/node@26.2.0)([email protected])([email protected]))
version: 5.0.2(@types/node@24.19.0)(@vitest/[email protected])([email protected])([email protected](@types/node@24.19.0)([email protected])([email protected]))
webpack-stats-plugin:
specifier: 1.1.3
version: 1.1.3
@@ -3216,8 +3219,8 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@26.2.0':
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
'@types/node@24.19.0':
resolution: {integrity: sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==}
'@types/[email protected]':
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
@@ -7177,8 +7180,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
[email protected]:
resolution: {integrity: sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==}
@@ -10798,11 +10801,11 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
'@types/[email protected]':
dependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
'@types/[email protected]': {}
@@ -10860,19 +10863,19 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/node@26.2.0':
'@types/node@24.19.0':
dependencies:
undici-types: 8.3.0
undici-types: 7.24.6
'@types/[email protected]': {}
'@types/[email protected]':
dependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
'@types/[email protected]':
dependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
'@types/[email protected]': {}
@@ -10890,7 +10893,7 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
@@ -11184,7 +11187,7 @@ snapshots:
obug: 2.1.4
std-env: 4.2.0
tinyrainbow: 3.1.1
vitest: 5.0.2(@types/node@26.2.0)(@vitest/[email protected])([email protected])([email protected](@types/node@26.2.0)([email protected])([email protected]))
vitest: 5.0.2(@types/node@24.19.0)(@vitest/[email protected])([email protected])([email protected](@types/node@24.19.0)([email protected])([email protected]))
'@vitest/[email protected]': {}
@@ -11192,14 +11195,14 @@ snapshots:
dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/[email protected]([email protected](@types/node@26.2.0)([email protected])([email protected]))':
'@vitest/[email protected]([email protected](@types/node@24.19.0)([email protected])([email protected]))':
dependencies:
'@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.2
estree-walker: 3.0.3
magic-string: 1.2.3
optionalDependencies:
vite: 8.3.1(@types/node@26.2.0)([email protected])([email protected])
vite: 8.3.1(@types/node@24.19.0)([email protected])([email protected])
'@vitest/[email protected]': {}
@@ -12059,7 +12062,7 @@ snapshots:
[email protected]([email protected]):
dependencies:
'@types/cors': 2.8.19
'@types/node': 26.2.0
'@types/node': 24.19.0
'@types/ws': 8.18.1
accepts: 1.3.8
base64id: 2.0.0
@@ -13137,7 +13140,7 @@ snapshots:
[email protected]:
dependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -15067,7 +15070,7 @@ snapshots:
[email protected]: {}
undici-types@8.3.0: {}
undici-types@7.24.6: {}
[email protected]: {}
@@ -15215,17 +15218,17 @@ snapshots:
- bare-abort-controller
- react-native-b4a
[email protected]([email protected])([email protected])([email protected](@types/node@26.2.0)([email protected])([email protected])):
[email protected]([email protected])([email protected])([email protected](@types/node@24.19.0)([email protected])([email protected])):
dependencies:
debug: 4.4.3([email protected])
globrex: 0.1.2
tsconfck: 3.1.6([email protected])
vite: 8.3.1(@types/node@26.2.0)([email protected])([email protected])
vite: 8.3.1(@types/node@24.19.0)([email protected])([email protected])
transitivePeerDependencies:
- supports-color
- typescript
[email protected](@types/node@26.2.0)([email protected])([email protected]):
[email protected](@types/node@24.19.0)([email protected])([email protected]):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
@@ -15233,15 +15236,15 @@ snapshots:
rolldown: 1.2.11
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
fsevents: 2.3.3
terser: 5.50.0
yaml: 2.9.1
[email protected](@types/node@26.2.0)(@vitest/[email protected])([email protected])([email protected](@types/node@26.2.0)([email protected])([email protected])):
[email protected](@types/node@24.19.0)(@vitest/[email protected])([email protected])([email protected](@types/node@24.19.0)([email protected])([email protected])):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.2([email protected](@types/node@26.2.0)([email protected])([email protected]))
'@vitest/mocker': 5.0.2([email protected](@types/node@24.19.0)([email protected])([email protected]))
chai: 6.2.2
es-module-lexer: 2.3.2
expect-type: 1.4.0
@@ -15252,10 +15255,10 @@ snapshots:
tinybench: 6.2.0
tinyexec: 1.3.1
tinyglobby: 0.2.17
vite: 8.3.1(@types/node@26.2.0)([email protected])([email protected])
vite: 8.3.1(@types/node@24.19.0)([email protected])([email protected])
why-is-node-running: 3.2.1
optionalDependencies:
'@types/node': 26.2.0
'@types/node': 24.19.0
'@vitest/coverage-v8': 5.0.2([email protected])
jsdom: 30.1.1
transitivePeerDependencies: