oxlint-plugin-arch 0.13.1 → 0.13.2
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/CHANGELOG.md +40 -0
- package/README.md +6 -6
- package/dist/index.js +1411 -267
- package/dist/preset/groups/top.d.ts +2 -0
- package/dist/preset/preset-sets.d.ts +2 -0
- package/dist/preset/preset.constants.d.ts +0 -1
- package/dist/preset/preset.js +251 -41
- package/dist/preset/preset.utils.d.ts +1 -0
- package/dist/rules/code/code-inside.d.ts +1 -0
- package/dist/rules/effect/effect-layer-keys.d.ts +1 -0
- package/dist/rules/effect/effect-service-params.d.ts +1 -0
- package/dist/rules/effect/effect-yield-name.d.ts +1 -0
- package/dist/rules/effect/effect-yields-first.d.ts +1 -0
- package/dist/rules/files/files-siblings.d.ts +1 -0
- package/dist/rules/files/files-top-level.d.ts +1 -0
- package/dist/rules/naming/naming-imports.d.ts +1 -0
- package/dist/rules/naming/naming-keys.d.ts +1 -0
- package/dist/rules/naming/naming-labels.d.ts +1 -0
- package/dist/rules/naming/naming-pairs.d.ts +1 -0
- package/dist/types/boundaries.types.d.ts +7 -0
- package/dist/types/code.types.d.ts +11 -0
- package/dist/types/effect.types.d.ts +20 -0
- package/dist/types/files.types.d.ts +33 -3
- package/dist/types/naming.types.d.ts +70 -2
- package/dist/types/preset.types.d.ts +26 -0
- package/dist/utils/naming.utils.d.ts +4 -1
- package/dist/utils/options.utils.d.ts +1 -0
- package/dist/vendor/dillon-anti-slop/index.js +3 -0
- package/package.json +1 -1
|
@@ -80,7 +80,6 @@ export declare const presetEffectServerImports: {
|
|
|
80
80
|
export declare const presetRecipeMark: unique symbol;
|
|
81
81
|
export declare const presetDetectBackend: string[];
|
|
82
82
|
export declare const presetDetectSkip: string[];
|
|
83
|
-
export declare const presetStylesheetNames: string[];
|
|
84
83
|
export declare const presetRegistryFiles: string[];
|
|
85
84
|
export declare const presetRegistrySkip: string[];
|
|
86
85
|
export declare const presetServerEntries: string[];
|
package/dist/preset/preset.js
CHANGED
|
@@ -399,6 +399,7 @@ var presetGroupOrder = [
|
|
|
399
399
|
"kit",
|
|
400
400
|
"limits",
|
|
401
401
|
"deny",
|
|
402
|
+
"top",
|
|
402
403
|
"externals"
|
|
403
404
|
];
|
|
404
405
|
var presetDefaultGroups = [
|
|
@@ -412,6 +413,7 @@ var presetDefaultGroups = [
|
|
|
412
413
|
"imports",
|
|
413
414
|
"tests",
|
|
414
415
|
"deny",
|
|
416
|
+
"top",
|
|
415
417
|
"externals"
|
|
416
418
|
];
|
|
417
419
|
var presetExternals = [
|
|
@@ -432,7 +434,7 @@ var presetGroupOptions = {
|
|
|
432
434
|
limits: { level: "error", lines: 400, types: 1000, complexity: 32 },
|
|
433
435
|
style: { level: "error", quotes: "single", semicolons: true, multiline: true, padding: true, dashes: true },
|
|
434
436
|
basics: { level: "error", trivial: true, state: true, switch: true, negation: true },
|
|
435
|
-
naming: { level: "error", files: true, exports: true, folders: true, unique: undefined },
|
|
437
|
+
naming: { level: "error", files: true, exports: true, folders: true, unique: undefined, imports: {} },
|
|
436
438
|
comments: { level: "error", directives: true },
|
|
437
439
|
types: { level: "error", placement: true, safety: true, colocated: [] },
|
|
438
440
|
imports: { level: "error", sort: true, boundaries: true, aliases: {}, internal: undefined },
|
|
@@ -449,9 +451,10 @@ var presetGroupOptions = {
|
|
|
449
451
|
schemas: { level: "error" },
|
|
450
452
|
forms: { level: "error" },
|
|
451
453
|
sql: { level: "error" },
|
|
452
|
-
effect: { level: "error", code: true, width: 140, exports: undefined, skip: undefined, entries: [] },
|
|
454
|
+
effect: { level: "error", code: true, width: 140, exports: undefined, skip: undefined, entries: [], files: [], strict: true },
|
|
453
455
|
console: { level: "error" },
|
|
454
|
-
deny: { level: "error", imports: {}, calls: {} },
|
|
456
|
+
deny: { level: "error", imports: {}, calls: {}, only: [] },
|
|
457
|
+
top: { level: "error", allow: undefined, deny: undefined, message: undefined, stem: undefined, singularize: undefined, roles: undefined, separators: undefined, file: undefined, placeholder: undefined },
|
|
455
458
|
externals: { level: "error" }
|
|
456
459
|
};
|
|
457
460
|
var presetQueryWrites = ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"];
|
|
@@ -518,7 +521,6 @@ var presetEffectServerImports = {
|
|
|
518
521
|
var presetRecipeMark = Symbol("recipe");
|
|
519
522
|
var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
|
|
520
523
|
var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
|
|
521
|
-
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
522
524
|
var presetRegistryFiles = ["core/core.services.ts", "service.ts"];
|
|
523
525
|
var presetRegistrySkip = ["brand", "database", "db", "math", "security", "tools"];
|
|
524
526
|
var presetServerEntries = ["rpc", "server", "security", "services"];
|
|
@@ -790,7 +792,8 @@ function presetSource(root, directory, configured) {
|
|
|
790
792
|
}
|
|
791
793
|
function presetFilled(conventions, exists) {
|
|
792
794
|
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
793
|
-
const
|
|
795
|
+
const denied = folder instanceof Object && folder.deny instanceof Object ? { ...folder.deny.imports, ...folder.deny.calls } : undefined;
|
|
796
|
+
const empty = denied !== undefined && Object.keys(denied).length === 0;
|
|
794
797
|
return exists(path) && !empty;
|
|
795
798
|
}));
|
|
796
799
|
}
|
|
@@ -814,8 +817,27 @@ function presetMerge(base, top) {
|
|
|
814
817
|
const merged = presetMergeObject(base, top, 2);
|
|
815
818
|
const before = base.layout instanceof Object ? base.layout.files : undefined;
|
|
816
819
|
const after = top.layout instanceof Object ? top.layout.files : undefined;
|
|
820
|
+
const owned = [base.deny, top.deny].flatMap((deny) => deny instanceof Object ? deny.only ?? [] : []);
|
|
821
|
+
if (owned.length > 0 && merged.deny instanceof Object) {
|
|
822
|
+
merged.deny = { ...merged.deny, only: owned };
|
|
823
|
+
}
|
|
817
824
|
return before && after && merged.layout instanceof Object ? { ...merged, layout: { ...merged.layout, files: [...before, ...after] } } : merged;
|
|
818
825
|
}
|
|
826
|
+
function presetNames(inherited, entries, option) {
|
|
827
|
+
if (!Array.isArray(entries) || !entries.every((entry) => typeof entry === "string" && presetLayoutFileName.test(entry))) {
|
|
828
|
+
throw new PresetError(`${option} takes file names such as \`*.prompts.ts\`; \`!*.utils.ts\` removes one.`);
|
|
829
|
+
}
|
|
830
|
+
const names = [...inherited];
|
|
831
|
+
for (const entry of entries) {
|
|
832
|
+
const index = names.indexOf(entry.startsWith("!") ? entry.slice(1) : entry);
|
|
833
|
+
if (entry.startsWith("!") && index !== -1) {
|
|
834
|
+
names.splice(index, 1);
|
|
835
|
+
} else if (!entry.startsWith("!") && index === -1) {
|
|
836
|
+
names.push(entry);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return names;
|
|
840
|
+
}
|
|
819
841
|
|
|
820
842
|
// src/preset/preset-effect.ts
|
|
821
843
|
var presetEffectQueryWrites = Object.fromEntries(presetQueryWrites.map((name) => [name, "A queries file only reads; write in an actions file."]));
|
|
@@ -823,8 +845,11 @@ var presetEffectWorkflowCalls = Object.fromEntries(["selectFrom", ...presetQuery
|
|
|
823
845
|
var presetEffectFrameworks = Object.fromEntries(["@tanstack/*", "react-dom", "react-dom/**", "next", "next/**", "vite"].map((name) => [name, "The server imports no web framework."]));
|
|
824
846
|
var presetEffectRouters = ["@tanstack/react-router", "next/link", "next/navigation", "next/router", "expo-router"];
|
|
825
847
|
var presetEffectErrors = ["Schema.TaggedError", "Schema.ErrorClass", "Data.TaggedError", "Data.Error"];
|
|
826
|
-
var
|
|
827
|
-
var
|
|
848
|
+
var presetEffectContractKinds = ["*.schemas.ts", "*.admin.schemas.ts", "*.contract.ts", "*.admin.contract.ts", "*.errors.ts", "*.layer.types.ts", "*.service.ts"];
|
|
849
|
+
var presetEffectSpans = [
|
|
850
|
+
"error",
|
|
851
|
+
{ labels: [{ callee: "Effect.fn", name: "{name}", drop: ["Action", "Query"], loose: true }] }
|
|
852
|
+
];
|
|
828
853
|
var presetEffectSchemaUses = {
|
|
829
854
|
namespaces: ["Schema"],
|
|
830
855
|
methods: ["Array", "Union", "Record", "Tuple"],
|
|
@@ -897,17 +922,18 @@ function presetEffectServerKeys(at) {
|
|
|
897
922
|
},
|
|
898
923
|
[at("domains/*/*.actions.ts")]: {
|
|
899
924
|
limits: { lines: 1000 },
|
|
900
|
-
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Action[A-Z]\\w*"], types: true }] }
|
|
925
|
+
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Action[A-Z]\\w*"], types: true }], "arch/naming-labels": presetEffectSpans }
|
|
901
926
|
},
|
|
902
927
|
[at("domains/*/*.queries.ts")]: {
|
|
903
928
|
limits: { lines: 1000 },
|
|
904
929
|
deny: { calls: presetEffectQueryWrites, imports: { "./*.actions{,.ts}": "A queries file imports no actions." } },
|
|
905
|
-
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Query[A-Z]\\w*"], types: true }] }
|
|
930
|
+
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Query[A-Z]\\w*"], types: true }], "arch/naming-labels": presetEffectSpans }
|
|
906
931
|
},
|
|
907
932
|
[at("domains/*/*.constants.ts")]: { rules: { "arch/files-no-functions": ["error", { reexports: false }] } },
|
|
908
933
|
[at("domains/*/*.layer.ts")]: { rules: { "arch/effect-layer-file": ["error", { suffix: "Layer" }] } },
|
|
909
934
|
[at("domains/*/*.driver.ts")]: { rules: { "arch/effect-layer-file": ["error", { suffix: "Layer" }] } },
|
|
910
935
|
[at("domains/*/*.workflows.ts")]: {
|
|
936
|
+
rules: { "arch/naming-labels": ["error", { labels: [{ callee: "Workflow.make", name: "{name}", drop: "Workflow$" }] }] },
|
|
911
937
|
deny: {
|
|
912
938
|
calls: presetEffectWorkflowCalls,
|
|
913
939
|
imports: {
|
|
@@ -926,7 +952,14 @@ function presetEffectServerKeys(at) {
|
|
|
926
952
|
[at("rpc")]: { rules: { "arch/code-delegate": "error" } }
|
|
927
953
|
};
|
|
928
954
|
}
|
|
929
|
-
function presetEffectContractKeys(at) {
|
|
955
|
+
function presetEffectContractKeys(at, files) {
|
|
956
|
+
const kinds = presetNames(presetEffectContractKinds, files ?? [], "effect.files");
|
|
957
|
+
if (kinds.some((kind) => !/^\*\.[a-z0-9.-]+\.ts$/.test(kind))) {
|
|
958
|
+
throw new PresetError("effect.files takes contract file kinds such as `*.service.ts`.");
|
|
959
|
+
}
|
|
960
|
+
const pattern = `^[a-z0-9]+(?:-[a-z0-9]+)*(?:${kinds.map((kind) => kind.slice(1, -3).replaceAll(".", "[.]")).join("|")})[.]ts$`;
|
|
961
|
+
const listed = kinds.flatMap((kind) => kind === "*.schemas.ts" ? ["<d>.schemas.ts", "<d>-<part>.schemas.ts"] : [`<d>${kind.slice(1)}`]);
|
|
962
|
+
const message = `Contract files use ${listed.slice(0, -1).join(", ")} or ${listed.at(-1)}.`;
|
|
930
963
|
const schemas = {
|
|
931
964
|
rules: {
|
|
932
965
|
"arch/files-constants-only": ["error", { types: true, functions: true }],
|
|
@@ -939,7 +972,7 @@ function presetEffectContractKeys(at) {
|
|
|
939
972
|
return {
|
|
940
973
|
[at("*/*.ts")]: {
|
|
941
974
|
rules: {
|
|
942
|
-
"arch/naming-file": ["error", { pattern
|
|
975
|
+
"arch/naming-file": ["error", { pattern, message }],
|
|
943
976
|
"arch/naming-folder": ["error", { singularize: "none", separators: [".", "-"] }],
|
|
944
977
|
"arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["{Domain}\\w*"] }]
|
|
945
978
|
}
|
|
@@ -949,6 +982,7 @@ function presetEffectContractKeys(at) {
|
|
|
949
982
|
[at("*/*.contract.ts")]: schemas,
|
|
950
983
|
[at("*/*.errors.ts")]: noFunctions,
|
|
951
984
|
[at("*/*.layer.types.ts")]: noFunctions,
|
|
985
|
+
[at("*/*.service.ts")]: noFunctions,
|
|
952
986
|
[at("shared/*.ts")]: {
|
|
953
987
|
rules: {
|
|
954
988
|
"arch/naming-file": "off",
|
|
@@ -967,17 +1001,113 @@ function presetEffectContractKeys(at) {
|
|
|
967
1001
|
}
|
|
968
1002
|
};
|
|
969
1003
|
}
|
|
970
|
-
function
|
|
1004
|
+
function presetEffectTop(allow) {
|
|
1005
|
+
return { top: { allow, stem: "folder" } };
|
|
1006
|
+
}
|
|
1007
|
+
function presetEffectStrictServer(at, service) {
|
|
1008
|
+
const operations = presetEffectTop([{ kind: "const", exported: true, from: "^Effect\\.fn" }, { kind: "const", names: "^decode[A-Z]" }]);
|
|
1009
|
+
const layerKeys = service === undefined ? {} : { "arch/effect-layer-keys": ["error", { service }] };
|
|
1010
|
+
return {
|
|
1011
|
+
[at(".")]: {
|
|
1012
|
+
rules: {
|
|
1013
|
+
"arch/effect-yield-name": "error",
|
|
1014
|
+
"arch/effect-yields-first": "error",
|
|
1015
|
+
"arch/code-inside": ["error", { inside: [{ callee: "*.transaction", calls: { "*Workflow.execute": "Start the workflow after the commit." } }] }]
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
[at("domains")]: { naming: { imports: { "./{file}.ts": "{File}" } } },
|
|
1019
|
+
[at("domains/*/*.actions.ts")]: operations,
|
|
1020
|
+
[at("domains/*/*.queries.ts")]: operations,
|
|
1021
|
+
[at("domains/*/*.utils.ts")]: {
|
|
1022
|
+
...operations,
|
|
1023
|
+
rules: { "arch/naming-labels": ["error", { labels: [{ callee: "Effect.fn", name: "{name}", loose: true }] }] }
|
|
1024
|
+
},
|
|
1025
|
+
[at("domains/*/*.layer.ts")]: {
|
|
1026
|
+
rules: { "arch/naming-keys": ["error", { callees: ["*.bind", "*.of"], name: "^{folder}(?:Action|Query){Path}$", inline: false }], ...layerKeys }
|
|
1027
|
+
},
|
|
1028
|
+
[at("domains/*/*.workflows.ts")]: {
|
|
1029
|
+
...presetEffectTop([{ kind: "const", from: "(?:^Workflow\\.make|\\.toLayer|^Layer\\.\\w+|^Schedule\\.\\w+)$" }]),
|
|
1030
|
+
rules: {
|
|
1031
|
+
"arch/files-exports": ["error", { stem: "folder", patterns: ["{Domain}\\w*Workflow", "{Domain}WorkflowsLayer", "{domain}Schedule[A-Z]\\w*"] }],
|
|
1032
|
+
"arch/naming-labels": [
|
|
1033
|
+
"error",
|
|
1034
|
+
{
|
|
1035
|
+
labels: [
|
|
1036
|
+
{ callee: "Workflow.make", name: "{name}", drop: "Workflow$" },
|
|
1037
|
+
{ callee: "Effect.fn", name: "{name}", within: "*.toLayer", loose: true }
|
|
1038
|
+
]
|
|
1039
|
+
}
|
|
1040
|
+
]
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
[at("domains/*/*.constants.ts")]: presetEffectTop([{ kind: "const", exported: true, names: "^{DOMAIN}_" }]),
|
|
1044
|
+
[at("domains/*/*.config.ts")]: presetEffectTop([{ kind: "const", exported: true, names: "^{domain}\\w*Config$" }]),
|
|
1045
|
+
[at("domains/*/*.prompts.ts")]: { rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Prompt[A-Z]\\w*"], types: true }] } },
|
|
1046
|
+
[at("rpc/*/*.ts")]: {
|
|
1047
|
+
...presetEffectTop([{ kind: "const", exported: true }]),
|
|
1048
|
+
rules: {
|
|
1049
|
+
"arch/naming-file": ["error", { template: "{group}/rpc.{group}.{area}.ts", message: "Handlers use rpc/<group>/rpc.<group>.<area>.ts." }],
|
|
1050
|
+
"arch/files-exports": ["error", { file: "rpc.{group}.{area}.ts", names: ["rpc{Group}{Area}"] }],
|
|
1051
|
+
"arch/naming-keys": ["error", { exports: "^rpc[A-Z]", delegate: true }]
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
[at("mails/*.mail.tsx")]: { rules: { "arch/files-exports": ["error", { file: "{name}.mail.tsx", names: ["{name}Mail", "{Name}Email"], types: true }] } }
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function presetEffectStrictContract(at, name, files) {
|
|
1058
|
+
const key = name === undefined ? [] : [{ callee: "Context.Service", name: `${name}/{folder}/{file}/{name}` }];
|
|
1059
|
+
const labels = key.length > 0 ? { "arch/naming-labels": ["error", { labels: key }] } : {};
|
|
1060
|
+
const service = presetNames(presetEffectContractKinds, files ?? [], "effect.files").includes("*.service.ts");
|
|
1061
|
+
const sibling = service ? { "arch/files-siblings": ["error", { require: "{folder}.service.ts", message: "A domain is reached through its service." }] } : {};
|
|
1062
|
+
return {
|
|
1063
|
+
[at("*/*.schemas.ts")]: presetEffectTop(["type", "interface", "namespace", { kind: "const", exported: true }]),
|
|
1064
|
+
[at("*/*.contract.ts")]: {
|
|
1065
|
+
...presetEffectTop(["type", { kind: "const", exported: true }, { kind: "const", exported: false, from: "^[A-Z]\\w*\\." }]),
|
|
1066
|
+
deny: { calls: { "Schema.Struct": "Name the Schema in <d>.schemas.ts." } },
|
|
1067
|
+
rules: sibling
|
|
1068
|
+
},
|
|
1069
|
+
[at("*/*.service.ts")]: {
|
|
1070
|
+
...presetEffectTop(["interface", "type", "class", "namespace"]),
|
|
1071
|
+
rules: { ...labels, "arch/effect-service-params": "error" }
|
|
1072
|
+
},
|
|
1073
|
+
[at("*/*.errors.ts")]: {
|
|
1074
|
+
...presetEffectTop([{ kind: "class", exported: true }]),
|
|
1075
|
+
rules: {
|
|
1076
|
+
"arch/naming-labels": [
|
|
1077
|
+
"error",
|
|
1078
|
+
{
|
|
1079
|
+
labels: [
|
|
1080
|
+
{ callee: ["Schema.TaggedErrorClass", "Schema.TaggedError", "Data.TaggedError"], name: "{name}" },
|
|
1081
|
+
{ callee: "*.Tagged", property: "tag", name: "{name}" },
|
|
1082
|
+
{ callee: "*.Tagged", property: "code", pattern: "^[A-Z][A-Z0-9_]*$" }
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
]
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
var presetEffectStrictTests = { top: { allow: ["describe*", "it*", "test*", "layer*"] } };
|
|
1091
|
+
function presetEffectUnit(root, key, value, contract) {
|
|
971
1092
|
if (!value.effect) {
|
|
972
1093
|
return {};
|
|
973
1094
|
}
|
|
974
1095
|
const at = presetEffectAt(root, key, value);
|
|
975
|
-
const
|
|
1096
|
+
const own = presetEffectContract(root, key, value);
|
|
976
1097
|
const server = presetEffectDomains(root, key, value);
|
|
977
|
-
const
|
|
1098
|
+
const options = value.effect instanceof Object ? value.effect : {};
|
|
1099
|
+
const service = contract && `${posix2.join(root, contract.source)}/{folder}/{folder}.service.ts`;
|
|
1100
|
+
const base = server ? presetEffectServerKeys(at) : presetEffectContractKeys(at, options.files);
|
|
1101
|
+
const tests = { "tests/**/*.test.ts": presetEffectStrictTests, "test/**/*.test.ts": presetEffectStrictTests };
|
|
1102
|
+
const strict = server ? { ...presetEffectStrictServer(at, service), ...tests } : presetEffectStrictContract(at, presetManifest(root, key).name, options.files);
|
|
1103
|
+
const locked = options.strict === false ? {} : strict;
|
|
1104
|
+
const paths = [...new Set([...Object.keys(base), ...Object.keys(locked)])];
|
|
1105
|
+
const merged = (path) => (path in base) && (path in locked) ? presetMerge(base[path], locked[path]) : base[path] ?? locked[path];
|
|
1106
|
+
const conventions = Object.fromEntries(paths.map((path) => [path, merged(path)]));
|
|
1107
|
+
const keys = server || own ? presetFilled(conventions, presetEffectExists(root, key)) : {};
|
|
978
1108
|
const errors = Object.fromEntries(presetEffectErrors.map((name) => [name, "Tagged errors live in <d>.errors.ts."]));
|
|
979
|
-
const placement = server ||
|
|
980
|
-
const imports = server ||
|
|
1109
|
+
const placement = server || own ? { types: { placement: false } } : {};
|
|
1110
|
+
const imports = server || own ? { ...presetEffectImports, ...presetEffectServerImports } : presetEffectImports;
|
|
981
1111
|
return {
|
|
982
1112
|
...keys,
|
|
983
1113
|
".": presetMerge({ ...placement, deny: { imports, calls: errors } }, keys["."] ?? {}),
|
|
@@ -1037,9 +1167,13 @@ function presetEffectKit(root, key, value, units) {
|
|
|
1037
1167
|
return { ".": { deny: { imports: Object.fromEntries(names.flatMap((name) => [[name, message], [`${name}/**`, message]])) } } };
|
|
1038
1168
|
}
|
|
1039
1169
|
function presetEffectConventions(root, key, value, units) {
|
|
1040
|
-
const
|
|
1170
|
+
const packages = units.flatMap(([other, folder]) => {
|
|
1171
|
+
const name = presetEffectContract(root, other, folder) ? presetManifest(root, other).name : undefined;
|
|
1172
|
+
return name === undefined ? [] : [{ name, source: presetSource(root, other, folder instanceof Object ? folder.source : undefined) }];
|
|
1173
|
+
});
|
|
1174
|
+
const contracts = packages.map((item) => item.name);
|
|
1041
1175
|
return [
|
|
1042
|
-
...Object.entries(presetEffectUnit(root, key, value)),
|
|
1176
|
+
...Object.entries(presetEffectUnit(root, key, value, packages.at(0))),
|
|
1043
1177
|
...Object.entries(presetEffectFrontend(root, key, value)),
|
|
1044
1178
|
...Object.entries(presetEffectWeb(root, key, value, contracts)),
|
|
1045
1179
|
...Object.entries(presetEffectKit(root, key, value, units))
|
|
@@ -1444,8 +1578,9 @@ var denyGroup = (project, settings) => {
|
|
|
1444
1578
|
const imports = kept(settings.imports);
|
|
1445
1579
|
const calls = kept(settings.calls);
|
|
1446
1580
|
const overrides = [];
|
|
1447
|
-
|
|
1448
|
-
|
|
1581
|
+
const only = settings.only.length > 0 ? { only: settings.only } : {};
|
|
1582
|
+
if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0 || settings.only.length > 0) {
|
|
1583
|
+
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls, ...only }] }));
|
|
1449
1584
|
}
|
|
1450
1585
|
const reads = Object.fromEntries(presetQueryWrites.map((name) => [name, "Query files only read; move writes to an action."]));
|
|
1451
1586
|
for (const unit of project.units.filter((item) => item.kind === "backend")) {
|
|
@@ -1453,7 +1588,7 @@ var denyGroup = (project, settings) => {
|
|
|
1453
1588
|
...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
|
|
1454
1589
|
...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
|
|
1455
1590
|
];
|
|
1456
|
-
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: kept({ ...reads, ...settings.calls }) }] }));
|
|
1591
|
+
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: kept({ ...reads, ...settings.calls }), ...only }] }));
|
|
1457
1592
|
}
|
|
1458
1593
|
return overrides;
|
|
1459
1594
|
};
|
|
@@ -1869,6 +2004,10 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1869
2004
|
}
|
|
1870
2005
|
}
|
|
1871
2006
|
add("exports", presetServiceScopes(project.serviceFolders, web, "**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", namingFrontendPrefix, { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}", ...testFiles]);
|
|
2007
|
+
const imports = Object.entries(settings.imports).flatMap(([source, name]) => name === false ? [] : [{ source, name }]);
|
|
2008
|
+
if (imports.length > 0) {
|
|
2009
|
+
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-imports": ["error", { imports }] }));
|
|
2010
|
+
}
|
|
1872
2011
|
if (typeof exportsPattern === "string") {
|
|
1873
2012
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-exports": ["error", { mode: "pattern", pattern: exportsPattern, types: true }] }));
|
|
1874
2013
|
}
|
|
@@ -2109,6 +2248,9 @@ function optionsAssertMockingApis(patterns) {
|
|
|
2109
2248
|
function optionsEscape(value) {
|
|
2110
2249
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2111
2250
|
}
|
|
2251
|
+
function optionsWildcards(names) {
|
|
2252
|
+
return [names ?? []].flat().map((name) => new RegExp(`^${name.split("*").map(optionsEscape).join(".*")}$`));
|
|
2253
|
+
}
|
|
2112
2254
|
|
|
2113
2255
|
// src/preset/groups/tests.ts
|
|
2114
2256
|
var testsGroup = (project, settings) => {
|
|
@@ -2155,6 +2297,16 @@ var testsGroup = (project, settings) => {
|
|
|
2155
2297
|
];
|
|
2156
2298
|
};
|
|
2157
2299
|
|
|
2300
|
+
// src/preset/groups/top.ts
|
|
2301
|
+
var topGroup = (_project, { level: _level, allow, ...rest }) => {
|
|
2302
|
+
if (allow === undefined && rest.deny === undefined) {
|
|
2303
|
+
return [];
|
|
2304
|
+
}
|
|
2305
|
+
const listed = allow === undefined || allow.includes("import") ? allow : ["import", ...allow];
|
|
2306
|
+
const options = Object.fromEntries(Object.entries({ allow: listed, ...rest }).filter(([, value]) => value !== undefined));
|
|
2307
|
+
return [presetOverride([`**/${presetExtensions}`], { "arch/files-top-level": ["error", options] })];
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2158
2310
|
// src/preset/groups/types.ts
|
|
2159
2311
|
var typesGroup = (project, settings) => {
|
|
2160
2312
|
const { web, backend, packageSources, app, appFiles, services, utilities, types, ui, testFiles, components, hooks } = project;
|
|
@@ -2215,9 +2367,69 @@ var typesGroup = (project, settings) => {
|
|
|
2215
2367
|
import { existsSync as existsSync5, readdirSync as readdirSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
2216
2368
|
import { posix as posix8, resolve as resolve8 } from "node:path";
|
|
2217
2369
|
|
|
2370
|
+
// src/preset/preset-sets.ts
|
|
2371
|
+
var presetSetsReserved = ["domain", "stem", "file", "folder", "parent", "name", "key", "value"];
|
|
2372
|
+
function presetSets(folders, sets = {}) {
|
|
2373
|
+
const names = Object.keys(sets);
|
|
2374
|
+
const reserved = names.find((name) => presetSetsReserved.includes(name) || !/^[a-z][A-Za-z0-9]*$/.test(name));
|
|
2375
|
+
if (reserved) {
|
|
2376
|
+
throw new PresetError(`Set \`${reserved}\` needs another name: start with a lowercase letter, use letters and digits, and avoid ${presetSetsReserved.join(", ")}.`);
|
|
2377
|
+
}
|
|
2378
|
+
if (names.length === 0) {
|
|
2379
|
+
return folders;
|
|
2380
|
+
}
|
|
2381
|
+
const token = new RegExp(`\\{(${names.join("|")})\\}`);
|
|
2382
|
+
const pairs = (name) => {
|
|
2383
|
+
if (!Object.hasOwn(sets, name)) {
|
|
2384
|
+
throw new PresetError(`Unknown set \`${name}\`.`);
|
|
2385
|
+
}
|
|
2386
|
+
const set = sets[name];
|
|
2387
|
+
return Array.isArray(set) ? set.map((item) => [item, item]) : Object.entries(set);
|
|
2388
|
+
};
|
|
2389
|
+
const text = (value, key, item) => value.replaceAll("{key}", key).replaceAll("{value}", item);
|
|
2390
|
+
const fill = (value, key, item) => {
|
|
2391
|
+
if (typeof value === "string") {
|
|
2392
|
+
return text(value, key, item);
|
|
2393
|
+
}
|
|
2394
|
+
if (Array.isArray(value)) {
|
|
2395
|
+
return value.map((entry) => fill(entry, key, item));
|
|
2396
|
+
}
|
|
2397
|
+
return value instanceof Object ? Object.fromEntries(Object.entries(value).map(([name, entry]) => [text(name, key, item), fill(entry, key, item)])) : value;
|
|
2398
|
+
};
|
|
2399
|
+
const spread = (value) => {
|
|
2400
|
+
const match = token.exec(value);
|
|
2401
|
+
return match ? pairs(match[1]).flatMap(([key]) => spread(value.replace(match[0], key))) : [value];
|
|
2402
|
+
};
|
|
2403
|
+
const expandEntries = (value) => Object.entries(value).flatMap(([name, entry]) => {
|
|
2404
|
+
const match = token.exec(name);
|
|
2405
|
+
return match ? pairs(match[1]).flatMap(([key, item]) => expandEntries({ [name.replace(match[0], key)]: fill(entry, key, item) })) : [[name, name === "message" ? entry : expand(entry)]];
|
|
2406
|
+
});
|
|
2407
|
+
const expand = (value) => {
|
|
2408
|
+
if (typeof value === "string") {
|
|
2409
|
+
return token.test(value) ? spread(value) : value;
|
|
2410
|
+
}
|
|
2411
|
+
if (!Array.isArray(value)) {
|
|
2412
|
+
const marks = value instanceof Object ? Object.getOwnPropertySymbols(value).map((mark) => [mark, Reflect.get(value, mark)]) : [];
|
|
2413
|
+
return value instanceof Object ? Object.assign(Object.fromEntries(expandEntries(value)), Object.fromEntries(marks)) : value;
|
|
2414
|
+
}
|
|
2415
|
+
return value.flatMap((entry) => {
|
|
2416
|
+
if (typeof entry === "string") {
|
|
2417
|
+
return spread(entry);
|
|
2418
|
+
}
|
|
2419
|
+
if (!(entry instanceof Object) || Array.isArray(entry) || typeof entry.each !== "string") {
|
|
2420
|
+
return [expand(entry)];
|
|
2421
|
+
}
|
|
2422
|
+
const { each, ...rest } = entry;
|
|
2423
|
+
return pairs(each).map(([key, item]) => expand(fill(rest, key, item)));
|
|
2424
|
+
});
|
|
2425
|
+
};
|
|
2426
|
+
return expand(folders);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2218
2429
|
// src/preset/preset-stylesheet.ts
|
|
2219
2430
|
import { readdirSync as readdirSync3, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
2220
2431
|
import { posix as posix6, resolve as resolve6 } from "node:path";
|
|
2432
|
+
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
2221
2433
|
function presetTailwindEntry(css) {
|
|
2222
2434
|
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
2223
2435
|
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
@@ -2267,7 +2479,7 @@ function presetUnitSource(root, key, kind, source) {
|
|
|
2267
2479
|
}
|
|
2268
2480
|
return kind === "console" || !existsSync4(resolve7(root, key, "src")) ? key : posix7.join(key, "src");
|
|
2269
2481
|
}
|
|
2270
|
-
function
|
|
2482
|
+
function presetSets2(folder, name) {
|
|
2271
2483
|
return folder[name] !== undefined && folder[name] !== false;
|
|
2272
2484
|
}
|
|
2273
2485
|
function presetKind(key, enabled) {
|
|
@@ -2303,8 +2515,8 @@ function presetUnit(root, key, scope) {
|
|
|
2303
2515
|
const { effective } = scope;
|
|
2304
2516
|
const wildcard = scope.parent && scope.parent.depth === scope.depth && scope.parent.key.includes("*") ? scope.parent.folder : {};
|
|
2305
2517
|
const folder = { ...wildcard, ...scope.folder };
|
|
2306
|
-
const own = presetKindGroups.some((name) =>
|
|
2307
|
-
const { kind, framework } = presetKind(key, (name) => own ?
|
|
2518
|
+
const own = presetKindGroups.some((name) => presetSets2(folder, name)) || presetHasManifest(root, key);
|
|
2519
|
+
const { kind, framework } = presetKind(key, (name) => own ? presetSets2(folder, name) : Boolean(effective[name]));
|
|
2308
2520
|
const layout = {};
|
|
2309
2521
|
const settings = effective.layout;
|
|
2310
2522
|
if (settings) {
|
|
@@ -2327,7 +2539,7 @@ function presetUnits(root, scopes, ignored) {
|
|
|
2327
2539
|
const candidates = [];
|
|
2328
2540
|
for (const scope of scopes) {
|
|
2329
2541
|
const manifest = presetHasManifest(root, scope.key);
|
|
2330
|
-
const own = presetKindGroups.some((name) =>
|
|
2542
|
+
const own = presetKindGroups.some((name) => presetSets2(scope.folder, name)) || manifest;
|
|
2331
2543
|
const top = scope.parent?.key === "." && (rootKind === "library" || own);
|
|
2332
2544
|
const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
|
|
2333
2545
|
const workspace = scope.parent !== null && scope.parent.key !== "." && manifest;
|
|
@@ -2434,20 +2646,16 @@ function presetDenyEntries(inherited, changes) {
|
|
|
2434
2646
|
return [key, message];
|
|
2435
2647
|
}));
|
|
2436
2648
|
}
|
|
2437
|
-
function
|
|
2438
|
-
|
|
2439
|
-
|
|
2649
|
+
function presetDenyOnly(inherited, entries = []) {
|
|
2650
|
+
const each = entries.find((entry) => entry.each !== undefined)?.each;
|
|
2651
|
+
if (each !== undefined) {
|
|
2652
|
+
throw new PresetError(`Unknown set \`${each}\`.`);
|
|
2440
2653
|
}
|
|
2441
|
-
const
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
if (entry.startsWith("!") && index !== -1) {
|
|
2445
|
-
names.splice(index, 1);
|
|
2446
|
-
} else if (!entry.startsWith("!") && index === -1) {
|
|
2447
|
-
names.push(entry);
|
|
2448
|
-
}
|
|
2654
|
+
const known = (entry) => ("in" in entry) && (("imports" in entry) || ("calls" in entry)) && Object.keys(entry).every((key) => ["imports", "calls", "in", "message"].includes(key));
|
|
2655
|
+
if (!Array.isArray(entries) || !entries.every((entry) => entry instanceof Object && known(entry))) {
|
|
2656
|
+
throw new PresetError("deny.only takes entries such as `{ imports: [...], in: [...], message }`, with `imports` or `calls` and the places that may use them.");
|
|
2449
2657
|
}
|
|
2450
|
-
return
|
|
2658
|
+
return [...inherited, ...entries];
|
|
2451
2659
|
}
|
|
2452
2660
|
function presetEffective(folder, parent) {
|
|
2453
2661
|
const resolveGroup = (name) => {
|
|
@@ -2461,13 +2669,14 @@ function presetEffective(folder, parent) {
|
|
|
2461
2669
|
effective.deny = {
|
|
2462
2670
|
...effective.deny,
|
|
2463
2671
|
imports: presetDenyEntries(inherited.imports, deny.imports),
|
|
2464
|
-
calls: presetDenyEntries(inherited.calls, deny.calls)
|
|
2672
|
+
calls: presetDenyEntries(inherited.calls, deny.calls),
|
|
2673
|
+
only: presetDenyOnly(inherited.only, deny.only)
|
|
2465
2674
|
};
|
|
2466
2675
|
}
|
|
2467
2676
|
const layout = folder.layout;
|
|
2468
2677
|
if (effective.layout && layout instanceof Object && layout.files !== undefined) {
|
|
2469
2678
|
const inherited = (parent ? parent.layout?.files : null) ?? presetGroupOptions.layout.files;
|
|
2470
|
-
effective.layout = { ...effective.layout, files:
|
|
2679
|
+
effective.layout = { ...effective.layout, files: presetNames(inherited, layout.files, "layout.files") };
|
|
2471
2680
|
}
|
|
2472
2681
|
return effective;
|
|
2473
2682
|
}
|
|
@@ -2488,7 +2697,7 @@ function presetFolder(key, value) {
|
|
|
2488
2697
|
if (folder.severity !== undefined && !["error", "warn"].includes(folder.severity)) {
|
|
2489
2698
|
throw new PresetError(`Folder \`${key}\` severity must be 'error' or 'warn'.`);
|
|
2490
2699
|
}
|
|
2491
|
-
return folder;
|
|
2700
|
+
return Array.isArray(folder.top) ? { ...folder, top: { allow: folder.top } } : folder;
|
|
2492
2701
|
}
|
|
2493
2702
|
function presetScopesTree(root, folders) {
|
|
2494
2703
|
const entries = Object.entries(folders).map(([key, value]) => [presetNormalizeKey(key), value]);
|
|
@@ -2533,7 +2742,7 @@ function presetScopesTree(root, folders) {
|
|
|
2533
2742
|
function presetProject(config) {
|
|
2534
2743
|
const given = resolve8(config.root ?? process.cwd());
|
|
2535
2744
|
const root = existsSync5(given) ? realpathSync2(given) : given;
|
|
2536
|
-
const expanded = Object.entries(config.folders ?? {}).flatMap(([key, value]) => presetBraces(key).map((path) => [presetPortableKey(path), value, key.includes("{")]));
|
|
2745
|
+
const expanded = Object.entries(presetSets(config.folders ?? {}, config.sets)).flatMap(([key, value]) => presetBraces(key).map((path) => [presetPortableKey(path), value, key.includes("{")]));
|
|
2537
2746
|
const files = expanded.filter(([key]) => presetIsFileKey(root, key));
|
|
2538
2747
|
const listed = expanded.filter((entry) => !files.includes(entry)).map(([key, value, fromList]) => [presetNormalizeKey(key), value, fromList]);
|
|
2539
2748
|
const twice = listed.find(([key, , fromList], index) => listed.some(([other, , otherFromList], at) => at < index && other === key && !fromList && !otherFromList));
|
|
@@ -2742,6 +2951,7 @@ var presetBuilders = {
|
|
|
2742
2951
|
shadcn: shadcnGroup,
|
|
2743
2952
|
limits: limitsGroup,
|
|
2744
2953
|
deny: denyGroup,
|
|
2954
|
+
top: topGroup,
|
|
2745
2955
|
externals: externalsGroup
|
|
2746
2956
|
};
|
|
2747
2957
|
function presetLevel(scope) {
|
|
@@ -21,3 +21,4 @@ export declare function presetIsFile(path: string): boolean;
|
|
|
21
21
|
export declare function presetSource(root: string, directory: string, configured?: string): string;
|
|
22
22
|
export declare function presetFilled(conventions: PresetTypes.PresetFolders, exists: (path: string) => boolean): PresetTypes.PresetFolders;
|
|
23
23
|
export declare function presetMerge(base: boolean | PresetTypes.PresetFolder | undefined, top: boolean | PresetTypes.PresetFolder): boolean | PresetTypes.PresetFolder;
|
|
24
|
+
export declare function presetNames(inherited: string[], entries: unknown, option: string): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const codeInside: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const effectLayerKeys: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const effectServiceParams: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const effectYieldName: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const effectYieldsFirst: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const filesSiblings: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const filesTopLevel: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const namingImports: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const namingKeys: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const namingLabels: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const namingPairs: import("@oxlint/plugins").Rule;
|
|
@@ -99,7 +99,14 @@ export interface BoundariesRpcDatabaseOptions {
|
|
|
99
99
|
message?: string;
|
|
100
100
|
methods?: string[];
|
|
101
101
|
}
|
|
102
|
+
export interface BoundariesDenyOnly {
|
|
103
|
+
imports?: string | string[];
|
|
104
|
+
calls?: string | string[];
|
|
105
|
+
in: string | string[];
|
|
106
|
+
message?: string;
|
|
107
|
+
}
|
|
102
108
|
export interface BoundariesDenyOptions {
|
|
103
109
|
imports?: Record<string, string>;
|
|
104
110
|
calls?: Record<string, string>;
|
|
111
|
+
only?: BoundariesDenyOnly[];
|
|
105
112
|
}
|
|
@@ -89,3 +89,14 @@ export interface CodeTracedOptions {
|
|
|
89
89
|
skip?: OptionsPattern;
|
|
90
90
|
message?: string;
|
|
91
91
|
}
|
|
92
|
+
export interface CodeInsideEntry {
|
|
93
|
+
callee: OptionsPattern;
|
|
94
|
+
calls: Record<string, string>;
|
|
95
|
+
}
|
|
96
|
+
export interface CodeInsideOptions {
|
|
97
|
+
inside: CodeInsideEntry[];
|
|
98
|
+
}
|
|
99
|
+
export interface CodeInsideCompiled {
|
|
100
|
+
callees: RegExp[];
|
|
101
|
+
calls: [RegExp, string][];
|
|
102
|
+
}
|
|
@@ -40,8 +40,28 @@ export interface EffectSchemaFirstDerived {
|
|
|
40
40
|
export interface EffectSchemaFirstOptions {
|
|
41
41
|
member?: string;
|
|
42
42
|
keep?: OptionsPattern;
|
|
43
|
+
allow?: OptionsPattern;
|
|
43
44
|
runtime?: string[];
|
|
44
45
|
rows?: EffectSchemaFirstRows;
|
|
45
46
|
order?: (EffectSchemaFirstSection | EffectSchemaFirstSection[])[];
|
|
46
47
|
derived?: EffectSchemaFirstDerived;
|
|
47
48
|
}
|
|
49
|
+
export interface EffectYieldNameOptions {
|
|
50
|
+
names?: Record<string, string | string[]>;
|
|
51
|
+
allow?: OptionsPattern;
|
|
52
|
+
}
|
|
53
|
+
export interface EffectLayerKeysOptions {
|
|
54
|
+
service: string;
|
|
55
|
+
callees?: string[];
|
|
56
|
+
declaration?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface EffectLayerKeysFrame {
|
|
59
|
+
open: string;
|
|
60
|
+
key?: string;
|
|
61
|
+
members: number;
|
|
62
|
+
}
|
|
63
|
+
export interface EffectServiceParamsOptions {
|
|
64
|
+
name?: OptionsPattern;
|
|
65
|
+
allow?: OptionsPattern;
|
|
66
|
+
declaration?: string;
|
|
67
|
+
}
|