workspace-conformance 1.3.0 → 1.5.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 +24 -1
- package/dist/cli.js +286 -13
- package/dist/index.cjs +288 -12
- package/dist/index.d.cts +82 -1
- package/dist/index.d.ts +82 -1
- package/dist/index.js +287 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { escapePath, glob, isDynamicPattern } from "tinyglobby";
|
|
5
|
-
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, ts } from "ts-morph";
|
|
5
|
+
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
6
6
|
import { createJitiLoader, validateStandard } from "cosmiconfig-extends";
|
|
7
7
|
import { readFile, realpath } from "node:fs/promises";
|
|
8
8
|
import { LineCounter, isMap, isNode, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
@@ -99,6 +99,23 @@ function expandTemplate(template, values) {
|
|
|
99
99
|
return value;
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
+
const NAME_PLACEHOLDER = "{name}";
|
|
103
|
+
/**
|
|
104
|
+
* What a name template must be, as a configuration error says it. It does not quote the template, as no configuration error quotes a value.
|
|
105
|
+
*/
|
|
106
|
+
const NAME_TEMPLATE_REQUIREMENT = "needs {name} exactly once, other text beside it and no other placeholder";
|
|
107
|
+
const ANY_PLACEHOLDER = /\{[a-z]+\}/u;
|
|
108
|
+
/**
|
|
109
|
+
* `template` split at `{name}`, or `undefined` unless it holds `{name}` exactly once, other text beside it and no other placeholder. Without other text every name would match, and with `{name}` twice a name could be split more than one way.
|
|
110
|
+
*/
|
|
111
|
+
function parseNameTemplate(template) {
|
|
112
|
+
const [before, after, ...rest] = template.split(NAME_PLACEHOLDER);
|
|
113
|
+
if (before === void 0 || after === void 0 || rest.length > 0 || before + after === "" || ANY_PLACEHOLDER.test(before + after)) return;
|
|
114
|
+
return {
|
|
115
|
+
before,
|
|
116
|
+
after
|
|
117
|
+
};
|
|
118
|
+
}
|
|
102
119
|
//#endregion
|
|
103
120
|
//#region src/ts-project.ts
|
|
104
121
|
/**
|
|
@@ -212,6 +229,123 @@ const aggregateMappers = async ({ cwd, options }) => {
|
|
|
212
229
|
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
213
230
|
};
|
|
214
231
|
//#endregion
|
|
232
|
+
//#region src/checks/codec-pairs.ts
|
|
233
|
+
function collect(container, prefix, found, entered) {
|
|
234
|
+
for (const [name, declarations] of container.getExportedDeclarations()) {
|
|
235
|
+
const value = declarations.find((declaration) => !Node.isInterfaceDeclaration(declaration) && !Node.isTypeAliasDeclaration(declaration) && !Node.isModuleDeclaration(declaration) && !Node.isSourceFile(declaration));
|
|
236
|
+
if (value !== void 0) found.set(`${prefix}${name}`, value);
|
|
237
|
+
for (const member of declarations) if (Node.isModuleDeclaration(member)) collect(member, `${prefix}${name}.`, found, entered);
|
|
238
|
+
else if (Node.isSourceFile(member) && !entered.has(member)) collect(member, `${prefix}${name}.`, found, /* @__PURE__ */ new Set([...entered, member]));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The values `file` exports, by name, wherever they are declared: a re-export counts. Those inside an exported namespace, or in a module re-exported as a namespace (`export * as ns from './x'`), are included under their qualified name (`Namespace.value`). Interfaces and type aliases are types, not codecs, and a namespace is where codecs live rather than one itself, so all three are left out.
|
|
243
|
+
*/
|
|
244
|
+
function exportedValues(file) {
|
|
245
|
+
const found = /* @__PURE__ */ new Map();
|
|
246
|
+
collect(file, "", found, /* @__PURE__ */ new Set([file]));
|
|
247
|
+
return found;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Where the declaration's name is written, or where the declaration starts when it has no name node.
|
|
251
|
+
*/
|
|
252
|
+
function valueLocation(declaration) {
|
|
253
|
+
const nameNode = Node.hasName(declaration) ? declaration.getNameNode() : void 0;
|
|
254
|
+
const { line, column } = declaration.getSourceFile().getLineAndColumnAtPos((nameNode ?? declaration).getStart());
|
|
255
|
+
return {
|
|
256
|
+
line,
|
|
257
|
+
column
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The template of an option, which the configuration has already validated; a template that does not parse is a configuration error naming the option.
|
|
262
|
+
*/
|
|
263
|
+
function templateOf(options, option) {
|
|
264
|
+
const template = parseNameTemplate(options[option]);
|
|
265
|
+
if (template === void 0) throw new ConformanceError(`checks.codec-pairs.${option}: ${NAME_TEMPLATE_REQUIREMENT}`);
|
|
266
|
+
return template;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The shared name a qualified export name gives under `template`, or `undefined` when its last segment does not fit the template. The namespace part is kept apart so a counterpart is looked for in the same namespace.
|
|
270
|
+
*/
|
|
271
|
+
function sharedName(qualified, template) {
|
|
272
|
+
const namespace = qualified.slice(0, qualified.lastIndexOf(".") + 1);
|
|
273
|
+
const local = qualified.slice(namespace.length);
|
|
274
|
+
if (local.length <= template.before.length + template.after.length || !local.startsWith(template.before) || !local.endsWith(template.after)) return;
|
|
275
|
+
return {
|
|
276
|
+
namespace,
|
|
277
|
+
name: local.slice(template.before.length, local.length - template.after.length)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Every encoder a codec file exports has the decoder its name implies, exported by the same file, and every decoder the encoder. A name is an encoder or a decoder when it fits that template; other exports are not codecs and are not judged. A half re-exported from another file counts, and is reported where it is declared.
|
|
282
|
+
*
|
|
283
|
+
* It checks that the counterpart is exported, not that the two round-trip: that is a property test of the repository's own.
|
|
284
|
+
*/
|
|
285
|
+
const codecPairs = async ({ cwd, options }) => {
|
|
286
|
+
const { project, files } = await createProject(cwd, options.tsConfig, options.codecs, {
|
|
287
|
+
check: "codec-pairs",
|
|
288
|
+
files: "codecs"
|
|
289
|
+
});
|
|
290
|
+
const excluded = new Set(options.exclude);
|
|
291
|
+
const encoder = templateOf(options, "encoder");
|
|
292
|
+
const decoder = templateOf(options, "decoder");
|
|
293
|
+
const halves = [{
|
|
294
|
+
template: encoder,
|
|
295
|
+
counterpart: decoder,
|
|
296
|
+
missing: "decoder"
|
|
297
|
+
}, {
|
|
298
|
+
template: decoder,
|
|
299
|
+
counterpart: encoder,
|
|
300
|
+
missing: "encoder"
|
|
301
|
+
}];
|
|
302
|
+
const violations = [];
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const values = exportedValues(project.getSourceFileOrThrow(resolve(cwd, file)));
|
|
305
|
+
for (const [qualified, declaration] of values) {
|
|
306
|
+
if (excluded.has(qualified)) continue;
|
|
307
|
+
for (const { template, counterpart, missing } of halves) {
|
|
308
|
+
const shared = sharedName(qualified, template);
|
|
309
|
+
if (shared === void 0) continue;
|
|
310
|
+
const expected = `${shared.namespace}${counterpart.before}${shared.name}${counterpart.after}`;
|
|
311
|
+
if (!values.has(expected)) violations.push({
|
|
312
|
+
code: `codec-pairs/missing-${missing}`,
|
|
313
|
+
message: `${qualified} has no ${missing}: ${file} does not export ${expected}`,
|
|
314
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
315
|
+
location: valueLocation(declaration)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
321
|
+
};
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/checks/type-references.ts
|
|
324
|
+
/**
|
|
325
|
+
* The identifier a reference ends in: `infer` in `z.infer`, the name itself when it is not qualified.
|
|
326
|
+
*/
|
|
327
|
+
function lastIdentifier(reference) {
|
|
328
|
+
if (Node.isQualifiedName(reference)) return reference.getRight();
|
|
329
|
+
if (Node.isPropertyAccessExpression(reference)) return reference.getNameNode();
|
|
330
|
+
return reference;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The declarations the identifier refers to, through any imports and re-exports.
|
|
334
|
+
*/
|
|
335
|
+
function declarationsOf(identifier) {
|
|
336
|
+
const symbol = identifier.getSymbol();
|
|
337
|
+
return (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations() ?? [];
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 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.
|
|
341
|
+
*/
|
|
342
|
+
function referenceNames(reference) {
|
|
343
|
+
const identifier = lastIdentifier(reference);
|
|
344
|
+
const symbol = identifier.getSymbol();
|
|
345
|
+
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
346
|
+
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
215
349
|
//#region src/checks/command-types.ts
|
|
216
350
|
/**
|
|
217
351
|
* 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`.
|
|
@@ -229,20 +363,10 @@ const DEFAULT_INFERENCES = [
|
|
|
229
363
|
*/
|
|
230
364
|
const STANDARD_SCHEMA_MEMBER = "~standard";
|
|
231
365
|
/**
|
|
232
|
-
* 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.
|
|
233
|
-
*/
|
|
234
|
-
function namesOf(typeName) {
|
|
235
|
-
const identifier = Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
|
|
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
|
-
/**
|
|
241
366
|
* 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.
|
|
242
367
|
*/
|
|
243
368
|
function aliasNamed(typeName) {
|
|
244
|
-
const
|
|
245
|
-
const declaration = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations().find(Node.isTypeAliasDeclaration);
|
|
369
|
+
const declaration = declarationsOf(lastIdentifier(typeName)).find(Node.isTypeAliasDeclaration);
|
|
246
370
|
return declaration?.getTypeParameters().length === 0 ? declaration : void 0;
|
|
247
371
|
}
|
|
248
372
|
/**
|
|
@@ -255,7 +379,7 @@ function problem$1(declaration, name, inferences, followed = /* @__PURE__ */ new
|
|
|
255
379
|
};
|
|
256
380
|
const written = declaration.getTypeNode();
|
|
257
381
|
if (Node.isTypeReference(written)) {
|
|
258
|
-
const inference =
|
|
382
|
+
const inference = referenceNames(written.getTypeName()).find((candidate) => inferences.includes(candidate));
|
|
259
383
|
if (inference !== void 0) {
|
|
260
384
|
const [argument] = written.getTypeArguments();
|
|
261
385
|
if (argument !== void 0 && Node.isTypeQuery(argument) && argument.getExprName().getType().getProperty(STANDARD_SCHEMA_MEMBER) !== void 0) return;
|
|
@@ -451,6 +575,120 @@ const commitTypes = async ({ cwd, options, configFiles }) => {
|
|
|
451
575
|
return violations;
|
|
452
576
|
};
|
|
453
577
|
//#endregion
|
|
578
|
+
//#region src/checks/derived-types.ts
|
|
579
|
+
/**
|
|
580
|
+
* 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`.
|
|
581
|
+
*/
|
|
582
|
+
const DEFAULT_DERIVED_TYPE_INFERENCES = [
|
|
583
|
+
...DEFAULT_INFERENCES,
|
|
584
|
+
"InferSelectModel",
|
|
585
|
+
"InferInsertModel",
|
|
586
|
+
"$inferSelect",
|
|
587
|
+
"$inferInsert"
|
|
588
|
+
];
|
|
589
|
+
/**
|
|
590
|
+
* Whether the node names something declared in one of the schema files.
|
|
591
|
+
*/
|
|
592
|
+
function declaredInSchema(name, derivation) {
|
|
593
|
+
const identifier = Node.isIdentifier(name) ? name : Node.isQualifiedName(name) ? name.getRight() : void 0;
|
|
594
|
+
return identifier !== void 0 && declarationsOf(identifier).some((declaration) => derivation.schemaFiles.has(relativePosix(derivation.cwd, declaration.getSourceFile().getFilePath())));
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* 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.
|
|
598
|
+
*/
|
|
599
|
+
function followable(reference, derivation) {
|
|
600
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
601
|
+
if (!Node.isIdentifier(name) && !Node.isQualifiedName(name) && !Node.isPropertyAccessExpression(name)) return;
|
|
602
|
+
const declaration = declarationsOf(lastIdentifier(name)).find((found) => Node.isTypeAliasDeclaration(found) || Node.isInterfaceDeclaration(found));
|
|
603
|
+
return declaration?.getTypeParameters().length === 0 && !derivation.followed.has(declaration) ? declaration : void 0;
|
|
604
|
+
}
|
|
605
|
+
function isNullish(node) {
|
|
606
|
+
return node.getKind() === SyntaxKind.UndefinedKeyword || Node.isLiteralTypeNode(node) && node.getLiteral().getKind() === SyntaxKind.NullKeyword;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 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.
|
|
610
|
+
*/
|
|
611
|
+
function derivedReference(reference, derivation) {
|
|
612
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
613
|
+
if ((Node.isIdentifier(name) || Node.isQualifiedName(name) || Node.isPropertyAccessExpression(name)) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate))) {
|
|
614
|
+
const [argument] = reference.getTypeArguments();
|
|
615
|
+
return argument !== void 0 && Node.isTypeQuery(argument) && declaredInSchema(argument.getExprName(), derivation);
|
|
616
|
+
}
|
|
617
|
+
const declaration = followable(reference, derivation);
|
|
618
|
+
if (declaration !== void 0) return derivedDeclaration(declaration, {
|
|
619
|
+
...derivation,
|
|
620
|
+
followed: /* @__PURE__ */ new Set([...derivation.followed, declaration])
|
|
621
|
+
});
|
|
622
|
+
return reference.getTypeArguments().some((argument) => derivedType(argument, derivation));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Whether a type, as written, is derived from a schema. A type literal is written by hand whatever its members are.
|
|
626
|
+
*/
|
|
627
|
+
function derivedType(node, derivation) {
|
|
628
|
+
if (Node.isTypeReference(node) || Node.isExpressionWithTypeArguments(node)) return derivedReference(node, derivation);
|
|
629
|
+
if (Node.isTypeQuery(node)) {
|
|
630
|
+
const name = node.getExprName();
|
|
631
|
+
return Node.isQualifiedName(name) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate)) && declaredInSchema(name.getLeft(), derivation);
|
|
632
|
+
}
|
|
633
|
+
if (Node.isParenthesizedTypeNode(node) || Node.isTypeOperatorTypeNode(node)) return derivedType(node.getTypeNode(), derivation);
|
|
634
|
+
if (Node.isArrayTypeNode(node)) return derivedType(node.getElementTypeNode(), derivation);
|
|
635
|
+
if (Node.isIndexedAccessTypeNode(node)) return derivedType(node.getObjectTypeNode(), derivation);
|
|
636
|
+
if (Node.isIntersectionTypeNode(node)) return node.getTypeNodes().some((member) => derivedType(member, derivation));
|
|
637
|
+
if (Node.isUnionTypeNode(node)) {
|
|
638
|
+
const members = node.getTypeNodes().filter((member) => !isNullish(member));
|
|
639
|
+
return members.length > 0 && members.every((member) => derivedType(member, derivation));
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* 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.
|
|
645
|
+
*/
|
|
646
|
+
function derivedDeclaration(declaration, derivation) {
|
|
647
|
+
if (Node.isInterfaceDeclaration(declaration)) return declaration.getExtends().some((heritage) => derivedReference(heritage, derivation));
|
|
648
|
+
return derivedType(declaration.getTypeNodeOrThrow(), derivation);
|
|
649
|
+
}
|
|
650
|
+
async function pairViolations(cwd, options, pair, index) {
|
|
651
|
+
const { project, files } = await createProject(cwd, options.tsConfig, pair.types, {
|
|
652
|
+
check: "derived-types",
|
|
653
|
+
files: `pairs.${String(index)}.types`
|
|
654
|
+
});
|
|
655
|
+
const schemaFiles = await findFiles(cwd, pair.schemas);
|
|
656
|
+
if (schemaFiles.length === 0) throw new ConformanceError(`checks.derived-types.pairs.${String(index)}.schemas: no file matches`);
|
|
657
|
+
const excluded = new Set(options.exclude);
|
|
658
|
+
const derivation = {
|
|
659
|
+
inferences: new Set(options.inferences ?? DEFAULT_DERIVED_TYPE_INFERENCES),
|
|
660
|
+
schemaFiles: new Set(schemaFiles),
|
|
661
|
+
cwd,
|
|
662
|
+
followed: /* @__PURE__ */ new Set()
|
|
663
|
+
};
|
|
664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
665
|
+
const violations = [];
|
|
666
|
+
for (const file of files) for (const [name, declaration] of exportedTypes(project.getSourceFileOrThrow(resolve(cwd, file)))) {
|
|
667
|
+
if (excluded.has(name) || seen.has(declaration)) continue;
|
|
668
|
+
seen.add(declaration);
|
|
669
|
+
if (!derivedDeclaration(declaration, {
|
|
670
|
+
...derivation,
|
|
671
|
+
followed: /* @__PURE__ */ new Set([declaration])
|
|
672
|
+
})) violations.push({
|
|
673
|
+
code: "derived-types/not-derived",
|
|
674
|
+
message: `${name} is not derived from a schema in ${pair.schemas.join(", ")}: nothing in its definition applies an inference generic to one`,
|
|
675
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
676
|
+
location: declarationLocation(declaration)
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return violations;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* 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`).
|
|
683
|
+
*
|
|
684
|
+
* 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.
|
|
685
|
+
*/
|
|
686
|
+
const derivedTypes = async ({ cwd, options }) => {
|
|
687
|
+
const violations = [];
|
|
688
|
+
for (const [index, pair] of options.pairs.entries()) violations.push(...await pairViolations(cwd, options, pair, index));
|
|
689
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
690
|
+
};
|
|
691
|
+
//#endregion
|
|
454
692
|
//#region src/checks/dockerfile-package-manager.ts
|
|
455
693
|
const DEFAULT_DOCKERFILES = [
|
|
456
694
|
"**/Dockerfile",
|
|
@@ -2940,6 +3178,10 @@ function compiles(source) {
|
|
|
2940
3178
|
*/
|
|
2941
3179
|
const pattern = z.string().min(1).refine(compiles, "not a valid regular expression");
|
|
2942
3180
|
const patterns = z.array(pattern);
|
|
3181
|
+
/**
|
|
3182
|
+
* A name template of `codec-pairs`.
|
|
3183
|
+
*/
|
|
3184
|
+
const nameTemplate = z.string().refine((template) => parseNameTemplate(template) !== void 0, NAME_TEMPLATE_REQUIREMENT);
|
|
2943
3185
|
const severity = z.enum(["warn", "error"]);
|
|
2944
3186
|
const requiredRules = z.record(z.string().min(1), severity);
|
|
2945
3187
|
const selection = {
|
|
@@ -2972,12 +3214,31 @@ const checks = z.strictObject({
|
|
|
2972
3214
|
exclude: z.exactOptional(names),
|
|
2973
3215
|
tsConfig: z.exactOptional(path)
|
|
2974
3216
|
}))),
|
|
3217
|
+
"codec-pairs": z.exactOptional(enabled(z.strictObject({
|
|
3218
|
+
codecs: paths,
|
|
3219
|
+
encoder: nameTemplate,
|
|
3220
|
+
decoder: nameTemplate,
|
|
3221
|
+
exclude: z.exactOptional(names),
|
|
3222
|
+
tsConfig: z.exactOptional(path)
|
|
3223
|
+
}).refine((options) => options.encoder !== options.decoder, {
|
|
3224
|
+
path: ["decoder"],
|
|
3225
|
+
message: "the decoder template is the encoder template, so every name would be its own pair"
|
|
3226
|
+
}))),
|
|
2975
3227
|
"command-types": z.exactOptional(enabled(z.strictObject({
|
|
2976
3228
|
commands: paths,
|
|
2977
3229
|
exclude: z.exactOptional(names),
|
|
2978
3230
|
inferences: z.exactOptional(paths),
|
|
2979
3231
|
tsConfig: z.exactOptional(path)
|
|
2980
3232
|
}))),
|
|
3233
|
+
"derived-types": z.exactOptional(enabled(z.strictObject({
|
|
3234
|
+
pairs: z.array(z.strictObject({
|
|
3235
|
+
schemas: paths,
|
|
3236
|
+
types: paths
|
|
3237
|
+
})).min(1),
|
|
3238
|
+
exclude: z.exactOptional(names),
|
|
3239
|
+
inferences: z.exactOptional(paths),
|
|
3240
|
+
tsConfig: z.exactOptional(path)
|
|
3241
|
+
}))),
|
|
2981
3242
|
"import-uphill": z.exactOptional(enabled(importGraph)),
|
|
2982
3243
|
"import-rank-skip": z.exactOptional(enabled(importGraph)),
|
|
2983
3244
|
"import-cross-slice": z.exactOptional(enabled(importGraph)),
|
|
@@ -3219,12 +3480,24 @@ const registry = {
|
|
|
3219
3480
|
select: (checks) => enabledOptions(checks["aggregate-mappers"]),
|
|
3220
3481
|
check: aggregateMappers
|
|
3221
3482
|
}),
|
|
3483
|
+
"codec-pairs": plain({
|
|
3484
|
+
name: "codec-pairs",
|
|
3485
|
+
description: "Every exported encoder in a codec file has a decoder under the naming templates, and every decoder an encoder",
|
|
3486
|
+
select: (checks) => enabledOptions(checks["codec-pairs"]),
|
|
3487
|
+
check: codecPairs
|
|
3488
|
+
}),
|
|
3222
3489
|
"command-types": plain({
|
|
3223
3490
|
name: "command-types",
|
|
3224
3491
|
description: "Every exported command type is derived from a schema, not hand-written",
|
|
3225
3492
|
select: (checks) => enabledOptions(checks["command-types"]),
|
|
3226
3493
|
check: commandTypes
|
|
3227
3494
|
}),
|
|
3495
|
+
"derived-types": plain({
|
|
3496
|
+
name: "derived-types",
|
|
3497
|
+
description: "Every exported type in a file paired with schema files is derived from one of those schemas, not hand-written",
|
|
3498
|
+
select: (checks) => enabledOptions(checks["derived-types"]),
|
|
3499
|
+
check: derivedTypes
|
|
3500
|
+
}),
|
|
3228
3501
|
"import-uphill": importCheck({
|
|
3229
3502
|
name: "import-uphill",
|
|
3230
3503
|
description: "No package imports a package of a higher rank",
|
|
@@ -3572,4 +3845,4 @@ function proveExhaustive(input) {
|
|
|
3572
3845
|
}) };
|
|
3573
3846
|
}
|
|
3574
3847
|
//#endregion
|
|
3575
|
-
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, 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 };
|
|
3848
|
+
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, codecPairs, 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.5.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-conformance": "./dist/cli.js"
|
|
7
7
|
},
|