oxlint-plugin-arch 0.6.3 → 0.8.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.
@@ -1,4 +1,4 @@
1
- import type { PresetRules, PresetPolicyName } from '../types/preset.types.ts';
1
+ import type { PresetRules, PresetPolicyName, PresetTestMockingException, PresetTestMockingPreset } from '../types/preset.types.ts';
2
2
  export declare const presetExtensions = "*.{ts,tsx,mts,cts}";
3
3
  export declare const presetPolicyOrder: PresetPolicyName[];
4
4
  export declare const presetCommonRules: PresetRules;
@@ -18,6 +18,16 @@ export declare const presetTailwindRules: {
18
18
  export declare const presetImportRules: PresetRules;
19
19
  export declare const presetNamingRules: PresetRules;
20
20
  export declare const presetTestRules: PresetRules;
21
+ /** Test-double APIs each `tests.mocking` preset bans, for `vi` and `jest`. */
22
+ export declare const presetTestMockingApis: Record<PresetTestMockingPreset, string[]>;
23
+ export declare const presetTestMockingAllow: Record<PresetTestMockingPreset, PresetTestMockingException[]>;
24
+ export declare const presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
25
+ /** HTTP methods a Next `route.ts` handler may export. */
26
+ export declare const presetNextHandlerMethods: string[];
27
+ /** Next route segment config exports, shared by pages, layouts and route handlers. */
28
+ export declare const presetNextSegmentConfig: string[];
29
+ /** Exports a Next page or layout may add next to its default component. */
30
+ export declare const presetNextSegmentExports: string[];
21
31
  export declare const presetExportPrefix: {
22
32
  stem: string;
23
33
  trailingRoles: string[];
package/dist/index.js CHANGED
@@ -749,6 +749,11 @@ function layoutNewline(text) {
749
749
  ` : `
750
750
  `;
751
751
  }
752
+ // src/utils/helpers/mocking.ts
753
+ function mockingApiPatternsAssert(patterns) {
754
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
755
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
756
+ }
752
757
  // src/utils/helpers/options.ts
753
758
  function optionsFirst(context, fallback) {
754
759
  return context.options[0] ?? fallback;
@@ -894,6 +899,49 @@ function routesInspectFunction(context, item, bannedHooks, banIntrinsicJsx) {
894
899
  }
895
900
  });
896
901
  }
902
+ // src/utils/helpers/runtime.ts
903
+ import { builtinModules } from "node:module";
904
+ var runtimeServerGlobals = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
905
+ var runtimeBrowserGlobals = [
906
+ "window",
907
+ "document",
908
+ "navigator",
909
+ "localStorage",
910
+ "sessionStorage",
911
+ "location",
912
+ "history",
913
+ "screen",
914
+ "HTMLElement",
915
+ "Element",
916
+ "HTMLInputElement",
917
+ "ResizeObserver",
918
+ "MutationObserver",
919
+ "IntersectionObserver",
920
+ "FileReader",
921
+ "matchMedia",
922
+ "getComputedStyle",
923
+ "requestAnimationFrame",
924
+ "cancelAnimationFrame",
925
+ "alert",
926
+ "confirm",
927
+ "prompt"
928
+ ];
929
+ function runtimeMatchesImport(values, source) {
930
+ return Boolean(values?.some((value) => source === value || source.startsWith(value + "/")));
931
+ }
932
+ function runtimeIsBackendImport(options, target, source) {
933
+ return options.backend.some((path) => target.startsWith(path + "/")) || options.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
934
+ }
935
+ function runtimeIsServerPlatformImport(source) {
936
+ return builtinModules.includes(source) || source === "@orpc/server" || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source);
937
+ }
938
+ function runtimeMemberName(node) {
939
+ if (node?.type !== "MemberExpression")
940
+ return null;
941
+ if (!node.computed && node.property.type === "Identifier")
942
+ return node.property.name;
943
+ return node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
944
+ }
897
945
  // src/utils/helpers/sanitizers.ts
898
946
  function sanitizersCallsConfigured(node, names) {
899
947
  let found = false;
@@ -2378,8 +2426,10 @@ var noFileLevelHelpers = defineRule16({
2378
2426
  } = optionsFirst(context, {});
2379
2427
  const allowed = optionsOptionalPatterns(allowPattern);
2380
2428
  const hooks = optionsPatterns(hookPattern);
2429
+ const exported = exportsCollect(program);
2430
+ const allowedExport = (name) => exported.some((binding) => binding.localName === name && optionsPatternsTest(allowed, binding.exportedName));
2381
2431
  for (const candidate of declarationsFileLevelFunctionCandidates(program)) {
2382
- if (detectComponents && reactComponentsIsLike(program, candidate) || optionsPatternsTest(hooks, candidate.name) || optionsPatternsTest(allowed, candidate.name)) {
2432
+ if (detectComponents && reactComponentsIsLike(program, candidate) || optionsPatternsTest(hooks, candidate.name) || optionsPatternsTest(allowed, candidate.name) || allowedExport(candidate.name)) {
2383
2433
  continue;
2384
2434
  }
2385
2435
  context.report({
@@ -4271,6 +4321,7 @@ var onlyExportComponents = defineRule36({
4271
4321
  additionalProperties: false,
4272
4322
  properties: {
4273
4323
  matchFileName: { type: "boolean" },
4324
+ trailingRoles: { type: "array", items: { type: "string" } },
4274
4325
  allowTypeExports: { type: "boolean" },
4275
4326
  denyTypePattern: optionsPatternSchema
4276
4327
  }
@@ -4288,10 +4339,15 @@ var onlyExportComponents = defineRule36({
4288
4339
  Program(program) {
4289
4340
  const {
4290
4341
  matchFileName = true,
4342
+ trailingRoles = [],
4291
4343
  allowTypeExports = true,
4292
4344
  denyTypePattern
4293
4345
  } = optionsFirst(context, {});
4294
- const expected = reactComponentsPascalFromBasename(namingFileBasename(context.filename));
4346
+ const basename = namingFileBasename(context.filename);
4347
+ const extension = basename.slice(basename.lastIndexOf("."));
4348
+ const role = trailingRoles.find((candidate) => basename.endsWith(`.${candidate}${extension}`));
4349
+ const expected = reactComponentsPascalFromBasename(role ? basename.replace(`.${role}${extension}`, extension) : basename);
4350
+ const expectedKeys = [expected, ...role ? [reactComponentsPascalFromBasename(basename)] : []].map((name) => name.toLowerCase());
4295
4351
  const denied = optionsOptionalPatterns(denyTypePattern);
4296
4352
  const components = [];
4297
4353
  for (const candidate of reactComponentsExportCandidates(program)) {
@@ -4324,10 +4380,9 @@ var onlyExportComponents = defineRule36({
4324
4380
  if (components.length === 0 || !matchFileName) {
4325
4381
  return;
4326
4382
  }
4327
- const expectedKey = expected.toLowerCase();
4328
- const exact = components.some((candidate) => candidate.name.toLowerCase() === expectedKey);
4329
- const allPrefixed = components.every((candidate) => candidate.name.toLowerCase().startsWith(expectedKey));
4330
- if (!exact && !allPrefixed) {
4383
+ const names = components.map((candidate) => candidate.name.toLowerCase());
4384
+ const matches = expectedKeys.some((expectedKey) => names.includes(expectedKey) || names.every((name) => name.startsWith(expectedKey)));
4385
+ if (!matches) {
4331
4386
  for (const candidate of components) {
4332
4387
  context.report({
4333
4388
  node: candidate.node,
@@ -4994,13 +5049,21 @@ var routeSurface = defineRule44({
4994
5049
  additionalProperties: false,
4995
5050
  properties: {
4996
5051
  exportName: { type: "string" },
5052
+ allowedExports: { type: "array", items: { type: "string" } },
5053
+ inspectExports: { type: "boolean" },
4997
5054
  bannedHooks: { type: "array", items: { type: "string" } },
4998
5055
  banIntrinsicJsx: { type: "boolean" }
4999
5056
  }
5000
5057
  }
5001
5058
  ],
5002
5059
  defaultOptions: [
5003
- { exportName: "Route", bannedHooks: ["useState", "useEffect", "useMutation"], banIntrinsicJsx: true }
5060
+ {
5061
+ exportName: "Route",
5062
+ allowedExports: [],
5063
+ inspectExports: false,
5064
+ bannedHooks: ["useState", "useEffect", "useMutation"],
5065
+ banIntrinsicJsx: true
5066
+ }
5004
5067
  ],
5005
5068
  messages: {
5006
5069
  extraExport: "Route files may only export '{{expected}}'; found '{{actual}}'.",
@@ -5011,20 +5074,34 @@ var routeSurface = defineRule44({
5011
5074
  createOnce(context) {
5012
5075
  return {
5013
5076
  Program(program) {
5014
- const { exportName, bannedHooks, banIntrinsicJsx } = optionsFirst(context);
5015
- for (const binding of exportsCollect(program)) {
5016
- if (binding.exportedName !== exportName) {
5077
+ const { exportName, allowedExports = [], inspectExports = false, bannedHooks, banIntrinsicJsx } = optionsFirst(context);
5078
+ const allowed = new Set([exportName, ...allowedExports]);
5079
+ const bindings = exportsCollect(program);
5080
+ for (const binding of bindings) {
5081
+ if (!allowed.has(binding.exportedName)) {
5017
5082
  context.report({
5018
5083
  node: binding.node,
5019
5084
  messageId: "extraExport",
5020
- data: { expected: exportName, actual: binding.exportedName }
5085
+ data: { expected: [...allowed].join("', '"), actual: binding.exportedName }
5021
5086
  });
5022
5087
  }
5023
5088
  }
5024
5089
  const banned = new Set(bannedHooks);
5090
+ const inspected = new Set;
5025
5091
  for (const item of declarationsTopLevelUnexportedFunctions(program)) {
5092
+ inspected.add(item.node.start);
5026
5093
  routesInspectFunction(context, item, banned, banIntrinsicJsx);
5027
5094
  }
5095
+ if (!inspectExports)
5096
+ return;
5097
+ const names = new Set(bindings.filter((binding) => binding.exportedName === exportName).map((binding) => binding.localName ?? binding.exportedName));
5098
+ const defaultExport = program.body.find((statement) => statement.type === "ExportDefaultDeclaration");
5099
+ const declaration = defaultExport?.declaration;
5100
+ const defaultFunctions = declaration?.type === "ArrowFunctionExpression" || declaration?.type === "FunctionExpression" ? [{ name: "default", node: declaration }] : [];
5101
+ for (const item of [...exportsCollectFunctions(program), ...defaultFunctions]) {
5102
+ if (names.has(item.name) && !inspected.has(item.node.start))
5103
+ routesInspectFunction(context, item, banned, banIntrinsicJsx);
5104
+ }
5028
5105
  }
5029
5106
  };
5030
5107
  }
@@ -1,2 +1,4 @@
1
+ export { default as reactModulesPreset } from './react-modules-preset.ts';
2
+ export { default as nextReactModulesPreset } from './next-react-modules-preset.ts';
1
3
  export { default as tanstackStartReactModulesPreset } from './tanstack-start-react-modules-preset.ts';
2
- export type { PresetOptions, PresetArchitecture, PresetArchitectureEntry, PresetFolderLayout, PresetTanstackStartOptions, PresetLimitsOptions, PresetModulesOptions, PresetImportsOptions, PresetOrpcOptions, PresetSqlOptions, PresetFormsOptions, PresetReactOptions, PresetTestsOptions, PresetPolicyOptions, PresetPolicy, PresetPolicyName, PresetTailwindOptions, PresetShadcnOptions, } from '../types/preset.types.ts';
4
+ export type { PresetOptions, PresetArchitecture, PresetArchitectureEntry, PresetFolderLayout, PresetFormattingOptions, PresetFramework, PresetNextOptions, PresetTanstackStartOptions, PresetLimitsOptions, PresetModulesOptions, PresetImportsOptions, PresetOrpcOptions, PresetSqlOptions, PresetFormsOptions, PresetReactOptions, PresetTestsOptions, PresetTestMockingApi, PresetTestMockingException, PresetTestMockingOptions, PresetTestMockingPreset, PresetPolicyOptions, PresetPolicy, PresetPolicyName, PresetTailwindOptions, PresetShadcnOptions, } from '../types/preset.types.ts';