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.
@@ -0,0 +1,2 @@
1
+ import type { PresetBuilder } from '../../types/preset.types.ts';
2
+ export declare const topGroup: PresetBuilder<'top'>;
@@ -0,0 +1,2 @@
1
+ import type * as PresetTypes from '../types/preset.types.ts';
2
+ export declare function presetSets(folders: PresetTypes.PresetFolders, sets?: Record<string, PresetTypes.PresetSet>): PresetTypes.PresetFolders;
@@ -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[];
@@ -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 empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
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 presetEffectContractNames = "^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.]schemas|[.]admin[.]schemas|[.]contract|[.]admin[.]contract|[.]errors|[.]layer[.]types)[.]ts$";
827
- var presetEffectContractMessage = "Contract files use <d>.schemas.ts, <d>-<part>.schemas.ts, <d>.admin.schemas.ts, <d>.contract.ts, <d>.admin.contract.ts, <d>.errors.ts or <d>.layer.types.ts.";
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: presetEffectContractNames, message: presetEffectContractMessage }],
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 presetEffectUnit(root, key, value) {
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 contract = presetEffectContract(root, key, value);
1096
+ const own = presetEffectContract(root, key, value);
976
1097
  const server = presetEffectDomains(root, key, value);
977
- const keys = server || contract ? presetFilled(server ? presetEffectServerKeys(at) : presetEffectContractKeys(at), presetEffectExists(root, key)) : {};
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 || contract ? { types: { placement: false } } : {};
980
- const imports = server || contract ? { ...presetEffectImports, ...presetEffectServerImports } : presetEffectImports;
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 contracts = units.flatMap(([other, folder]) => presetEffectContract(root, other, folder) ? presetManifest(root, other).name ?? [] : []);
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
- if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0) {
1448
- overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls }] }));
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 presetSets(folder, name) {
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) => presetSets(folder, name)) || presetHasManifest(root, key);
2307
- const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
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) => presetSets(scope.folder, name)) || manifest;
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 presetLayoutNames(inherited, entries) {
2438
- if (!Array.isArray(entries) || !entries.every((entry) => typeof entry === "string" && presetLayoutFileName.test(entry))) {
2439
- throw new PresetError("layout.files takes file names such as `*.prompts.ts`; `!*.utils.ts` removes one.");
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 names = [...inherited];
2442
- for (const entry of entries) {
2443
- const index = names.indexOf(entry.startsWith("!") ? entry.slice(1) : entry);
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 names;
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: presetLayoutNames(inherited, 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
+ }