feat(core-consent): scaffold package with types, IConsent, withConsent brand wrapper
- Add packages/core-consent with ConsentCategory, ConsentState, UserConsentState types and IConsent interface - Add withConsent wrapper attaching __consentChecked brand at bind time; unit tests assert brand attachment and factory passthrough - Add ConsentChecked<F> type to core-shared/conformance/brands.ts and isConsentChecked helper to brand-runtime.ts - Extend FeatureManifest with requiresConsent?: readonly string[] field - Extend assertFeatureConformance to require __consentChecked brand when requiresConsent.length > 0; synthetic fixture tests cover pass/fail cases - Propagate __consentChecked in withSpan PROPAGATED_BRANDS so the outermost binding carries the brand Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"generatedAt": "2026-05-19T10:16:15.209Z",
|
||||
"commit": "8cf9f4b",
|
||||
"generatedAt": "2026-05-19T10:39:50.009Z",
|
||||
"commit": "f5d08dc",
|
||||
"repo": {
|
||||
"statements": 96.48,
|
||||
"branches": 91.72,
|
||||
"functions": 96.83,
|
||||
"lines": 96.48,
|
||||
"statements": 96.5,
|
||||
"branches": 91.82,
|
||||
"functions": 96.89,
|
||||
"lines": 96.5,
|
||||
"counts": {
|
||||
"lf": 4264,
|
||||
"lh": 4114,
|
||||
"brf": 809,
|
||||
"brh": 742,
|
||||
"fnf": 252,
|
||||
"fnh": 244
|
||||
"lf": 4291,
|
||||
"lh": 4141,
|
||||
"brf": 819,
|
||||
"brh": 752,
|
||||
"fnf": 257,
|
||||
"fnh": 249
|
||||
}
|
||||
},
|
||||
"byPackage": {
|
||||
@@ -58,18 +58,32 @@
|
||||
"fnh": 10
|
||||
}
|
||||
},
|
||||
"@repo/core-shared": {
|
||||
"statements": 98,
|
||||
"branches": 95.81,
|
||||
"functions": 92,
|
||||
"lines": 98,
|
||||
"@repo/core-consent": {
|
||||
"statements": 100,
|
||||
"branches": 100,
|
||||
"functions": 100,
|
||||
"lines": 100,
|
||||
"counts": {
|
||||
"lf": 1050,
|
||||
"lh": 1029,
|
||||
"brf": 310,
|
||||
"brh": 297,
|
||||
"fnf": 100,
|
||||
"fnh": 92
|
||||
"lf": 10,
|
||||
"lh": 10,
|
||||
"brf": 4,
|
||||
"brh": 4,
|
||||
"fnf": 4,
|
||||
"fnh": 4
|
||||
}
|
||||
},
|
||||
"@repo/core-shared": {
|
||||
"statements": 98.03,
|
||||
"branches": 95.89,
|
||||
"functions": 92.08,
|
||||
"lines": 98.03,
|
||||
"counts": {
|
||||
"lf": 1067,
|
||||
"lh": 1046,
|
||||
"brf": 316,
|
||||
"brh": 303,
|
||||
"fnf": 101,
|
||||
"fnh": 93
|
||||
}
|
||||
},
|
||||
"@repo/marketing-pages": {
|
||||
|
||||
29
packages/core-consent/AGENTS.md
Normal file
29
packages/core-consent/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# @repo/core-consent
|
||||
|
||||
Optional core package providing a vendor-neutral consent management interface. Scaffold via `pnpm turbo gen core-package consent` (once the generator supports it).
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/
|
||||
consent-types.ts # ConsentCategory, ConsentState, UserConsentState
|
||||
consent.interface.ts # IConsent — isGranted, grant, withdraw, getCategories
|
||||
with-consent.ts # withConsent wrapper attaching ConsentChecked brand
|
||||
index.ts # Barrel export
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
`IConsent` exposes four methods:
|
||||
|
||||
- `isGranted(category)` — synchronous check whether consent is granted
|
||||
- `grant(category)` — record consent grant for a category
|
||||
- `withdraw(category)` — record consent withdrawal for a category
|
||||
- `getCategories()` — list all known consent states
|
||||
|
||||
The interface is vendor-neutral: no storage implementation is bundled here. Concrete implementations (e.g. a Payload-backed store) are wired at DI bind time in `bind-production` (Story 04).
|
||||
|
||||
`withConsent` wraps a use-case factory at bind time, attaches the `__consentChecked` brand, and is the innermost wrapper in the composition chain:
|
||||
`withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)`
|
||||
|
||||
See `docs/architecture/agent-first-workflow-and-conformance.md` for the dependency-injection conventions.
|
||||
3
packages/core-consent/eslint.config.js
Normal file
3
packages/core-consent/eslint.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/core-eslint/base";
|
||||
|
||||
export default baseConfig;
|
||||
26
packages/core-consent/package.json
Normal file
26
packages/core-consent/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@repo/core-consent",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/core-eslint": "workspace:*",
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
22
packages/core-consent/src/consent-types.ts
Normal file
22
packages/core-consent/src/consent-types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Known consent categories. The `(string & {})` escape hatch keeps the union
|
||||
* open for custom categories while still providing autocomplete for the
|
||||
* standard values.
|
||||
*/
|
||||
export type ConsentCategory =
|
||||
| "necessary"
|
||||
| "functional"
|
||||
| "analytics"
|
||||
| "marketing"
|
||||
| (string & {});
|
||||
|
||||
/** Whether a subject has granted or denied consent for a category. */
|
||||
export type ConsentState = "granted" | "denied" | "pending";
|
||||
|
||||
/** Per-category consent record for a single subject. */
|
||||
export type UserConsentState = {
|
||||
readonly category: ConsentCategory;
|
||||
readonly state: ConsentState;
|
||||
readonly grantedAt?: Date;
|
||||
readonly withdrawnAt?: Date;
|
||||
};
|
||||
22
packages/core-consent/src/consent.interface.ts
Normal file
22
packages/core-consent/src/consent.interface.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ConsentCategory, UserConsentState } from "./consent-types";
|
||||
|
||||
/**
|
||||
* Vendor-neutral consent management interface.
|
||||
*
|
||||
* Feature binders that receive a consent instance operate through this
|
||||
* interface. Concrete implementations (Payload-backed, in-memory, etc.) are
|
||||
* wired at DI bind time and never imported by feature packages directly.
|
||||
*/
|
||||
export interface IConsent {
|
||||
/** Synchronous check — true when the subject has granted the category. */
|
||||
isGranted(category: ConsentCategory): boolean;
|
||||
|
||||
/** Record a consent grant for the given category. */
|
||||
grant(category: ConsentCategory): Promise<void>;
|
||||
|
||||
/** Record a consent withdrawal for the given category. */
|
||||
withdraw(category: ConsentCategory): Promise<void>;
|
||||
|
||||
/** Return the full list of per-category consent states. */
|
||||
getCategories(): UserConsentState[];
|
||||
}
|
||||
8
packages/core-consent/src/index.ts
Normal file
8
packages/core-consent/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type {
|
||||
ConsentCategory,
|
||||
ConsentState,
|
||||
UserConsentState,
|
||||
} from "./consent-types";
|
||||
export type { IConsent } from "./consent.interface";
|
||||
export type { ConsentChecked } from "./with-consent";
|
||||
export { withConsent } from "./with-consent";
|
||||
55
packages/core-consent/src/with-consent.test.ts
Normal file
55
packages/core-consent/src/with-consent.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, expectTypeOf } from "vitest";
|
||||
import { withConsent, type ConsentChecked } from "@/with-consent";
|
||||
import type { IConsent } from "@/consent.interface";
|
||||
import { isConsentChecked } from "@repo/core-shared/conformance";
|
||||
|
||||
function makeConsent(): IConsent {
|
||||
return {
|
||||
isGranted: () => true,
|
||||
grant: () => Promise.resolve(),
|
||||
withdraw: () => Promise.resolve(),
|
||||
getCategories: () => [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("withConsent", () => {
|
||||
it("returns a ConsentChecked<F>", () => {
|
||||
const consent = makeConsent();
|
||||
const fn = async (_input: { id: string }) => ({ ok: true });
|
||||
const wrapped = withConsent(consent, fn);
|
||||
expectTypeOf(wrapped).toMatchTypeOf<ConsentChecked<typeof fn>>();
|
||||
});
|
||||
|
||||
it("attaches __consentChecked as a non-enumerable property on the wrapped function", () => {
|
||||
const consent = makeConsent();
|
||||
const fn = async () => ({ ok: true });
|
||||
const wrapped = withConsent(consent, fn);
|
||||
expect(isConsentChecked(wrapped)).toBe(true);
|
||||
expect(Object.keys(wrapped)).not.toContain("__consentChecked");
|
||||
});
|
||||
|
||||
it("does NOT pollute the original input function with the brand", () => {
|
||||
const consent = makeConsent();
|
||||
const fn = async () => ({ ok: true });
|
||||
const wrapped = withConsent(consent, fn);
|
||||
expect(isConsentChecked(fn)).toBe(false);
|
||||
expect(wrapped).not.toBe(fn);
|
||||
});
|
||||
|
||||
it("passes input and output through unchanged", async () => {
|
||||
const consent = makeConsent();
|
||||
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
||||
const wrapped = withConsent(consent, fn);
|
||||
const result = await wrapped({ id: "abc" });
|
||||
expect(result).toEqual({ ok: true, id: "abc" });
|
||||
});
|
||||
|
||||
it("propagates errors", async () => {
|
||||
const consent = makeConsent();
|
||||
const err = new Error("boom");
|
||||
const wrapped = withConsent(consent, async () => {
|
||||
throw err;
|
||||
});
|
||||
await expect(wrapped()).rejects.toBe(err);
|
||||
});
|
||||
});
|
||||
30
packages/core-consent/src/with-consent.ts
Normal file
30
packages/core-consent/src/with-consent.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { IConsent } from "./consent.interface";
|
||||
import type { ConsentChecked } from "@repo/core-shared/conformance";
|
||||
import { attachBrand } from "@repo/core-shared/conformance";
|
||||
|
||||
export type { ConsentChecked };
|
||||
|
||||
/**
|
||||
* Use-case wrapper applied at DI bind time. The wrapper is a thin closure
|
||||
* that forwards to `fn` unchanged and carries the `__consentChecked` brand.
|
||||
* The forward closure keeps the brand on a fresh function so the original
|
||||
* `fn` reference is not mutated — important when the same factory output is
|
||||
* used elsewhere unwrapped (dev-seed paths, tests).
|
||||
*
|
||||
* Composition order (innermost to outermost):
|
||||
* withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)
|
||||
*
|
||||
* The wrapper exists to:
|
||||
* (1) require callers to pass the consent instance at bind time (dep is available)
|
||||
* (2) attach the `__consentChecked` brand so the boot-time assertion can verify
|
||||
* consent-gated use cases were bound through the consent-aware path.
|
||||
*/
|
||||
export function withConsent<Args extends unknown[], R>(
|
||||
consent: IConsent,
|
||||
fn: (...args: Args) => Promise<R>,
|
||||
): ConsentChecked<(...args: Args) => Promise<R>> {
|
||||
void consent;
|
||||
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
||||
attachBrand(wrapped, "__consentChecked");
|
||||
return wrapped as ConsentChecked<(...args: Args) => Promise<R>>;
|
||||
}
|
||||
12
packages/core-consent/tsconfig.json
Normal file
12
packages/core-consent/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@repo/core-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
4
packages/core-consent/turbo.json
Normal file
4
packages/core-consent/turbo.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tags": ["core"]
|
||||
}
|
||||
15
packages/core-consent/vitest.config.ts
Normal file
15
packages/core-consent/vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig, mergeConfig } from "vitest/config";
|
||||
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
|
||||
|
||||
export default mergeConfig(
|
||||
nodeVitestConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
resolve: {
|
||||
alias: { "@": path.resolve(__dirname, "./src") },
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -285,6 +285,82 @@ describe("assertFeatureConformance", () => {
|
||||
).toThrow(/Analyzed|__analyzed/);
|
||||
});
|
||||
|
||||
it("throws when feature requiresConsent but binding is missing __consentChecked brand", () => {
|
||||
const container = new Container();
|
||||
const sym = Symbol("test.processData");
|
||||
const ctx = makeCtx();
|
||||
const wrappedNoConsent = withSpan(
|
||||
ctx.tracer,
|
||||
{ name: "test.processData", op: "use-case" },
|
||||
withCapture(
|
||||
ctx.logger,
|
||||
{ feature: "test", layer: "use-case" },
|
||||
async (x: number) => x,
|
||||
),
|
||||
);
|
||||
container.bind(sym).toConstantValue(wrappedNoConsent);
|
||||
|
||||
const manifest = defineFeature({
|
||||
name: "test",
|
||||
requiredCores: [],
|
||||
useCases: {
|
||||
processData: {
|
||||
mutates: false,
|
||||
audits: [],
|
||||
publishes: [],
|
||||
consumes: [],
|
||||
},
|
||||
},
|
||||
realtimeChannels: [],
|
||||
jobs: [],
|
||||
requiresConsent: ["analytics"],
|
||||
} as const);
|
||||
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { processData: sym }, ctx),
|
||||
).toThrow(ConformanceError);
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { processData: sym }, ctx),
|
||||
).toThrow(/__consentChecked/);
|
||||
});
|
||||
|
||||
it("passes when feature requiresConsent and binding carries __consentChecked brand", () => {
|
||||
const container = new Container();
|
||||
const sym = Symbol("test.processData");
|
||||
const ctx = makeCtx();
|
||||
const base = withSpan(
|
||||
ctx.tracer,
|
||||
{ name: "test.processData", op: "use-case" },
|
||||
withCapture(
|
||||
ctx.logger,
|
||||
{ feature: "test", layer: "use-case" },
|
||||
async (x: number) => x,
|
||||
),
|
||||
);
|
||||
attachBrand(base, "__consentChecked");
|
||||
container.bind(sym).toConstantValue(base);
|
||||
|
||||
const manifest = defineFeature({
|
||||
name: "test",
|
||||
requiredCores: [],
|
||||
useCases: {
|
||||
processData: {
|
||||
mutates: false,
|
||||
audits: [],
|
||||
publishes: [],
|
||||
consumes: [],
|
||||
},
|
||||
},
|
||||
realtimeChannels: [],
|
||||
jobs: [],
|
||||
requiresConsent: ["analytics"],
|
||||
} as const);
|
||||
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { processData: sym }, ctx),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("passes when analyticsEvents is empty and __analyzed brand is absent", () => {
|
||||
const container = new Container();
|
||||
const sym = Symbol("test.signIn");
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isCaptured,
|
||||
isAudited,
|
||||
isAnalyzed,
|
||||
isConsentChecked,
|
||||
} from "./brand-runtime";
|
||||
import { ConformanceError } from "./conformance-error";
|
||||
import type { BindContext } from "../di/bind-context";
|
||||
@@ -70,5 +71,12 @@ export function assertFeatureConformance(
|
||||
);
|
||||
}
|
||||
}
|
||||
if ((manifest.requiresConsent?.length ?? 0) > 0) {
|
||||
if (!isConsentChecked(bound)) {
|
||||
throw new ConformanceError(
|
||||
`${manifest.name}.${name}: feature declares requiresConsent but binding is missing __consentChecked brand — was withConsent applied at bind time?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,14 @@
|
||||
* commitment, not a mutable flag.
|
||||
*/
|
||||
|
||||
import type { Analyzed } from "./brands";
|
||||
import type { Analyzed, ConsentChecked } from "./brands";
|
||||
|
||||
type Brand = "__instrumented" | "__captured" | "__audited" | "__analyzed";
|
||||
type Brand =
|
||||
| "__instrumented"
|
||||
| "__captured"
|
||||
| "__audited"
|
||||
| "__analyzed"
|
||||
| "__consentChecked";
|
||||
|
||||
/**
|
||||
* Attaches the brand as a non-enumerable property on the given function.
|
||||
@@ -57,3 +62,9 @@ export function isAudited(fn: unknown): boolean {
|
||||
export function isAnalyzed<F extends object>(fn: unknown): fn is Analyzed<F> {
|
||||
return hasBrand(fn, "__analyzed");
|
||||
}
|
||||
|
||||
export function isConsentChecked<F extends object>(
|
||||
fn: unknown,
|
||||
): fn is ConsentChecked<F> {
|
||||
return hasBrand(fn, "__consentChecked");
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
export type Instrumented<F> = F & { readonly __instrumented: true };
|
||||
export type Captured<F> = F & { readonly __captured: true };
|
||||
export type Analyzed<F> = F & { readonly __analyzed: true };
|
||||
export type ConsentChecked<F> = F & { readonly __consentChecked: true };
|
||||
|
||||
@@ -29,6 +29,14 @@ export type FeatureManifest = {
|
||||
readonly realtimeChannels: readonly string[];
|
||||
readonly jobs: readonly string[];
|
||||
readonly coverage?: CoverageManifest;
|
||||
/**
|
||||
* Consent categories this feature's use cases require before processing
|
||||
* personal data. When non-empty, `assertFeatureConformance` requires every
|
||||
* bound use case to carry the `__consentChecked` brand from `withConsent`.
|
||||
* Defaults to `[]` — existing features with no consent requirements omit
|
||||
* or declare an empty array.
|
||||
*/
|
||||
readonly requiresConsent?: readonly string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type { Instrumented, Captured, Analyzed } from "./brands";
|
||||
export type {
|
||||
Instrumented,
|
||||
Captured,
|
||||
Analyzed,
|
||||
ConsentChecked,
|
||||
} from "./brands";
|
||||
export type { FeatureManifest, UseCaseManifest } from "./define-feature";
|
||||
export { defineFeature } from "./define-feature";
|
||||
export type {
|
||||
@@ -23,6 +28,7 @@ export {
|
||||
isCaptured,
|
||||
isAudited,
|
||||
isAnalyzed,
|
||||
isConsentChecked,
|
||||
} from "./brand-runtime";
|
||||
export { ConformanceError } from "./conformance-error";
|
||||
export { assertFeatureConformance } from "./assert-bindings";
|
||||
|
||||
@@ -2,7 +2,12 @@ import type { ITracer, SpanOpts } from "./tracer.interface";
|
||||
import type { Instrumented } from "../conformance/brands";
|
||||
import { attachBrand } from "../conformance/brand-runtime";
|
||||
|
||||
const PROPAGATED_BRANDS = ["__captured", "__audited", "__analyzed"] as const;
|
||||
const PROPAGATED_BRANDS = [
|
||||
"__captured",
|
||||
"__audited",
|
||||
"__analyzed",
|
||||
"__consentChecked",
|
||||
] as const;
|
||||
|
||||
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
||||
tracer: ITracer,
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -25,6 +25,9 @@ importers:
|
||||
"@types/node":
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.17
|
||||
"@typescript-eslint/parser":
|
||||
specifier: ^8.25.0
|
||||
version: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
fallow:
|
||||
specifier: ^2.73.0
|
||||
version: 2.73.0
|
||||
@@ -584,6 +587,31 @@ importers:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
packages/core-consent:
|
||||
dependencies:
|
||||
"@repo/core-shared":
|
||||
specifier: workspace:*
|
||||
version: link:../core-shared
|
||||
devDependencies:
|
||||
"@repo/core-eslint":
|
||||
specifier: workspace:*
|
||||
version: link:../core-eslint
|
||||
"@repo/core-testing":
|
||||
specifier: workspace:*
|
||||
version: link:../core-testing
|
||||
"@repo/core-typescript":
|
||||
specifier: workspace:*
|
||||
version: link:../core-typescript
|
||||
"@vitest/coverage-v8":
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))
|
||||
typescript:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
packages/core-eslint:
|
||||
dependencies:
|
||||
globals:
|
||||
|
||||
Reference in New Issue
Block a user