workspace-conformance 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/dist/cli.js +350 -13
- package/dist/index.cjs +352 -12
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +351 -14
- package/package.json +7 -1
package/dist/index.d.ts
CHANGED
|
@@ -397,6 +397,40 @@ interface CommandTypesOptions {
|
|
|
397
397
|
*/
|
|
398
398
|
readonly tsConfig?: string;
|
|
399
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* A set of schema files and the files whose exported types must be derived from those schemas.
|
|
402
|
+
*/
|
|
403
|
+
interface DerivedTypesPair {
|
|
404
|
+
/**
|
|
405
|
+
* Globs of the files that declare the schemas, relative to the directory the checks run in.
|
|
406
|
+
*/
|
|
407
|
+
readonly schemas: readonly string[];
|
|
408
|
+
/**
|
|
409
|
+
* Globs of the files whose exported interfaces and type aliases must be derived from a schema declared in `schemas`.
|
|
410
|
+
*/
|
|
411
|
+
readonly types: readonly string[];
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Options of the `derived-types` check: every type a file exports is derived from a schema of its pair.
|
|
415
|
+
*/
|
|
416
|
+
interface DerivedTypesOptions {
|
|
417
|
+
/**
|
|
418
|
+
* The schema files and the type files that must derive from them. Each type file is judged against the schemas of every pair that lists it.
|
|
419
|
+
*/
|
|
420
|
+
readonly pairs: readonly DerivedTypesPair[];
|
|
421
|
+
/**
|
|
422
|
+
* Names of exported types that are deliberately written by hand.
|
|
423
|
+
*/
|
|
424
|
+
readonly exclude?: readonly string[];
|
|
425
|
+
/**
|
|
426
|
+
* The last names of the generics that turn a schema into its type, and of the members that hold it, such as `infer` in `z.infer<typeof schema>` and `$inferSelect` in `typeof table.$inferSelect`. `infer`, `input`, `output`, `TypeOf`, `InferInput`, `InferOutput`, `InferSelectModel`, `InferInsertModel`, `$inferSelect` and `$inferInsert` when omitted.
|
|
427
|
+
*/
|
|
428
|
+
readonly inferences?: readonly string[];
|
|
429
|
+
/**
|
|
430
|
+
* The tsconfig that supplies compiler options and module resolution; `tsconfig.json` when omitted.
|
|
431
|
+
*/
|
|
432
|
+
readonly tsConfig?: string;
|
|
433
|
+
}
|
|
400
434
|
/**
|
|
401
435
|
* Options shared by the checks that cruise the workspace's import graph.
|
|
402
436
|
*/
|
|
@@ -474,6 +508,48 @@ interface DockerfilePackageManagerOptions {
|
|
|
474
508
|
*/
|
|
475
509
|
readonly packageJson?: string;
|
|
476
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* The severity a rule must have at least: `warn` is met by `warn` or `error`, `error` only by `error`.
|
|
513
|
+
*/
|
|
514
|
+
type EslintRequiredSeverity = "warn" | "error";
|
|
515
|
+
/**
|
|
516
|
+
* A file whose resolved ESLint configuration is checked.
|
|
517
|
+
*/
|
|
518
|
+
interface EslintSample {
|
|
519
|
+
/**
|
|
520
|
+
* The file ESLint would lint, relative to the directory the checks run in. It need not exist: ESLint resolves a file's configuration from its path alone.
|
|
521
|
+
*/
|
|
522
|
+
readonly path: string;
|
|
523
|
+
/**
|
|
524
|
+
* Rules required for this file on top of the check's `rules`, which this file's entries override.
|
|
525
|
+
*/
|
|
526
|
+
readonly rules?: Readonly<Record<string, EslintRequiredSeverity>>;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Options of the `eslint` check: the repository's own ESLint, resolved from the directory the checks run in, lints the sample files with the required rules.
|
|
530
|
+
*/
|
|
531
|
+
interface EslintOptions {
|
|
532
|
+
/**
|
|
533
|
+
* One file per kind of source the repository lints (`src/index.ts`, `package.json`, `eslint.config.ts`). A bare string is `{ path }`. Each must be linted: not ignored, and matched by a configuration block.
|
|
534
|
+
*/
|
|
535
|
+
readonly samples: readonly (string | EslintSample)[];
|
|
536
|
+
/**
|
|
537
|
+
* Rules required for every sample, with the severity each must have at least.
|
|
538
|
+
*/
|
|
539
|
+
readonly rules?: Readonly<Record<string, EslintRequiredSeverity>>;
|
|
540
|
+
/**
|
|
541
|
+
* The config file under test, relative to the directory the checks run in, instead of the one ESLint finds from there.
|
|
542
|
+
*/
|
|
543
|
+
readonly configFile?: string;
|
|
544
|
+
/**
|
|
545
|
+
* Also lint the workspace and report every message ESLint produces. Off when omitted: the default level lints nothing and only reads resolved configuration.
|
|
546
|
+
*/
|
|
547
|
+
readonly lint?: boolean;
|
|
548
|
+
/**
|
|
549
|
+
* What the full level lints, as file and directory patterns relative to the directory the checks run in. `.` when omitted. Used only with `lint`.
|
|
550
|
+
*/
|
|
551
|
+
readonly lintPatterns?: readonly string[];
|
|
552
|
+
}
|
|
477
553
|
/**
|
|
478
554
|
* Options of the `migrations-directory` check: the directory a schema generator writes migrations to is the one the deploy tool applies them from, and no script applies them with the generator.
|
|
479
555
|
*/
|
|
@@ -662,6 +738,7 @@ type SettingsReviewThreadResolutionOptions = SettingsOptions;
|
|
|
662
738
|
interface CheckOptionsByName {
|
|
663
739
|
readonly "aggregate-mappers": AggregateMappersOptions;
|
|
664
740
|
readonly "command-types": CommandTypesOptions;
|
|
741
|
+
readonly "derived-types": DerivedTypesOptions;
|
|
665
742
|
readonly "import-uphill": ImportGraphOptions;
|
|
666
743
|
readonly "import-rank-skip": ImportGraphOptions;
|
|
667
744
|
readonly "import-cross-slice": ImportGraphOptions;
|
|
@@ -672,6 +749,7 @@ interface CheckOptionsByName {
|
|
|
672
749
|
readonly "commit-types": CommitTypesOptions;
|
|
673
750
|
readonly "dockerfile-package-manager": DockerfilePackageManagerOptions;
|
|
674
751
|
readonly "migrations-directory": MigrationsDirectoryOptions;
|
|
752
|
+
readonly eslint: EslintOptions;
|
|
675
753
|
readonly "workflow-job-ordering": WorkflowJobOrderingOptions;
|
|
676
754
|
readonly "workflow-skippable-jobs": WorkflowSkippableJobsOptions;
|
|
677
755
|
readonly "workflow-runner-resolution": WorkflowRunnerResolutionOptions;
|
|
@@ -722,6 +800,18 @@ export declare const PRESET_COMMIT_TYPES: readonly string[];
|
|
|
722
800
|
*/
|
|
723
801
|
export declare const commitTypes: CheckFunction<CommitTypesOptions>;
|
|
724
802
|
//#endregion
|
|
803
|
+
//#region src/checks/derived-types.d.ts
|
|
804
|
+
/**
|
|
805
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
806
|
+
*/
|
|
807
|
+
export declare const DEFAULT_DERIVED_TYPE_INFERENCES: readonly string[];
|
|
808
|
+
/**
|
|
809
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
810
|
+
*
|
|
811
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
812
|
+
*/
|
|
813
|
+
export declare const derivedTypes: CheckFunction<DerivedTypesOptions>;
|
|
814
|
+
//#endregion
|
|
725
815
|
//#region src/checks/dockerfile-package-manager.d.ts
|
|
726
816
|
/**
|
|
727
817
|
* Every pin of the package manager that `packageManager` names, in each Dockerfile, is the version `packageManager` names, with any `+sha` integrity suffix ignored on both sides. Pins of other package managers are not the workspace's package manager and are left alone.
|
|
@@ -730,6 +820,16 @@ export declare const commitTypes: CheckFunction<CommitTypesOptions>;
|
|
|
730
820
|
*/
|
|
731
821
|
export declare const dockerfilePackageManager: CheckFunction<DockerfilePackageManagerOptions>;
|
|
732
822
|
//#endregion
|
|
823
|
+
//#region src/checks/eslint.d.ts
|
|
824
|
+
/**
|
|
825
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
826
|
+
*
|
|
827
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
828
|
+
*
|
|
829
|
+
* The config is a black box: the check sees which rules are active on a file, not why. It does not embed a config, so a repository without one is reported and nothing is substituted.
|
|
830
|
+
*/
|
|
831
|
+
export declare const eslint: CheckFunction<EslintOptions>;
|
|
832
|
+
//#endregion
|
|
733
833
|
//#region src/workspace/packages.d.ts
|
|
734
834
|
/**
|
|
735
835
|
* A package directory found under the workspace root, before the layout classifies it.
|
|
@@ -1222,4 +1322,4 @@ export declare function proveExhaustive(input: {
|
|
|
1222
1322
|
readonly compilerOptions?: CompilerOptions;
|
|
1223
1323
|
}): ExhaustivenessProof;
|
|
1224
1324
|
//#endregion
|
|
1225
|
-
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
|
1325
|
+
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { escapePath, glob, isDynamicPattern } from "tinyglobby";
|
|
4
|
-
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, ts } from "ts-morph";
|
|
5
|
+
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
5
6
|
import { createJitiLoader, validateStandard } from "cosmiconfig-extends";
|
|
6
7
|
import { readFile, realpath } from "node:fs/promises";
|
|
7
8
|
import { LineCounter, isMap, isNode, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
@@ -211,6 +212,32 @@ const aggregateMappers = async ({ cwd, options }) => {
|
|
|
211
212
|
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
212
213
|
};
|
|
213
214
|
//#endregion
|
|
215
|
+
//#region src/checks/type-references.ts
|
|
216
|
+
/**
|
|
217
|
+
* The identifier a reference ends in: `infer` in `z.infer`, the name itself when it is not qualified.
|
|
218
|
+
*/
|
|
219
|
+
function lastIdentifier(reference) {
|
|
220
|
+
if (Node.isQualifiedName(reference)) return reference.getRight();
|
|
221
|
+
if (Node.isPropertyAccessExpression(reference)) return reference.getNameNode();
|
|
222
|
+
return reference;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The declarations the identifier refers to, through any imports and re-exports.
|
|
226
|
+
*/
|
|
227
|
+
function declarationsOf(identifier) {
|
|
228
|
+
const symbol = identifier.getSymbol();
|
|
229
|
+
return (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations() ?? [];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The names a reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
233
|
+
*/
|
|
234
|
+
function referenceNames(reference) {
|
|
235
|
+
const identifier = lastIdentifier(reference);
|
|
236
|
+
const symbol = identifier.getSymbol();
|
|
237
|
+
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
238
|
+
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
214
241
|
//#region src/checks/command-types.ts
|
|
215
242
|
/**
|
|
216
243
|
* The last names of the generics that turn a schema into its type, used when the options name none: `z.infer`, `z.input`, `z.output`, `z.TypeOf` and Valibot's `InferInput` and `InferOutput`.
|
|
@@ -228,20 +255,10 @@ const DEFAULT_INFERENCES = [
|
|
|
228
255
|
*/
|
|
229
256
|
const STANDARD_SCHEMA_MEMBER = "~standard";
|
|
230
257
|
/**
|
|
231
|
-
* The names a type reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
232
|
-
*/
|
|
233
|
-
function namesOf(typeName) {
|
|
234
|
-
const identifier = Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
|
|
235
|
-
const symbol = identifier.getSymbol();
|
|
236
|
-
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
237
|
-
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
258
|
* The local alias without type parameters that a type reference names, or `undefined` when it names anything else. Such an alias is a name for the type it is written as, so it is judged by that.
|
|
241
259
|
*/
|
|
242
260
|
function aliasNamed(typeName) {
|
|
243
|
-
const
|
|
244
|
-
const declaration = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations().find(Node.isTypeAliasDeclaration);
|
|
261
|
+
const declaration = declarationsOf(lastIdentifier(typeName)).find(Node.isTypeAliasDeclaration);
|
|
245
262
|
return declaration?.getTypeParameters().length === 0 ? declaration : void 0;
|
|
246
263
|
}
|
|
247
264
|
/**
|
|
@@ -254,7 +271,7 @@ function problem$1(declaration, name, inferences, followed = /* @__PURE__ */ new
|
|
|
254
271
|
};
|
|
255
272
|
const written = declaration.getTypeNode();
|
|
256
273
|
if (Node.isTypeReference(written)) {
|
|
257
|
-
const inference =
|
|
274
|
+
const inference = referenceNames(written.getTypeName()).find((candidate) => inferences.includes(candidate));
|
|
258
275
|
if (inference !== void 0) {
|
|
259
276
|
const [argument] = written.getTypeArguments();
|
|
260
277
|
if (argument !== void 0 && Node.isTypeQuery(argument) && argument.getExprName().getType().getProperty(STANDARD_SCHEMA_MEMBER) !== void 0) return;
|
|
@@ -450,6 +467,120 @@ const commitTypes = async ({ cwd, options, configFiles }) => {
|
|
|
450
467
|
return violations;
|
|
451
468
|
};
|
|
452
469
|
//#endregion
|
|
470
|
+
//#region src/checks/derived-types.ts
|
|
471
|
+
/**
|
|
472
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
473
|
+
*/
|
|
474
|
+
const DEFAULT_DERIVED_TYPE_INFERENCES = [
|
|
475
|
+
...DEFAULT_INFERENCES,
|
|
476
|
+
"InferSelectModel",
|
|
477
|
+
"InferInsertModel",
|
|
478
|
+
"$inferSelect",
|
|
479
|
+
"$inferInsert"
|
|
480
|
+
];
|
|
481
|
+
/**
|
|
482
|
+
* Whether the node names something declared in one of the schema files.
|
|
483
|
+
*/
|
|
484
|
+
function declaredInSchema(name, derivation) {
|
|
485
|
+
const identifier = Node.isIdentifier(name) ? name : Node.isQualifiedName(name) ? name.getRight() : void 0;
|
|
486
|
+
return identifier !== void 0 && declarationsOf(identifier).some((declaration) => derivation.schemaFiles.has(relativePosix(derivation.cwd, declaration.getSourceFile().getFilePath())));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The interface or type alias without type parameters a reference names, when it has not been followed yet. Such a declaration is a name for what it is written as, so it is judged by that.
|
|
490
|
+
*/
|
|
491
|
+
function followable(reference, derivation) {
|
|
492
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
493
|
+
if (!Node.isIdentifier(name) && !Node.isQualifiedName(name) && !Node.isPropertyAccessExpression(name)) return;
|
|
494
|
+
const declaration = declarationsOf(lastIdentifier(name)).find((found) => Node.isTypeAliasDeclaration(found) || Node.isInterfaceDeclaration(found));
|
|
495
|
+
return declaration?.getTypeParameters().length === 0 && !derivation.followed.has(declaration) ? declaration : void 0;
|
|
496
|
+
}
|
|
497
|
+
function isNullish(node) {
|
|
498
|
+
return node.getKind() === SyntaxKind.UndefinedKeyword || Node.isLiteralTypeNode(node) && node.getLiteral().getKind() === SyntaxKind.NullKeyword;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Whether a reference is derived: an inference applied to `typeof schema`, an alias or interface that is itself derived, or another generic given a derived type, as `Partial<Order>` is.
|
|
502
|
+
*/
|
|
503
|
+
function derivedReference(reference, derivation) {
|
|
504
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
505
|
+
if ((Node.isIdentifier(name) || Node.isQualifiedName(name) || Node.isPropertyAccessExpression(name)) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate))) {
|
|
506
|
+
const [argument] = reference.getTypeArguments();
|
|
507
|
+
return argument !== void 0 && Node.isTypeQuery(argument) && declaredInSchema(argument.getExprName(), derivation);
|
|
508
|
+
}
|
|
509
|
+
const declaration = followable(reference, derivation);
|
|
510
|
+
if (declaration !== void 0) return derivedDeclaration(declaration, {
|
|
511
|
+
...derivation,
|
|
512
|
+
followed: /* @__PURE__ */ new Set([...derivation.followed, declaration])
|
|
513
|
+
});
|
|
514
|
+
return reference.getTypeArguments().some((argument) => derivedType(argument, derivation));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Whether a type, as written, is derived from a schema. A type literal is written by hand whatever its members are.
|
|
518
|
+
*/
|
|
519
|
+
function derivedType(node, derivation) {
|
|
520
|
+
if (Node.isTypeReference(node) || Node.isExpressionWithTypeArguments(node)) return derivedReference(node, derivation);
|
|
521
|
+
if (Node.isTypeQuery(node)) {
|
|
522
|
+
const name = node.getExprName();
|
|
523
|
+
return Node.isQualifiedName(name) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate)) && declaredInSchema(name.getLeft(), derivation);
|
|
524
|
+
}
|
|
525
|
+
if (Node.isParenthesizedTypeNode(node) || Node.isTypeOperatorTypeNode(node)) return derivedType(node.getTypeNode(), derivation);
|
|
526
|
+
if (Node.isArrayTypeNode(node)) return derivedType(node.getElementTypeNode(), derivation);
|
|
527
|
+
if (Node.isIndexedAccessTypeNode(node)) return derivedType(node.getObjectTypeNode(), derivation);
|
|
528
|
+
if (Node.isIntersectionTypeNode(node)) return node.getTypeNodes().some((member) => derivedType(member, derivation));
|
|
529
|
+
if (Node.isUnionTypeNode(node)) {
|
|
530
|
+
const members = node.getTypeNodes().filter((member) => !isNullish(member));
|
|
531
|
+
return members.length > 0 && members.every((member) => derivedType(member, derivation));
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Whether an interface or type alias is derived: an alias by the type it is written as, an interface by whether it extends a derived type.
|
|
537
|
+
*/
|
|
538
|
+
function derivedDeclaration(declaration, derivation) {
|
|
539
|
+
if (Node.isInterfaceDeclaration(declaration)) return declaration.getExtends().some((heritage) => derivedReference(heritage, derivation));
|
|
540
|
+
return derivedType(declaration.getTypeNodeOrThrow(), derivation);
|
|
541
|
+
}
|
|
542
|
+
async function pairViolations(cwd, options, pair, index) {
|
|
543
|
+
const { project, files } = await createProject(cwd, options.tsConfig, pair.types, {
|
|
544
|
+
check: "derived-types",
|
|
545
|
+
files: `pairs.${String(index)}.types`
|
|
546
|
+
});
|
|
547
|
+
const schemaFiles = await findFiles(cwd, pair.schemas);
|
|
548
|
+
if (schemaFiles.length === 0) throw new ConformanceError(`checks.derived-types.pairs.${String(index)}.schemas: no file matches`);
|
|
549
|
+
const excluded = new Set(options.exclude);
|
|
550
|
+
const derivation = {
|
|
551
|
+
inferences: new Set(options.inferences ?? DEFAULT_DERIVED_TYPE_INFERENCES),
|
|
552
|
+
schemaFiles: new Set(schemaFiles),
|
|
553
|
+
cwd,
|
|
554
|
+
followed: /* @__PURE__ */ new Set()
|
|
555
|
+
};
|
|
556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
557
|
+
const violations = [];
|
|
558
|
+
for (const file of files) for (const [name, declaration] of exportedTypes(project.getSourceFileOrThrow(resolve(cwd, file)))) {
|
|
559
|
+
if (excluded.has(name) || seen.has(declaration)) continue;
|
|
560
|
+
seen.add(declaration);
|
|
561
|
+
if (!derivedDeclaration(declaration, {
|
|
562
|
+
...derivation,
|
|
563
|
+
followed: /* @__PURE__ */ new Set([declaration])
|
|
564
|
+
})) violations.push({
|
|
565
|
+
code: "derived-types/not-derived",
|
|
566
|
+
message: `${name} is not derived from a schema in ${pair.schemas.join(", ")}: nothing in its definition applies an inference generic to one`,
|
|
567
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
568
|
+
location: declarationLocation(declaration)
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return violations;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
575
|
+
*
|
|
576
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
577
|
+
*/
|
|
578
|
+
const derivedTypes = async ({ cwd, options }) => {
|
|
579
|
+
const violations = [];
|
|
580
|
+
for (const [index, pair] of options.pairs.entries()) violations.push(...await pairViolations(cwd, options, pair, index));
|
|
581
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
582
|
+
};
|
|
583
|
+
//#endregion
|
|
453
584
|
//#region src/checks/dockerfile-package-manager.ts
|
|
454
585
|
const DEFAULT_DOCKERFILES = [
|
|
455
586
|
"**/Dockerfile",
|
|
@@ -542,6 +673,179 @@ const dockerfilePackageManager = async ({ cwd, options }) => {
|
|
|
542
673
|
return violations;
|
|
543
674
|
};
|
|
544
675
|
//#endregion
|
|
676
|
+
//#region src/checks/eslint.ts
|
|
677
|
+
/**
|
|
678
|
+
* The patterns the full level lints when the options name none.
|
|
679
|
+
*/
|
|
680
|
+
const DEFAULT_LINT_PATTERNS = ["."];
|
|
681
|
+
const SEVERITY_RANK = {
|
|
682
|
+
off: 0,
|
|
683
|
+
warn: 1,
|
|
684
|
+
error: 2
|
|
685
|
+
};
|
|
686
|
+
function hasEslintClass(value) {
|
|
687
|
+
return isRecord(value) && typeof value["ESLint"] === "function";
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The `ESLint` class of the ESLint installed where the checks run, found the way a module in `cwd` finds it. It is not this package's own copy, so the results agree with the repository's own lint script.
|
|
691
|
+
*/
|
|
692
|
+
function loadEslint(cwd) {
|
|
693
|
+
const requireFromCwd = createRequire(join(cwd, "package.json"));
|
|
694
|
+
try {
|
|
695
|
+
requireFromCwd.resolve("eslint");
|
|
696
|
+
} catch (error) {
|
|
697
|
+
throw new ConformanceError(`the eslint check uses the repository's own ESLint, and 'eslint' does not resolve from ${cwd}; install it there`, { cause: error });
|
|
698
|
+
}
|
|
699
|
+
const loaded = requireFromCwd("eslint");
|
|
700
|
+
if (!hasEslintClass(loaded)) throw new ConformanceError(`the 'eslint' that resolves from ${cwd} does not export the ESLint class of the Node API`);
|
|
701
|
+
return loaded.ESLint;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* The severity a rule entry resolves to. An entry is a severity or an array whose first element is one, and a severity is `0`, `1`, `2`, `'off'`, `'warn'` or `'error'`; `calculateConfigForFile` returns whichever form the config was written in.
|
|
705
|
+
*/
|
|
706
|
+
function normaliseSeverity(entry) {
|
|
707
|
+
const severity = Array.isArray(entry) ? entry[0] : entry;
|
|
708
|
+
if (severity === 0 || severity === "off") return "off";
|
|
709
|
+
if (severity === 1 || severity === "warn") return "warn";
|
|
710
|
+
if (severity === 2 || severity === "error") return "error";
|
|
711
|
+
throw new ConformanceError(`ESLint resolved ${JSON.stringify(entry)} as a rule entry, which is not a severity or an array that starts with one`);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* The severity of each rule in a resolved configuration.
|
|
715
|
+
*/
|
|
716
|
+
function resolvedSeverities(config) {
|
|
717
|
+
if (!isRecord(config)) throw new ConformanceError("ESLint resolved a configuration that is not an object");
|
|
718
|
+
const { rules } = config;
|
|
719
|
+
if (rules !== void 0 && !isRecord(rules)) throw new ConformanceError("ESLint resolved a configuration whose 'rules' is not an object");
|
|
720
|
+
return new Map(Object.entries(rules ?? {}).map(([rule, entry]) => [rule, normaliseSeverity(entry)]));
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Whether `error` is the one `calculateConfigForFile` throws when ESLint finds no config file for the path.
|
|
724
|
+
*/
|
|
725
|
+
function isMissingConfig(error) {
|
|
726
|
+
return isRecord(error) && error["messageTemplate"] === "config-file-missing";
|
|
727
|
+
}
|
|
728
|
+
function sampleOf(sample) {
|
|
729
|
+
return typeof sample === "string" ? { path: sample } : sample;
|
|
730
|
+
}
|
|
731
|
+
function ruleViolations(file, required, actual) {
|
|
732
|
+
return Object.entries(required).flatMap(([rule, severity]) => {
|
|
733
|
+
const found = actual.get(rule);
|
|
734
|
+
if (found === void 0) return [{
|
|
735
|
+
code: "eslint/rule-missing",
|
|
736
|
+
message: `the rule '${rule}' is not configured for ${file} (required: ${severity})`,
|
|
737
|
+
file
|
|
738
|
+
}];
|
|
739
|
+
if (found === "off") return [{
|
|
740
|
+
code: "eslint/rule-off",
|
|
741
|
+
message: `the rule '${rule}' is off for ${file} (required: ${severity})`,
|
|
742
|
+
file
|
|
743
|
+
}];
|
|
744
|
+
if (SEVERITY_RANK[found] < SEVERITY_RANK[severity]) return [{
|
|
745
|
+
code: "eslint/rule-too-weak",
|
|
746
|
+
message: `the rule '${rule}' is '${found}' for ${file} (required: ${severity})`,
|
|
747
|
+
file
|
|
748
|
+
}];
|
|
749
|
+
return [];
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
async function sampleViolations(eslint, sample, shared) {
|
|
753
|
+
const file = sample.path;
|
|
754
|
+
let config;
|
|
755
|
+
try {
|
|
756
|
+
config = await eslint.calculateConfigForFile(file);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (isMissingConfig(error)) return [{
|
|
759
|
+
code: "eslint/no-config",
|
|
760
|
+
message: `ESLint finds no config file for ${file}; the check does not fall back to a config of its own`,
|
|
761
|
+
file
|
|
762
|
+
}];
|
|
763
|
+
throw new ConformanceError(`ESLint could not resolve its configuration for ${file}`, { cause: error });
|
|
764
|
+
}
|
|
765
|
+
if (config === void 0) return [{
|
|
766
|
+
code: "eslint/not-linted",
|
|
767
|
+
message: `${file} is not linted: it is ignored, or no configuration block matches it`,
|
|
768
|
+
file
|
|
769
|
+
}];
|
|
770
|
+
return ruleViolations(file, {
|
|
771
|
+
...shared,
|
|
772
|
+
...sample.rules
|
|
773
|
+
}, resolvedSeverities(config));
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Whether `error` is ESLint refusing a pattern because it selects no file that is linted.
|
|
777
|
+
*/
|
|
778
|
+
function isNothingLinted(error) {
|
|
779
|
+
return error instanceof Error && isRecord(error) && (error["messageTemplate"] === "file-not-found" || error["messageTemplate"] === "all-matched-files-ignored");
|
|
780
|
+
}
|
|
781
|
+
function messageViolation(cwd, result, message) {
|
|
782
|
+
const file = relativePosix(cwd, result.filePath);
|
|
783
|
+
const location = {
|
|
784
|
+
line: message.line,
|
|
785
|
+
column: message.column
|
|
786
|
+
};
|
|
787
|
+
if (message.ruleId !== null) {
|
|
788
|
+
const severity = message.severity === 1 ? "warn" : "error";
|
|
789
|
+
return {
|
|
790
|
+
code: `eslint/lint/${message.ruleId}`,
|
|
791
|
+
message: `${message.ruleId} (${severity}): ${message.message}`,
|
|
792
|
+
file,
|
|
793
|
+
location
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return message.fatal === true ? {
|
|
797
|
+
code: "eslint/fatal",
|
|
798
|
+
message: message.message,
|
|
799
|
+
file,
|
|
800
|
+
location
|
|
801
|
+
} : {
|
|
802
|
+
code: "eslint/lint-message",
|
|
803
|
+
message: message.message,
|
|
804
|
+
file,
|
|
805
|
+
location
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function byPosition(left, right) {
|
|
809
|
+
return left.file.localeCompare(right.file) || (left.location?.line ?? 0) - (right.location?.line ?? 0) || (left.location?.column ?? 0) - (right.location?.column ?? 0) || left.code.localeCompare(right.code);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Lints the workspace through the Node API, with the repository's own config, and maps every message to a violation. A pattern that selects no linted file is itself a violation, since the workspace is then not linted at all.
|
|
813
|
+
*/
|
|
814
|
+
async function lintViolations(cwd, eslint, patterns) {
|
|
815
|
+
const violations = [];
|
|
816
|
+
const results = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const pattern of patterns) try {
|
|
818
|
+
for (const result of await eslint.lintFiles([pattern])) results.set(result.filePath, result);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (!isNothingLinted(error)) throw new ConformanceError(`ESLint could not lint '${pattern}'`, { cause: error });
|
|
821
|
+
violations.push({
|
|
822
|
+
code: "eslint/nothing-linted",
|
|
823
|
+
message: error.message,
|
|
824
|
+
file: pattern
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
for (const result of results.values()) for (const message of result.messages) violations.push(messageViolation(cwd, result, message));
|
|
828
|
+
return violations.sort(byPosition);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
832
|
+
*
|
|
833
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
834
|
+
*
|
|
835
|
+
* The config is a black box: the check sees which rules are active on a file, not why. It does not embed a config, so a repository without one is reported and nothing is substituted.
|
|
836
|
+
*/
|
|
837
|
+
const eslint = async ({ cwd, options }) => {
|
|
838
|
+
const instance = new (loadEslint(cwd))({
|
|
839
|
+
cwd,
|
|
840
|
+
...options.configFile === void 0 ? {} : { overrideConfigFile: resolve(cwd, options.configFile) }
|
|
841
|
+
});
|
|
842
|
+
const shared = options.rules ?? {};
|
|
843
|
+
const violations = [];
|
|
844
|
+
for (const sample of options.samples.map(sampleOf)) violations.push(...await sampleViolations(instance, sample, shared));
|
|
845
|
+
if (options.lint === true) violations.push(...await lintViolations(cwd, instance, options.lintPatterns ?? DEFAULT_LINT_PATTERNS));
|
|
846
|
+
return violations;
|
|
847
|
+
};
|
|
848
|
+
//#endregion
|
|
545
849
|
//#region src/workspace/rules.ts
|
|
546
850
|
const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/gu;
|
|
547
851
|
function escapeRegExp(text) {
|
|
@@ -2766,6 +3070,8 @@ function compiles(source) {
|
|
|
2766
3070
|
*/
|
|
2767
3071
|
const pattern = z.string().min(1).refine(compiles, "not a valid regular expression");
|
|
2768
3072
|
const patterns = z.array(pattern);
|
|
3073
|
+
const severity = z.enum(["warn", "error"]);
|
|
3074
|
+
const requiredRules = z.record(z.string().min(1), severity);
|
|
2769
3075
|
const selection = {
|
|
2770
3076
|
workflows: z.exactOptional(paths),
|
|
2771
3077
|
exclude: z.exactOptional(paths)
|
|
@@ -2802,6 +3108,15 @@ const checks = z.strictObject({
|
|
|
2802
3108
|
inferences: z.exactOptional(paths),
|
|
2803
3109
|
tsConfig: z.exactOptional(path)
|
|
2804
3110
|
}))),
|
|
3111
|
+
"derived-types": z.exactOptional(enabled(z.strictObject({
|
|
3112
|
+
pairs: z.array(z.strictObject({
|
|
3113
|
+
schemas: paths,
|
|
3114
|
+
types: paths
|
|
3115
|
+
})).min(1),
|
|
3116
|
+
exclude: z.exactOptional(names),
|
|
3117
|
+
inferences: z.exactOptional(paths),
|
|
3118
|
+
tsConfig: z.exactOptional(path)
|
|
3119
|
+
}))),
|
|
2805
3120
|
"import-uphill": z.exactOptional(enabled(importGraph)),
|
|
2806
3121
|
"import-rank-skip": z.exactOptional(enabled(importGraph)),
|
|
2807
3122
|
"import-cross-slice": z.exactOptional(enabled(importGraph)),
|
|
@@ -2825,6 +3140,16 @@ const checks = z.strictObject({
|
|
|
2825
3140
|
exclude: z.exactOptional(paths),
|
|
2826
3141
|
packageJson: z.exactOptional(path)
|
|
2827
3142
|
}))),
|
|
3143
|
+
eslint: z.exactOptional(enabled(z.strictObject({
|
|
3144
|
+
samples: z.array(z.union([path, z.strictObject({
|
|
3145
|
+
path,
|
|
3146
|
+
rules: z.exactOptional(requiredRules)
|
|
3147
|
+
})])).min(1),
|
|
3148
|
+
rules: z.exactOptional(requiredRules),
|
|
3149
|
+
configFile: z.exactOptional(path),
|
|
3150
|
+
lint: z.exactOptional(z.boolean()),
|
|
3151
|
+
lintPatterns: z.exactOptional(paths)
|
|
3152
|
+
}))),
|
|
2828
3153
|
"migrations-directory": z.exactOptional(enabled(z.strictObject({
|
|
2829
3154
|
generator: z.enum(generatorNames),
|
|
2830
3155
|
deployTool: z.enum(deployToolNames),
|
|
@@ -3039,6 +3364,12 @@ const registry = {
|
|
|
3039
3364
|
select: (checks) => enabledOptions(checks["command-types"]),
|
|
3040
3365
|
check: commandTypes
|
|
3041
3366
|
}),
|
|
3367
|
+
"derived-types": plain({
|
|
3368
|
+
name: "derived-types",
|
|
3369
|
+
description: "Every exported type in a file paired with schema files is derived from one of those schemas, not hand-written",
|
|
3370
|
+
select: (checks) => enabledOptions(checks["derived-types"]),
|
|
3371
|
+
check: derivedTypes
|
|
3372
|
+
}),
|
|
3042
3373
|
"import-uphill": importCheck({
|
|
3043
3374
|
name: "import-uphill",
|
|
3044
3375
|
description: "No package imports a package of a higher rank",
|
|
@@ -3099,6 +3430,12 @@ const registry = {
|
|
|
3099
3430
|
select: (checks) => enabledOptions(checks["migrations-directory"]),
|
|
3100
3431
|
check: migrationsDirectory
|
|
3101
3432
|
}),
|
|
3433
|
+
eslint: plain({
|
|
3434
|
+
name: "eslint",
|
|
3435
|
+
description: "The repository's own ESLint lints the sample files with the required rules enabled at the required severity",
|
|
3436
|
+
select: (checks) => enabledOptions(checks.eslint),
|
|
3437
|
+
check: eslint
|
|
3438
|
+
}),
|
|
3102
3439
|
"workflow-job-ordering": plain({
|
|
3103
3440
|
name: "workflow-job-ordering",
|
|
3104
3441
|
description: "Release, deploy and junction jobs are ordered so a failure stops what follows",
|
|
@@ -3380,4 +3717,4 @@ function proveExhaustive(input) {
|
|
|
3380
3717
|
}) };
|
|
3381
3718
|
}
|
|
3382
3719
|
//#endregion
|
|
3383
|
-
export { ConformanceError, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, dockerfilePackageManager, effectiveEnv, effectivePermissions, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
|
3720
|
+
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, derivedTypes, dockerfilePackageManager, effectiveEnv, effectivePermissions, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-conformance",
|
|
3
3
|
"description": "Conformance checks for a pnpm workspace that ESLint cannot express: type-graph, import-graph and cross-file checks driven by the shared layout section of @exadev/config",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-conformance": "./dist/cli.js"
|
|
7
7
|
},
|
|
@@ -72,8 +72,14 @@
|
|
|
72
72
|
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"cosmiconfig": "^9.0.0 || ^10.0.0",
|
|
75
|
+
"eslint": "^9.0.0 || ^10.0.0",
|
|
75
76
|
"typescript": "^5.9.0 || ^6.0.0"
|
|
76
77
|
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"eslint": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
77
83
|
"publishConfig": {
|
|
78
84
|
"access": "public",
|
|
79
85
|
"provenance": true
|