@isentinel/eslint-config 6.0.0-beta.42 → 6.0.0-beta.44

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 CHANGED
@@ -890,6 +890,53 @@ otherwise, you can install them manually:
890
890
  pnpm i -D eslint-plugin-eslint-plugin
891
891
  ```
892
892
 
893
+ #### Co-located Tests
894
+
895
+ `projectStructure` requires every source file to sit next to its test file,
896
+ through
897
+ [eslint-plugin-project-structure](https://github.com/Igorkowalski94/eslint-plugin-project-structure).
898
+ `src/place-service.ts` then has to be joined by `src/place-service.spec.ts`.
899
+ Only `enforceExistence` is driven here, so the rule never enforces naming:
900
+
901
+ ```ts
902
+ // eslint.config.ts
903
+ import isentinel from "@isentinel/eslint-config";
904
+
905
+ export default isentinel({
906
+ projectStructure: {
907
+ // The plugin's name placeholders, plus `{ext}` for the extension of the
908
+ // file that matched. Default: ["{node-name}.spec.{ext}"]
909
+ enforceExistence: "__tests__/{node-name}.test.{ext}",
910
+ // Default: every source file the preset lints
911
+ files: ["src/**/*.{ts,tsx}"],
912
+ // Default: tests, `.d.ts` files and build configs
913
+ ignores: ["**/index.ts"],
914
+ // Default: `process.cwd()`
915
+ projectRoot: import.meta.dirname,
916
+ // Restricts the check to one subtree. Default: all of `projectRoot`
917
+ structureRoot: "src",
918
+ },
919
+ });
920
+ ```
921
+
922
+ ```bash
923
+ pnpm i -D eslint-plugin-project-structure
924
+ ```
925
+
926
+ `{node-name}` matches the kebab-case filenames the preset enforces; the others
927
+ are `{nodeName}`, `{NodeName}`, `{node_name}` and `{NODE_NAME}`. A file already
928
+ matching a template is exempt from it. A source file with no test disables the
929
+ rule at the source.
930
+
931
+ The rule needs no type information, so oxlint runs it too: the same option is
932
+ accepted by the oxlint factory, and hybrid mode (`oxlint: true`) hands the check
933
+ to oxlint.
934
+
935
+ Two caveats. Deleting `foo.spec.ts` does not change `foo.ts`, so a cached run
936
+ replays the old clean result — the check is only sound uncached. And the plugin
937
+ writes `projectStructure.cache.json` into `projectRoot` while it has something
938
+ to report, so gitignore it.
939
+
893
940
  ### Git hooks
894
941
 
895
942
  If you want to apply lint and auto-fix before every commit, use
package/dist/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { existsSync, readFileSync } from "fs";
10
10
  import { execSync } from "node:child_process";
11
11
  //#region package.json
12
- var version = "6.0.0-beta.42";
12
+ var version = "6.0.0-beta.44";
13
13
  var package_default = {
14
14
  name: "@isentinel/eslint-config",
15
15
  version,
@@ -155,6 +155,7 @@ var package_default = {
155
155
  "eslint-plugin-jest": "catalog:peer",
156
156
  "eslint-plugin-jest-extended": "catalog:peer",
157
157
  "eslint-plugin-n": "catalog:peer",
158
+ "eslint-plugin-project-structure": "catalog:peer",
158
159
  "eslint-plugin-react-jsx": "catalog:peer",
159
160
  "eslint-plugin-react-naming-convention": "catalog:peer",
160
161
  "eslint-plugin-react-x": "catalog:peer",
@@ -179,6 +180,7 @@ var package_default = {
179
180
  "eslint-plugin-jest": "^29.15.0",
180
181
  "eslint-plugin-jest-extended": "^3.0.0",
181
182
  "eslint-plugin-n": "^17.0.0 || ^18.0.0",
183
+ "eslint-plugin-project-structure": "^3.14.0",
182
184
  "eslint-plugin-react-jsx": "^5.10.0",
183
185
  "eslint-plugin-react-naming-convention": "^5.10.0",
184
186
  "eslint-plugin-react-x": "^5.10.0",
@@ -192,6 +194,7 @@ var package_default = {
192
194
  "eslint-plugin-jest": { "optional": true },
193
195
  "eslint-plugin-jest-extended": { "optional": true },
194
196
  "eslint-plugin-n": { "optional": true },
197
+ "eslint-plugin-project-structure": { "optional": true },
195
198
  "eslint-plugin-react-jsx": { "optional": true },
196
199
  "eslint-plugin-react-naming-convention": { "optional": true },
197
200
  "eslint-plugin-react-x": { "optional": true },
@@ -531,7 +534,7 @@ async function updateEslintFiles(result) {
531
534
  //#endregion
532
535
  //#region src/cli/constants-generated.ts
533
536
  const versionsMap = {
534
- "eslint": "10.7.0",
537
+ "eslint": "10.8.1",
535
538
  "eslint-plugin-jest": "29.16.0",
536
539
  "eslint-plugin-react-jsx": "5.17.3",
537
540
  "eslint-plugin-react-naming-convention": "5.17.3",
package/dist/index.d.mts CHANGED
@@ -6488,6 +6488,7 @@ interface OverrideSiteChains {
6488
6488
  jsonc: [JsoncRuleDefaults];
6489
6489
  markdown: [MarkdownRuleDefaults];
6490
6490
  naming: FeatureChain;
6491
+ projectStructure: FeatureChain;
6491
6492
  react: ReactChain;
6492
6493
  roblox: MainChain;
6493
6494
  toml: [TomlRuleDefaults];
@@ -6497,7 +6498,7 @@ interface OverrideSiteChains {
6497
6498
  type TestChain = [TestRuleDefaults, SourceFeatureRuleDefaults, MainRuleDefaults];
6498
6499
  type ValidateOptionsWith<O, VK extends VariantKey> = ValidateSite<O, "rules", MainChain, VK> & ValidateSubOptionsFromTable<O, OverrideSiteChains, VK> & ValidateTestOption<O, TestChain, VK>;
6499
6500
  //#endregion
6500
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/1320bd07004d149fd5c550ffda02aec5ecff7717d8f1602b7e9e51dffe8368d5/node_modules/eslint-config-flat-gitignore/dist/index.d.mts
6501
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/0a594adbd94ca75e2950b1104992894d49bc93f4f2bc935ad192d7104e4c271e/node_modules/eslint-config-flat-gitignore/dist/index.d.mts
6501
6502
  interface FlatGitignoreOptions {
6502
6503
  /**
6503
6504
  * Name of the configuration.
@@ -10470,6 +10471,21 @@ interface RuleOptions {
10470
10471
  * @see https://eslint.org/docs/latest/rules/preserve-caught-error
10471
10472
  */
10472
10473
  'preserve-caught-error'?: Linter.RuleEntry<PreserveCaughtError>;
10474
+ /**
10475
+ * Enforce advanced naming rules and prohibit the use of given selectors in a given file. Have full control over what your file can contain and the naming conventions it must follow.
10476
+ * @see https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bfile%E2%80%91composition#root
10477
+ */
10478
+ 'project-structure/file-composition'?: Linter.RuleEntry<ProjectStructureFileComposition>;
10479
+ /**
10480
+ * Enforce rules on folder structure to keep your repository consistent, orderly and well thought out.
10481
+ * @see https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bfolder%E2%80%91structure#root
10482
+ */
10483
+ 'project-structure/folder-structure'?: Linter.RuleEntry<ProjectStructureFolderStructure>;
10484
+ /**
10485
+ * A key principle of a healthy project is to prevent the creation of a massive dependency tree, where removing or editing one feature triggers a chain reaction that impacts the entire project. Create independent modules to keep your project scalable and easy to maintain. Get rid of dependencies between modules and create truly independent functionalities.
10486
+ * @see https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bindependent%E2%80%91modules#root
10487
+ */
10488
+ 'project-structure/independent-modules'?: Linter.RuleEntry<ProjectStructureIndependentModules>;
10473
10489
  /**
10474
10490
  * Require returning inside each `then()` to create readable and reusable Promise chains.
10475
10491
  * @see https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/always-return.md
@@ -23278,6 +23294,18 @@ type PreserveCaughtError = [] | [{
23278
23294
  argumentPosition: number;
23279
23295
  })[];
23280
23296
  }];
23297
+ // ----- project-structure/file-composition -----
23298
+ type ProjectStructureFileComposition = [] | [{
23299
+ [k: string]: unknown | undefined;
23300
+ }];
23301
+ // ----- project-structure/folder-structure -----
23302
+ type ProjectStructureFolderStructure = [] | [{
23303
+ [k: string]: unknown | undefined;
23304
+ }];
23305
+ // ----- project-structure/independent-modules -----
23306
+ type ProjectStructureIndependentModules = [] | [{
23307
+ [k: string]: unknown | undefined;
23308
+ }];
23281
23309
  // ----- promise/always-return -----
23282
23310
  type PromiseAlwaysReturn = [] | [{
23283
23311
  ignoreLastCallback?: boolean;
@@ -27158,7 +27186,7 @@ type Yoda = [] | [("always" | "never")] | [("always" | "never"), {
27158
27186
  onlyEquality?: boolean;
27159
27187
  }];
27160
27188
  // Names of all the configs
27161
- type ConfigNames = 'isentinel/eslint/comments' | 'isentinel/eslint/comments/src' | 'isentinel/e18e/rules' | 'isentinel/eslint-plugin/setup' | 'isentinel/eslint-plugin/rules' | 'isentinel/flawless/setup' | 'isentinel/flawless/rules' | 'isentinel/flawless/markdown-code' | 'isentinel/flawless/rules-type-aware' | 'isentinel/gitignore' | 'isentinel/ignores' | 'isentinel/imports/rules' | 'isentinel/imports/game' | 'isentinel/javascript/setup' | 'isentinel/javascript/rules' | 'isentinel/jsdoc/setup' | 'isentinel/jsdoc' | 'isentinel/jsonc/setup' | 'isentinel/jsonc/rules' | 'isentinel/jsonc/tsconfig' | 'isentinel/markdown/setup' | 'isentinel/markdown/processor' | 'isentinel/markdown/parser' | 'isentinel/markdown/disables' | 'isentinel/naming/setup' | 'isentinel/naming/ts/rules-type-aware' | 'isentinel/naming/tsx/rules-type-aware' | 'isentinel/node/rules' | 'isentinel/oxfmt/setup' | 'isentinel/oxfmt/javascript' | 'isentinel/oxfmt/typescript' | 'isentinel/oxfmt/css' | 'isentinel/oxfmt/scss' | 'isentinel/oxfmt/less' | 'isentinel/oxfmt/html' | 'isentinel/oxfmt/markdown' | 'isentinel/oxfmt/graphql' | 'isentinel/oxfmt/json' | 'isentinel/oxfmt/yaml' | 'isentinel/package-json/setup' | 'isentinel/package-json' | 'isentinel/package-json/root' | 'isentinel/perfectionist/setup' | 'isentinel/perfectionist' | 'isentinel/perfectionist/jsx' | 'isentinel/pnpm/setup' | 'isentinel/pnpm/package-json' | 'isentinel/pnpm/pnpm-workspace-yaml' | 'isentinel/promise' | 'isentinel/react/setup' | 'isentinel/react/setup/naming' | 'isentinel/react/setup/testing-library' | 'isentinel/react/rules' | 'isentinel/react/type-aware-rules' | 'isentinel/roblox/setup' | 'isentinel/roblox/parser' | 'isentinel/roblox/type-aware-parser' | 'isentinel/roblox' | 'isentinel/roblox/rules-type-aware' | 'isentinel/roblox/format-lua/setup' | 'isentinel/roblox/format-lua' | 'isentinel/small-rules/setup' | 'isentinel/small-rules' | 'isentinel/sonarjs' | 'isentinel/spelling/setup' | 'isentinel/spelling' | 'isentinel/stylistic/setup' | 'isentinel/stylistic' | 'isentinel/test/jest/setup' | 'isentinel/test/jest/rules' | 'isentinel/test/jest/type-tests' | 'isentinel/test/vitest/setup' | 'isentinel/test/vitest/rules' | 'isentinel/test/vitest/type-tests' | 'isentinel/toml/setup' | 'isentinel/toml/rules' | 'isentinel/typescript/setup' | 'isentinel/typescript/parser' | 'isentinel/typescript/type-aware-parser' | 'isentinel/typescript/rules' | 'isentinel/typescript/rules-type-aware' | 'isentinel/typescript/erasable-syntax-only' | 'isentinel/unicorn/setup' | 'isentinel/unicorn/rules' | 'isentinel/unicorn/root' | 'isentinel/yaml/setup' | 'isentinel/yaml/rules';
27189
+ type ConfigNames = 'isentinel/eslint/comments' | 'isentinel/eslint/comments/src' | 'isentinel/e18e/rules' | 'isentinel/eslint-plugin/setup' | 'isentinel/eslint-plugin/rules' | 'isentinel/flawless/setup' | 'isentinel/flawless/rules' | 'isentinel/flawless/markdown-code' | 'isentinel/flawless/rules-type-aware' | 'isentinel/gitignore' | 'isentinel/ignores' | 'isentinel/imports/rules' | 'isentinel/imports/game' | 'isentinel/javascript/setup' | 'isentinel/javascript/rules' | 'isentinel/jsdoc/setup' | 'isentinel/jsdoc' | 'isentinel/jsonc/setup' | 'isentinel/jsonc/rules' | 'isentinel/jsonc/tsconfig' | 'isentinel/markdown/setup' | 'isentinel/markdown/processor' | 'isentinel/markdown/parser' | 'isentinel/markdown/disables' | 'isentinel/naming/setup' | 'isentinel/naming/ts/rules-type-aware' | 'isentinel/naming/tsx/rules-type-aware' | 'isentinel/node/rules' | 'isentinel/oxfmt/setup' | 'isentinel/oxfmt/javascript' | 'isentinel/oxfmt/typescript' | 'isentinel/oxfmt/css' | 'isentinel/oxfmt/scss' | 'isentinel/oxfmt/less' | 'isentinel/oxfmt/html' | 'isentinel/oxfmt/markdown' | 'isentinel/oxfmt/graphql' | 'isentinel/oxfmt/json' | 'isentinel/oxfmt/yaml' | 'isentinel/package-json/setup' | 'isentinel/package-json' | 'isentinel/package-json/root' | 'isentinel/perfectionist/setup' | 'isentinel/perfectionist' | 'isentinel/perfectionist/jsx' | 'isentinel/pnpm/setup' | 'isentinel/pnpm/package-json' | 'isentinel/pnpm/pnpm-workspace-yaml' | 'isentinel/project-structure/setup' | 'isentinel/project-structure/rules' | 'isentinel/promise' | 'isentinel/react/setup' | 'isentinel/react/setup/naming' | 'isentinel/react/setup/testing-library' | 'isentinel/react/rules' | 'isentinel/react/type-aware-rules' | 'isentinel/roblox/setup' | 'isentinel/roblox/parser' | 'isentinel/roblox/type-aware-parser' | 'isentinel/roblox' | 'isentinel/roblox/rules-type-aware' | 'isentinel/roblox/format-lua/setup' | 'isentinel/roblox/format-lua' | 'isentinel/small-rules/setup' | 'isentinel/small-rules' | 'isentinel/sonarjs' | 'isentinel/spelling/setup' | 'isentinel/spelling' | 'isentinel/stylistic/setup' | 'isentinel/stylistic' | 'isentinel/test/jest/setup' | 'isentinel/test/jest/rules' | 'isentinel/test/jest/type-tests' | 'isentinel/test/vitest/setup' | 'isentinel/test/vitest/rules' | 'isentinel/test/vitest/type-tests' | 'isentinel/toml/setup' | 'isentinel/toml/rules' | 'isentinel/typescript/setup' | 'isentinel/typescript/parser' | 'isentinel/typescript/type-aware-parser' | 'isentinel/typescript/rules' | 'isentinel/typescript/rules-type-aware' | 'isentinel/typescript/erasable-syntax-only' | 'isentinel/unicorn/setup' | 'isentinel/unicorn/rules' | 'isentinel/unicorn/root' | 'isentinel/yaml/setup' | 'isentinel/yaml/rules';
27162
27190
  //#endregion
27163
27191
  //#region src/utils.d.ts
27164
27192
  type ExtractRuleOptions<T> = T extends Linter.RuleEntry<infer U> ? U : never;
@@ -27496,6 +27524,37 @@ interface NamingConfig extends OptionsOverridesTypeAware {
27496
27524
  */
27497
27525
  selectorsTsx?: Array<NamingSelector>;
27498
27526
  }
27527
+ interface ProjectStructureConfig extends OptionsOverrides {
27528
+ /**
27529
+ * The files each linted source file has to sit next to, as
27530
+ * `enforceExistence` templates. Takes the plugin's name placeholders
27531
+ * (`{nodeName}`, `{NodeName}`, `{node-name}`, `{node_name}`,
27532
+ * `{NODE_NAME}`) plus `{ext}` for the extension of the file that matched.
27533
+ * A file already matching a template is exempt from it.
27534
+ *
27535
+ * @default ["{node-name}.spec.{ext}"]
27536
+ */
27537
+ enforceExistence?: Array<string> | string;
27538
+ /**
27539
+ * Files exempt from the check.
27540
+ *
27541
+ * @default [...GLOB_TESTS, GLOB_DTS, ...GLOB_BUILD_CONFIGS]
27542
+ */
27543
+ ignores?: Array<string>;
27544
+ /**
27545
+ * The folder the enforced paths resolve against, and where the plugin
27546
+ * writes `projectStructure.cache.json` when it reports.
27547
+ *
27548
+ * @default process.cwd()
27549
+ */
27550
+ projectRoot?: string;
27551
+ /**
27552
+ * Restricts the check to one subtree of `projectRoot`, as a relative path.
27553
+ *
27554
+ * @default undefined - the whole of `projectRoot`
27555
+ */
27556
+ structureRoot?: string;
27557
+ }
27499
27558
  interface OptionsTypeScriptParserOptions {
27500
27559
  /**
27501
27560
  * Glob patterns for files that should be type aware.
@@ -27844,6 +27903,14 @@ interface OptionsConfig extends OptionsComponentExtensions, OptionsProjectType {
27844
27903
  * @see https://github.com/antfu/pnpm-workspace-utils
27845
27904
  */
27846
27905
  pnpm?: boolean | OptionsPnpm;
27906
+ /**
27907
+ * Require every source file to have a co-located test file, via
27908
+ * [eslint-plugin-project-structure](https://github.com/Igorkowalski94/eslint-plugin-project-structure).
27909
+ *
27910
+ * @default false
27911
+ * @requires eslint-plugin-project-structure
27912
+ */
27913
+ projectStructure?: boolean | ProjectStructureConfig;
27847
27914
  /**
27848
27915
  * Enable react rules.
27849
27916
  *
@@ -28282,6 +28349,9 @@ declare function perfectionist(config?: OptionsProjectType & PerfectionistConfig
28282
28349
  //#region src/eslint/configs/pnpm.d.ts
28283
28350
  declare function pnpm(options: OptionsPnpm & Required<OptionsIsInEditor>): Promise<Array<TypedFlatConfigItem>>;
28284
28351
  //#endregion
28352
+ //#region src/eslint/configs/project-structure.d.ts
28353
+ declare function projectStructure({ enforceExistence, files, ignores, overrides, projectRoot, structureRoot }?: ProjectStructureConfig): Promise<Array<TypedFlatConfigItem>>;
28354
+ //#endregion
28285
28355
  //#region src/eslint/configs/promise.d.ts
28286
28356
  declare function promise(): Array<TypedFlatConfigItem>;
28287
28357
  //#endregion
@@ -28388,4 +28458,4 @@ declare function unicorn({ complementIgnores, nameReplacements, roblox, root: cu
28388
28458
  //#region src/eslint/configs/yaml.d.ts
28389
28459
  declare function yaml({ files, overrides, stylistic }?: OptionsFiles & OptionsOverrides & OptionsStylistic): Promise<Array<TypedFlatConfigItem>>;
28390
28460
  //#endregion
28391
- export { type Awaitable, type ConfigNames, type ConfigSettings, type FlatConfigComposer, GLOB_ALL_JSON, GLOB_ALL_SRC, GLOB_BIN, GLOB_BUILD_CONFIGS, GLOB_CSS, GLOB_DTS, GLOB_EXCLUDE, GLOB_GRAPHQL, GLOB_HTML, GLOB_JS, GLOB_JSON, GLOB_JSON5, GLOB_JSONC, GLOB_JSX, GLOB_LESS, GLOB_LINTABLE_EXTENSIONS, GLOB_LUA, GLOB_MARKDOWN, GLOB_MARKDOWN_BLOCKS, GLOB_MARKDOWN_CODE, GLOB_MARKDOWN_IN_MARKDOWN, GLOB_MISE, GLOB_PACKAGE_JSON, GLOB_PACKAGE_JSON_ROOT, GLOB_POSTCSS, GLOB_ROOT, GLOB_ROOT_SRC, GLOB_SCSS, GLOB_SRC, GLOB_SRC_EXT, GLOB_SRC_EXTENSIONS, GLOB_STYLE, GLOB_TESTS, GLOB_TOML, GLOB_TS, GLOB_TSCONFIG, GLOB_TSX, GLOB_TYPE_TESTS, GLOB_XML, GLOB_YAML, GitignoreOptions, type JsdocOptions, MARKDOWN_PROCESSOR_NAME, type NamedFlatConfigItem, type NamedOptionsConfig, type NamingConfig, type NamingSelector, type OptionsComponentExtensions, type OptionsConfig, type OptionsE18e, type OptionsFiles, type OptionsFilesTypeAware, type OptionsFormatters, type OptionsHasRoblox, type OptionsHasTypeScript, type OptionsIsInEditor, type OptionsJest, type OptionsOverrides, type OptionsOverridesTypeAware, type OptionsPnpm, type OptionsProjectType, type OptionsStylistic, type OptionsTestFramework, type OptionsTypeScriptErasableOnly, type OptionsTypeScriptParserOptions, type OptionsTypeScriptWithTypes, type OptionsTypescript, type OptionsUnicorn, type OptionsVitest, type OxfmtOptions, type PerfectionistConfig, type PrettierOptions, ROBLOX_ALLOWED_WORDS, type ReactConfig, type ReactSettings, type Rules, type SpellCheckConfig, type StylisticConfig, StylisticConfigDefaults, type TypedFlatConfigItem, comments, isentinel as default, isentinel, defaultPluginRenaming, disables, e18e, eslintPlugin, flawless, gitignore, ignores$1 as ignores, imports, isAgentAutofixDisabled, isInAgentSession, isInEditorEnvironment, isInGitHooksOrLintStaged, javascript, jsdoc, jsonc, loadSmallRulesPlugin, markdown, naming, node, oxfmt, packageJson, perfectionist, pnpm, promise, react, roblox$1 as roblox, smallRules, sonarjs, sortCspell, sortGithubAction, sortMiseToml, sortPnpmWorkspace, sortRojoProject, sortTsconfig, spelling, stylistic$1 as stylistic, test, toml, typescript, unicorn, yaml };
28461
+ export { type Awaitable, type ConfigNames, type ConfigSettings, type FlatConfigComposer, GLOB_ALL_JSON, GLOB_ALL_SRC, GLOB_BIN, GLOB_BUILD_CONFIGS, GLOB_CSS, GLOB_DTS, GLOB_EXCLUDE, GLOB_GRAPHQL, GLOB_HTML, GLOB_JS, GLOB_JSON, GLOB_JSON5, GLOB_JSONC, GLOB_JSX, GLOB_LESS, GLOB_LINTABLE_EXTENSIONS, GLOB_LUA, GLOB_MARKDOWN, GLOB_MARKDOWN_BLOCKS, GLOB_MARKDOWN_CODE, GLOB_MARKDOWN_IN_MARKDOWN, GLOB_MISE, GLOB_PACKAGE_JSON, GLOB_PACKAGE_JSON_ROOT, GLOB_POSTCSS, GLOB_ROOT, GLOB_ROOT_SRC, GLOB_SCSS, GLOB_SRC, GLOB_SRC_EXT, GLOB_SRC_EXTENSIONS, GLOB_STYLE, GLOB_TESTS, GLOB_TOML, GLOB_TS, GLOB_TSCONFIG, GLOB_TSX, GLOB_TYPE_TESTS, GLOB_XML, GLOB_YAML, GitignoreOptions, type JsdocOptions, MARKDOWN_PROCESSOR_NAME, type NamedFlatConfigItem, type NamedOptionsConfig, type NamingConfig, type NamingSelector, type OptionsComponentExtensions, type OptionsConfig, type OptionsE18e, type OptionsFiles, type OptionsFilesTypeAware, type OptionsFormatters, type OptionsHasRoblox, type OptionsHasTypeScript, type OptionsIsInEditor, type OptionsJest, type OptionsOverrides, type OptionsOverridesTypeAware, type OptionsPnpm, type OptionsProjectType, type OptionsStylistic, type OptionsTestFramework, type OptionsTypeScriptErasableOnly, type OptionsTypeScriptParserOptions, type OptionsTypeScriptWithTypes, type OptionsTypescript, type OptionsUnicorn, type OptionsVitest, type OxfmtOptions, type PerfectionistConfig, type PrettierOptions, type ProjectStructureConfig, ROBLOX_ALLOWED_WORDS, type ReactConfig, type ReactSettings, type Rules, type SpellCheckConfig, type StylisticConfig, StylisticConfigDefaults, type TypedFlatConfigItem, comments, isentinel as default, isentinel, defaultPluginRenaming, disables, e18e, eslintPlugin, flawless, gitignore, ignores$1 as ignores, imports, isAgentAutofixDisabled, isInAgentSession, isInEditorEnvironment, isInGitHooksOrLintStaged, javascript, jsdoc, jsonc, loadSmallRulesPlugin, markdown, naming, node, oxfmt, packageJson, perfectionist, pnpm, projectStructure, promise, react, roblox$1 as roblox, smallRules, sonarjs, sortCspell, sortGithubAction, sortMiseToml, sortPnpmWorkspace, sortRojoProject, sortTsconfig, spelling, stylistic$1 as stylistic, test, toml, typescript, unicorn, yaml };
package/dist/index.mjs CHANGED
@@ -1576,8 +1576,10 @@ function disables({ root, workspaceRoot }) {
1576
1576
  rules: {
1577
1577
  "antfu/no-top-level-await": "off",
1578
1578
  "no-console": "off",
1579
+ "roblox/no-null": "off",
1579
1580
  "small-rules/require-async-suffix": "off",
1580
- "ts/explicit-function-return-type": "off"
1581
+ "ts/explicit-function-return-type": "off",
1582
+ "unicorn/no-null": "off"
1581
1583
  }
1582
1584
  },
1583
1585
  {
@@ -1640,7 +1642,8 @@ function disables({ root, workspaceRoot }) {
1640
1642
  "import/no-default-export": "off",
1641
1643
  "small-rules/require-async-suffix": "off",
1642
1644
  "sonar/file-name-differ-from-class": "off",
1643
- "unicorn/filename-case": "off"
1645
+ "unicorn/filename-case": "off",
1646
+ "unicorn/no-null": "off"
1644
1647
  }
1645
1648
  },
1646
1649
  {
@@ -1886,7 +1889,7 @@ async function flawless(options = {}, prettierOptions = {}) {
1886
1889
  ];
1887
1890
  }
1888
1891
  //#endregion
1889
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@eslint/compat/2.1.0/b781043dbe4db6d501df442031a61cb3b4cc6aca6bc33271bed3c8c8e1ad757d/node_modules/@eslint/compat/dist/esm/index.js
1892
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@eslint/compat/2.1.0/90f1da88f603538864cab10657b202d55290237a3b641df9ac72b842a3be07f2/node_modules/@eslint/compat/dist/esm/index.js
1890
1893
  /**
1891
1894
  * @fileoverview Ignore file utilities for the compat package.
1892
1895
  * @author Nicholas C. Zakas
@@ -1915,7 +1918,7 @@ function convertIgnorePatternToMinimatch(pattern) {
1915
1918
  }
1916
1919
  var init_esm$1 = __esmMin((() => {}));
1917
1920
  //#endregion
1918
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/1320bd07004d149fd5c550ffda02aec5ecff7717d8f1602b7e9e51dffe8368d5/node_modules/eslint-config-flat-gitignore/dist/index.mjs
1921
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/0a594adbd94ca75e2950b1104992894d49bc93f4f2bc935ad192d7104e4c271e/node_modules/eslint-config-flat-gitignore/dist/index.mjs
1919
1922
  var dist_exports = /* @__PURE__ */ __exportAll$2({ default: () => ignore });
1920
1923
  function toArray(array) {
1921
1924
  array = array ?? [];
@@ -3947,6 +3950,92 @@ async function detectCatalogUsage() {
3947
3950
  return yaml.includes("catalog:") || yaml.includes("catalogs:");
3948
3951
  }
3949
3952
  //#endregion
3953
+ //#region src/rules/project-structure.ts
3954
+ init_globs();
3955
+ /** Stands for the extension of the file a rule matched. */
3956
+ const EXTENSION_TOKEN = "{ext}";
3957
+ const DEFAULT_ENFORCE_EXISTENCE = [`{node-name}.spec.${EXTENSION_TOKEN}`];
3958
+ /** Reusable-rule id for "a folder of any name". */
3959
+ const ANY_FOLDER = "isentinelAnyFolder";
3960
+ /** The files the co-location check runs on, before `ignores`. */
3961
+ const PROJECT_STRUCTURE_FILES = [GLOB_SRC];
3962
+ /** The files the co-location check skips. */
3963
+ const PROJECT_STRUCTURE_IGNORES = [
3964
+ ...GLOB_TESTS,
3965
+ GLOB_DTS,
3966
+ ...GLOB_BUILD_CONFIGS
3967
+ ];
3968
+ /**
3969
+ * Co-location rules shared between the ESLint and oxlint factories.
3970
+ *
3971
+ * `folder-structure` is not type-aware and runs unchanged as an oxlint
3972
+ * jsPlugin, so both engines have to hand it the same options or they disagree
3973
+ * about which files are co-located.
3974
+ *
3975
+ * @param options - The templates to enforce, and the roots they resolve
3976
+ * against.
3977
+ * @returns The rule map.
3978
+ */
3979
+ function folderStructureRules({ enforceExistence = DEFAULT_ENFORCE_EXISTENCE, projectRoot = process.cwd(), structureRoot } = {}) {
3980
+ const children = buildChildren(typeof enforceExistence === "string" ? [enforceExistence] : enforceExistence);
3981
+ return { "project-structure/folder-structure": ["error", {
3982
+ longPathsInfo: false,
3983
+ projectRoot,
3984
+ rules: { [ANY_FOLDER]: {
3985
+ name: "*",
3986
+ children
3987
+ } },
3988
+ structure: children,
3989
+ ...structureRoot === void 0 ? {} : { structureRoot }
3990
+ }] };
3991
+ }
3992
+ /**
3993
+ * The rules matching the files inside one folder, in the order
3994
+ * `folder-structure` tries them - first match wins.
3995
+ *
3996
+ * Every entry is permissive: `structure` is required by the rule and an
3997
+ * unmatched node is an error, so anything less would turn the co-location
3998
+ * check into whole-tree naming enforcement.
3999
+ *
4000
+ * @param templates - The `enforceExistence` templates, before substitution.
4001
+ * @returns The child rules, ending in the recursive folder rule.
4002
+ */
4003
+ function buildChildren(templates) {
4004
+ const children = templates.map((template) => {
4005
+ return { name: (template.split("/").pop() ?? template).replaceAll(/\{[^{}]*\}/g, "*") };
4006
+ });
4007
+ for (const extension of GLOB_SRC_EXTENSIONS) children.push({
4008
+ name: `*.${extension}`,
4009
+ enforceExistence: templates.map((template) => {
4010
+ return template.replaceAll(EXTENSION_TOKEN, () => extension);
4011
+ })
4012
+ });
4013
+ children.push({ ruleId: ANY_FOLDER });
4014
+ return children;
4015
+ }
4016
+ //#endregion
4017
+ //#region src/eslint/configs/project-structure.ts
4018
+ async function projectStructure({ enforceExistence, files = PROJECT_STRUCTURE_FILES, ignores = PROJECT_STRUCTURE_IGNORES, overrides = {}, projectRoot, structureRoot } = {}) {
4019
+ await ensurePackages(["eslint-plugin-project-structure"]);
4020
+ const { projectStructurePlugin } = await import("eslint-plugin-project-structure");
4021
+ return [{
4022
+ name: "isentinel/project-structure/setup",
4023
+ plugins: { "project-structure": projectStructurePlugin }
4024
+ }, {
4025
+ name: "isentinel/project-structure/rules",
4026
+ files,
4027
+ ignores,
4028
+ rules: {
4029
+ ...folderStructureRules({
4030
+ enforceExistence,
4031
+ projectRoot,
4032
+ structureRoot
4033
+ }),
4034
+ ...overrides
4035
+ }
4036
+ }];
4037
+ }
4038
+ //#endregion
3950
4039
  //#region src/rules/promise.ts
3951
4040
  /**
3952
4041
  * Promise rules shared between the ESLint and oxlint factories.
@@ -6154,7 +6243,7 @@ async function unicorn({ complementIgnores, nameReplacements, roblox = true, roo
6154
6243
  ];
6155
6244
  }
6156
6245
  //#endregion
6157
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/286798c5ceb234dddb418d7e51fa7c87c58efb51bf8e94fcfd0ad11404fe81c8/node_modules/eslint-plugin-yml/lib/chunk-CfYAbeIz.mjs
6246
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/a00fc88a04d0f2081267792f61f05ad4ceff0f52e1ff41acbbc866518b4fd9c1/node_modules/eslint-plugin-yml/lib/chunk-CfYAbeIz.mjs
6158
6247
  var __defProp$1, __exportAll$1;
6159
6248
  var init_chunk_CfYAbeIz = __esmMin((() => {
6160
6249
  __defProp$1 = Object.defineProperty;
@@ -9337,7 +9426,7 @@ var init_lib$1 = __esmMin((() => {
9337
9426
  ({ ...meta_exports$1 });
9338
9427
  }));
9339
9428
  //#endregion
9340
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/286798c5ceb234dddb418d7e51fa7c87c58efb51bf8e94fcfd0ad11404fe81c8/node_modules/eslint-plugin-yml/lib/index.mjs
9429
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/a00fc88a04d0f2081267792f61f05ad4ceff0f52e1ff41acbbc866518b4fd9c1/node_modules/eslint-plugin-yml/lib/index.mjs
9341
9430
  var lib_exports = /* @__PURE__ */ __exportAll$2({
9342
9431
  configs: () => configs,
9343
9432
  default: () => src_default,
@@ -15344,6 +15433,7 @@ const typeAwarePrefixes = /* @__PURE__ */ new Set([
15344
15433
  "package-json",
15345
15434
  "perfectionist",
15346
15435
  "pnpm",
15436
+ "project-structure",
15347
15437
  "promise",
15348
15438
  "react",
15349
15439
  "react-jsx",
@@ -15594,6 +15684,7 @@ var init_adapters = __esmMin((() => {
15594
15684
  "oxfmt": "eslint-plugin-oxfmt",
15595
15685
  "oxlint-comments": "oxlint-plugin-oxlint-comments",
15596
15686
  "perfectionist": "eslint-plugin-perfectionist",
15687
+ "project-structure": "eslint-plugin-project-structure",
15597
15688
  "promise-js": "eslint-plugin-promise",
15598
15689
  "react-jsx": "eslint-plugin-react-jsx",
15599
15690
  "react-naming-convention": "eslint-plugin-react-naming-convention",
@@ -16483,6 +16574,9 @@ var init_oxlint_capabilities = __esmMin((() => {
16483
16574
  "prefer-spread",
16484
16575
  "prefer-template",
16485
16576
  "preserve-caught-error",
16577
+ "project-structure/file-composition",
16578
+ "project-structure/folder-structure",
16579
+ "project-structure/independent-modules",
16486
16580
  "promise/always-return",
16487
16581
  "promise/avoid-new",
16488
16582
  "promise/catch-or-return",
@@ -19919,7 +20013,7 @@ const flatConfigProps = [
19919
20013
  "settings"
19920
20014
  ];
19921
20015
  async function isentinel(options, ...userConfigs) {
19922
- const { autoRenamePlugins = true, componentExts: componentExtensions = [], e18e: enableE18e = true, eslintPlugin: enableEslintPlugin = false, formatters, gitignore: enableGitignore = true, jsdoc: enableJsdoc = true, jsonc: enableJsonc = true, jsx: enableJsx = true, markdown: enableMarkdown = true, oxlint: enableOxlint = false, pnpm: enableCatalogs = findUpSync("pnpm-workspace.yaml") !== void 0, react: enableReact = false, root: customRootGlobs, spellCheck: enableSpellCheck, toml: enableToml = true, yaml: enableYaml = true } = options;
20016
+ const { autoRenamePlugins = true, componentExts: componentExtensions = [], e18e: enableE18e = true, eslintPlugin: enableEslintPlugin = false, formatters, gitignore: enableGitignore = true, jsdoc: enableJsdoc = true, jsonc: enableJsonc = true, jsx: enableJsx = true, markdown: enableMarkdown = true, oxlint: enableOxlint = false, pnpm: enableCatalogs = findUpSync("pnpm-workspace.yaml") !== void 0, projectStructure: enableProjectStructure = false, react: enableReact = false, root: customRootGlobs, spellCheck: enableSpellCheck, toml: enableToml = true, yaml: enableYaml = true } = options;
19923
20017
  const rootGlobs = mergeGlobs(GLOB_ROOT, customRootGlobs);
19924
20018
  const enableRoblox = options.roblox !== false;
19925
20019
  const reactOptions = resolveSubOptions(options, "react");
@@ -20042,6 +20136,7 @@ async function isentinel(options, ...userConfigs) {
20042
20136
  ...testOptions
20043
20137
  }));
20044
20138
  }
20139
+ if (enableProjectStructure !== false) configs.push(projectStructure(resolveSubOptions(options, "projectStructure")));
20045
20140
  if (enableReact !== false) configs.push(react({
20046
20141
  ...resolveSubOptions(options, "react"),
20047
20142
  ...getOverrides(options, "react"),
@@ -20169,4 +20264,4 @@ async function isentinel(options, ...userConfigs) {
20169
20264
  return composer;
20170
20265
  }
20171
20266
  //#endregion
20172
- export { GLOB_ALL_JSON, GLOB_ALL_SRC, GLOB_BIN, GLOB_BUILD_CONFIGS, GLOB_CSS, GLOB_DTS, GLOB_EXCLUDE, GLOB_GRAPHQL, GLOB_HTML, GLOB_JS, GLOB_JSON, GLOB_JSON5, GLOB_JSONC, GLOB_JSX, GLOB_LESS, GLOB_LINTABLE_EXTENSIONS, GLOB_LUA, GLOB_MARKDOWN, GLOB_MARKDOWN_BLOCKS, GLOB_MARKDOWN_CODE, GLOB_MARKDOWN_IN_MARKDOWN, GLOB_MISE, GLOB_PACKAGE_JSON, GLOB_PACKAGE_JSON_ROOT, GLOB_POSTCSS, GLOB_ROOT, GLOB_ROOT_SRC, GLOB_SCSS, GLOB_SRC, GLOB_SRC_EXT, GLOB_SRC_EXTENSIONS, GLOB_STYLE, GLOB_TESTS, GLOB_TOML, GLOB_TS, GLOB_TSCONFIG, GLOB_TSX, GLOB_TYPE_TESTS, GLOB_XML, GLOB_YAML, MARKDOWN_PROCESSOR_NAME, ROBLOX_ALLOWED_WORDS, StylisticConfigDefaults, comments, isentinel as default, isentinel, defaultPluginRenaming, disables, e18e, eslintPlugin, flawless, gitignore, ignores, imports, isAgentAutofixDisabled, isInAgentSession, isInEditorEnvironment, isInGitHooksOrLintStaged, javascript, jsdoc, jsonc, loadSmallRulesPlugin, markdown, naming, node, oxfmt, packageJson, perfectionist, pnpm, promise, react, roblox, smallRules, sonarjs, sortCspell, sortGithubAction, sortMiseToml, sortPnpmWorkspace, sortRojoProject, sortTsconfig, spelling, stylistic, test, toml, typescript, unicorn, yaml };
20267
+ export { GLOB_ALL_JSON, GLOB_ALL_SRC, GLOB_BIN, GLOB_BUILD_CONFIGS, GLOB_CSS, GLOB_DTS, GLOB_EXCLUDE, GLOB_GRAPHQL, GLOB_HTML, GLOB_JS, GLOB_JSON, GLOB_JSON5, GLOB_JSONC, GLOB_JSX, GLOB_LESS, GLOB_LINTABLE_EXTENSIONS, GLOB_LUA, GLOB_MARKDOWN, GLOB_MARKDOWN_BLOCKS, GLOB_MARKDOWN_CODE, GLOB_MARKDOWN_IN_MARKDOWN, GLOB_MISE, GLOB_PACKAGE_JSON, GLOB_PACKAGE_JSON_ROOT, GLOB_POSTCSS, GLOB_ROOT, GLOB_ROOT_SRC, GLOB_SCSS, GLOB_SRC, GLOB_SRC_EXT, GLOB_SRC_EXTENSIONS, GLOB_STYLE, GLOB_TESTS, GLOB_TOML, GLOB_TS, GLOB_TSCONFIG, GLOB_TSX, GLOB_TYPE_TESTS, GLOB_XML, GLOB_YAML, MARKDOWN_PROCESSOR_NAME, ROBLOX_ALLOWED_WORDS, StylisticConfigDefaults, comments, isentinel as default, isentinel, defaultPluginRenaming, disables, e18e, eslintPlugin, flawless, gitignore, ignores, imports, isAgentAutofixDisabled, isInAgentSession, isInEditorEnvironment, isInGitHooksOrLintStaged, javascript, jsdoc, jsonc, loadSmallRulesPlugin, markdown, naming, node, oxfmt, packageJson, perfectionist, pnpm, projectStructure, promise, react, roblox, smallRules, sonarjs, sortCspell, sortGithubAction, sortMiseToml, sortPnpmWorkspace, sortRojoProject, sortTsconfig, spelling, stylistic, test, toml, typescript, unicorn, yaml };
package/dist/lint-cli.mjs CHANGED
@@ -38,7 +38,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
38
  }) : target, mod));
39
39
  //#endregion
40
40
  //#region package.json
41
- var version = "6.0.0-beta.42";
41
+ var version = "6.0.0-beta.44";
42
42
  //#endregion
43
43
  //#region src/lint-cli/lib/cli/types.ts
44
44
  /**
@@ -1104,17 +1104,22 @@ const PACKAGE_RESOLUTION = {
1104
1104
  */
1105
1105
  function applyHashBust(run, bust, hash) {
1106
1106
  if (hash === void 0) return {
1107
- busted: false,
1107
+ cleared: [],
1108
1108
  firstRun: false
1109
1109
  };
1110
1110
  const swap = swapState(statePath(run.cwd, bust.name, run.key), hash);
1111
1111
  if (swap !== "changed") return {
1112
- busted: false,
1112
+ cleared: [],
1113
1113
  firstRun: swap === "first"
1114
1114
  };
1115
- for (const base of bust.caches) fs.rmSync(path.resolve(run.cwd, cacheFileFor(base, run.key)), { force: true });
1115
+ const cleared = [];
1116
+ for (const base of bust.caches) {
1117
+ const cacheFilePath = path.resolve(run.cwd, cacheFileFor(base, run.key));
1118
+ fs.rmSync(cacheFilePath, { force: true });
1119
+ cleared.push(cacheFilePath);
1120
+ }
1116
1121
  return {
1117
- busted: true,
1122
+ cleared,
1118
1123
  firstRun: false
1119
1124
  };
1120
1125
  }
@@ -4854,6 +4859,17 @@ function buildGateValue(resolution, options) {
4854
4859
  //#region src/lint-cli/lib/typescript/affected.ts
4855
4860
  const warned = /* @__PURE__ */ new Set();
4856
4861
  /**
4862
+ * State-file base name for the builder's incremental state, shared by the
4863
+ * per-project path and the prefix {@link hasBuilderState} scans for.
4864
+ */
4865
+ const BUILD_INFO_STATE = "tsbuildinfo";
4866
+ /**
4867
+ * State-file base name for the gate paired with each buildinfo. Deliberately
4868
+ * not a `tsbuildinfo-` suffix, so anything enumerating a variant's buildinfo
4869
+ * files does not pick a gate up as one (see {@link gateStatePath}).
4870
+ */
4871
+ const GATE_STATE = "tsgate";
4872
+ /**
4857
4873
  * Compute the set of files whose type-aware lint results may have changed since
4858
4874
  * the previous run, using TypeScript's builder API. The builder does a native
4859
4875
  * shape-hash BFS: it recomputes each dependent's emitted-`.d.ts` shape hash and
@@ -4911,6 +4927,44 @@ function computeAffectedFiles({ key, cwd }, mode) {
4911
4927
  }
4912
4928
  }
4913
4929
  /**
4930
+ * Whether a previous run left builder state for this mode and config variant.
4931
+ *
4932
+ * The one thing a builder pass does that nothing else can is establish this
4933
+ * state. Its affected set is disposable — a caller with an empty cache re-lints
4934
+ * regardless — but a run that skips the builder while no state exists only
4935
+ * defers the cost onto the next run, which then reports `firstRun` and throws
4936
+ * its affected set away. A file edited in between would be re-linted on its own
4937
+ * mtime while its importers kept stale type-aware cache entries, which no later
4938
+ * run revisits. Callers that skip the builder as an optimisation ask this
4939
+ * first.
4940
+ *
4941
+ * Any one project's state is enough: {@link computeAffectedFiles} reports
4942
+ * `firstRun` only when no project at all had state, and a newly added project
4943
+ * contributes its whole file set rather than an empty one.
4944
+ *
4945
+ * @param run - The run context.
4946
+ * @param mode - The active ESLint type-aware mode (never `"off"` here).
4947
+ * @returns True when at least one buildinfo for this mode and variant exists.
4948
+ */
4949
+ function hasBuilderState({ key, cwd }, mode) {
4950
+ const prefix = `${path.basename(statePath(cwd, BUILD_INFO_STATE, modeSuffix(mode), key))}-`;
4951
+ try {
4952
+ return fs.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
4953
+ } catch {
4954
+ return false;
4955
+ }
4956
+ }
4957
+ /**
4958
+ * The state-file segment discriminating the type-aware mode a builder ran
4959
+ * under. Two modes resolve different programs, so their state cannot be shared.
4960
+ *
4961
+ * @param mode - The active ESLint type-aware mode (never `"off"` here).
4962
+ * @returns The file-name segment for that mode.
4963
+ */
4964
+ function modeSuffix(mode) {
4965
+ return mode === "only" ? "typeaware" : "full";
4966
+ }
4967
+ /**
4914
4968
  * Resolve the builder incremental-state (`.tsbuildinfo`) file for a mode and
4915
4969
  * config variant.
4916
4970
  *
@@ -4934,7 +4988,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
4934
4988
  * @returns The absolute path to the mode's buildinfo file.
4935
4989
  */
4936
4990
  function builderStatePath(cwd, mode, key, projectId) {
4937
- return statePath(cwd, "tsbuildinfo", mode === "only" ? "typeaware" : "full", key, projectId);
4991
+ return statePath(cwd, BUILD_INFO_STATE, modeSuffix(mode), key, projectId);
4938
4992
  }
4939
4993
  /**
4940
4994
  * Resolve the gate state paired with one {@link builderStatePath} buildinfo:
@@ -4959,7 +5013,7 @@ function builderStatePath(cwd, mode, key, projectId) {
4959
5013
  * @returns The absolute path to the gate state file.
4960
5014
  */
4961
5015
  function gateStatePath(cwd, mode, key, projectId) {
4962
- return statePath(cwd, "tsgate", mode === "only" ? "typeaware" : "full", key, projectId);
5016
+ return statePath(cwd, GATE_STATE, modeSuffix(mode), key, projectId);
4963
5017
  }
4964
5018
  /**
4965
5019
  * Emit a warning at most once per distinct message. Keyed by message rather
@@ -5291,15 +5345,17 @@ function sizePasses(descriptors, run, inputs) {
5291
5345
  * @returns The number of dirty files.
5292
5346
  */
5293
5347
  function mutatingDirtyCount(descriptor, cacheLocation, targetFiles, { clearedCaches, run }) {
5294
- if (clearedCaches.has(cacheLocation)) return targetFiles.length;
5348
+ const buildsProgram = descriptor.invalidation !== "none";
5349
+ const mode = descriptor.invalidation === "only" ? "only" : void 0;
5350
+ if (clearedCaches.has(cacheLocation) && (!buildsProgram || hasBuilderState(run, mode))) return targetFiles.length;
5295
5351
  const cache = openCache(cacheLocation, run.ci);
5296
5352
  const dirty = new Set((cache?.getUpdatedFiles(targetFiles) ?? targetFiles).map((file) => normalizePath(file)));
5297
- if (descriptor.invalidation !== "none") {
5353
+ if (buildsProgram) {
5298
5354
  const outcome = applyTypeAwareInvalidation(run, {
5299
5355
  alreadyDirty: dirty,
5300
5356
  cache,
5301
5357
  cacheLocation,
5302
- mode: descriptor.invalidation === "only" ? "only" : void 0,
5358
+ mode,
5303
5359
  targetFiles
5304
5360
  });
5305
5361
  if (outcome.busted) return targetFiles.length;
@@ -5423,9 +5479,13 @@ function plan(options, run) {
5423
5479
  const canMutateCaches = mutate && options.cache;
5424
5480
  const hasTypeAwarePass = descriptors.some((descriptor) => descriptor.invalidation !== "none");
5425
5481
  const configHash = options.cache ? computeConfigHash(cwd, files.configFiles) : void 0;
5426
- if (canMutateCaches) applyHashBust(run, CONFIG_DRIFT, configHash);
5427
- if (canMutateCaches && hasTypeAwarePass) applyHashBust(run, PACKAGE_RESOLUTION, computePackageJsonHash(cwd));
5428
- const clearedCaches = new Set(canMutateCaches ? sweepStaleCaches(cwd, newestBustMtime) : []);
5482
+ const cleared = [];
5483
+ if (canMutateCaches) {
5484
+ cleared.push(...applyHashBust(run, CONFIG_DRIFT, configHash).cleared);
5485
+ if (hasTypeAwarePass) cleared.push(...applyHashBust(run, PACKAGE_RESOLUTION, computePackageJsonHash(cwd)).cleared);
5486
+ cleared.push(...sweepStaleCaches(cwd, newestBustMtime));
5487
+ }
5488
+ const clearedCaches = new Set(cleared);
5429
5489
  const sizingFiles = withoutIgnored(files, resolveIgnoredFiles(run, configHash, files.lintable));
5430
5490
  const inputs = {
5431
5491
  clearedCaches,
package/dist/oxlint.d.mts CHANGED
@@ -3860,6 +3860,7 @@ interface OverrideSiteChains {
3860
3860
  e18e: MainChain;
3861
3861
  eslintPlugin: FeatureChain;
3862
3862
  javascript: MainChain;
3863
+ projectStructure: FeatureChain;
3863
3864
  react: ReactChain;
3864
3865
  roblox: MainChain;
3865
3866
  typescript: MainChain;
@@ -7787,6 +7788,21 @@ interface RuleOptions {
7787
7788
  * @see https://eslint.org/docs/latest/rules/preserve-caught-error
7788
7789
  */
7789
7790
  'preserve-caught-error'?: Linter.RuleEntry<PreserveCaughtError>;
7791
+ /**
7792
+ * Enforce advanced naming rules and prohibit the use of given selectors in a given file. Have full control over what your file can contain and the naming conventions it must follow.
7793
+ * @see https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bfile%E2%80%91composition#root
7794
+ */
7795
+ 'project-structure/file-composition'?: Linter.RuleEntry<ProjectStructureFileComposition>;
7796
+ /**
7797
+ * Enforce rules on folder structure to keep your repository consistent, orderly and well thought out.
7798
+ * @see https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bfolder%E2%80%91structure#root
7799
+ */
7800
+ 'project-structure/folder-structure'?: Linter.RuleEntry<ProjectStructureFolderStructure>;
7801
+ /**
7802
+ * A key principle of a healthy project is to prevent the creation of a massive dependency tree, where removing or editing one feature triggers a chain reaction that impacts the entire project. Create independent modules to keep your project scalable and easy to maintain. Get rid of dependencies between modules and create truly independent functionalities.
7803
+ * @see https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bindependent%E2%80%91modules#root
7804
+ */
7805
+ 'project-structure/independent-modules'?: Linter.RuleEntry<ProjectStructureIndependentModules>;
7790
7806
  /**
7791
7807
  * Require returning inside each `then()` to create readable and reusable Promise chains.
7792
7808
  * @see https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/always-return.md
@@ -20595,6 +20611,18 @@ type PreserveCaughtError = [] | [{
20595
20611
  argumentPosition: number;
20596
20612
  })[];
20597
20613
  }];
20614
+ // ----- project-structure/file-composition -----
20615
+ type ProjectStructureFileComposition = [] | [{
20616
+ [k: string]: unknown | undefined;
20617
+ }];
20618
+ // ----- project-structure/folder-structure -----
20619
+ type ProjectStructureFolderStructure = [] | [{
20620
+ [k: string]: unknown | undefined;
20621
+ }];
20622
+ // ----- project-structure/independent-modules -----
20623
+ type ProjectStructureIndependentModules = [] | [{
20624
+ [k: string]: unknown | undefined;
20625
+ }];
20598
20626
  // ----- promise/always-return -----
20599
20627
  type PromiseAlwaysReturn = [] | [{
20600
20628
  ignoreLastCallback?: boolean;
@@ -24764,6 +24792,37 @@ interface NamingConfig extends OptionsOverridesTypeAware {
24764
24792
  */
24765
24793
  selectorsTsx?: Array<NamingSelector>;
24766
24794
  }
24795
+ interface ProjectStructureConfig extends OptionsOverrides {
24796
+ /**
24797
+ * The files each linted source file has to sit next to, as
24798
+ * `enforceExistence` templates. Takes the plugin's name placeholders
24799
+ * (`{nodeName}`, `{NodeName}`, `{node-name}`, `{node_name}`,
24800
+ * `{NODE_NAME}`) plus `{ext}` for the extension of the file that matched.
24801
+ * A file already matching a template is exempt from it.
24802
+ *
24803
+ * @default ["{node-name}.spec.{ext}"]
24804
+ */
24805
+ enforceExistence?: Array<string> | string;
24806
+ /**
24807
+ * Files exempt from the check.
24808
+ *
24809
+ * @default [...GLOB_TESTS, GLOB_DTS, ...GLOB_BUILD_CONFIGS]
24810
+ */
24811
+ ignores?: Array<string>;
24812
+ /**
24813
+ * The folder the enforced paths resolve against, and where the plugin
24814
+ * writes `projectStructure.cache.json` when it reports.
24815
+ *
24816
+ * @default process.cwd()
24817
+ */
24818
+ projectRoot?: string;
24819
+ /**
24820
+ * Restricts the check to one subtree of `projectRoot`, as a relative path.
24821
+ *
24822
+ * @default undefined - the whole of `projectRoot`
24823
+ */
24824
+ structureRoot?: string;
24825
+ }
24767
24826
  interface OptionsTypeScriptParserOptions {
24768
24827
  /**
24769
24828
  * Glob patterns for files that should be type aware.
@@ -25112,6 +25171,14 @@ interface OptionsConfig extends OptionsComponentExtensions, OptionsProjectType {
25112
25171
  * @see https://github.com/antfu/pnpm-workspace-utils
25113
25172
  */
25114
25173
  pnpm?: boolean | OptionsPnpm;
25174
+ /**
25175
+ * Require every source file to have a co-located test file, via
25176
+ * [eslint-plugin-project-structure](https://github.com/Igorkowalski94/eslint-plugin-project-structure).
25177
+ *
25178
+ * @default false
25179
+ * @requires eslint-plugin-project-structure
25180
+ */
25181
+ projectStructure?: boolean | ProjectStructureConfig;
25115
25182
  /**
25116
25183
  * Enable react rules.
25117
25184
  *
@@ -33193,7 +33260,7 @@ type OxlintCommentsRequireDescription = [] | [{
33193
33260
  ignore?: ("oxlint-disable" | "oxlint-disable-line" | "oxlint-disable-next-line" | "oxlint-enable")[];
33194
33261
  }];
33195
33262
  /** JsPlugin prefixes whose rules keep their ESLint-side names in oxlint configs. */
33196
- type OxlintKeptJsPluginPrefix = '@cspell' | 'antfu' | 'better-max-params' | 'comment-length' | 'de-morgan' | 'e18e' | 'erasable-syntax-only' | 'eslint-plugin' | 'flawless' | 'jest-extended' | 'oxfmt' | 'perfectionist' | 'react-jsx' | 'react-naming-convention' | 'roblox' | 'sentinel' | 'small-rules' | 'sonar' | 'style' | 'testing-library' | 'ts' | 'unused-imports';
33263
+ type OxlintKeptJsPluginPrefix = '@cspell' | 'antfu' | 'better-max-params' | 'comment-length' | 'de-morgan' | 'e18e' | 'erasable-syntax-only' | 'eslint-plugin' | 'flawless' | 'jest-extended' | 'oxfmt' | 'perfectionist' | 'project-structure' | 'react-jsx' | 'react-naming-convention' | 'roblox' | 'sentinel' | 'small-rules' | 'sonar' | 'style' | 'testing-library' | 'ts' | 'unused-imports';
33197
33264
  /** JsPlugin rules whose oxlint names match the ESLint-side names. */
33198
33265
  type OxlintKeptJsPluginRuleOptions = { [K in keyof RuleOptions as K extends `${OxlintKeptJsPluginPrefix}/${string}` ? K : never]: RuleOptions[K]; };
33199
33266
  /** All rules known to the oxlint factory, keyed by canonical oxlint rule name. */
package/dist/oxlint.mjs CHANGED
@@ -616,6 +616,9 @@ const jsPluginRuleNames = /* @__PURE__ */ new Set([
616
616
  "prefer-spread",
617
617
  "prefer-template",
618
618
  "preserve-caught-error",
619
+ "project-structure/file-composition",
620
+ "project-structure/folder-structure",
621
+ "project-structure/independent-modules",
619
622
  "promise/always-return",
620
623
  "promise/avoid-new",
621
624
  "promise/catch-or-return",
@@ -3237,6 +3240,7 @@ const enabledPresetRuleNames = /* @__PURE__ */ new Set([
3237
3240
  "prefer-rest-params",
3238
3241
  "prefer-spread",
3239
3242
  "prefer-template",
3243
+ "project-structure/folder-structure",
3240
3244
  "promise/always-return",
3241
3245
  "promise/catch-or-return",
3242
3246
  "promise/no-multiple-resolved",
@@ -4746,6 +4750,7 @@ const oxlintJsPlugins = {
4746
4750
  "oxfmt": "eslint-plugin-oxfmt",
4747
4751
  "oxlint-comments": "oxlint-plugin-oxlint-comments",
4748
4752
  "perfectionist": "eslint-plugin-perfectionist",
4753
+ "project-structure": "eslint-plugin-project-structure",
4749
4754
  "promise-js": "eslint-plugin-promise",
4750
4755
  "react-jsx": "eslint-plugin-react-jsx",
4751
4756
  "react-naming-convention": "eslint-plugin-react-naming-convention",
@@ -5164,6 +5169,23 @@ const GLOB_ROOT = [
5164
5169
  ];
5165
5170
  const GLOB_SRC_EXT = "{,c,m}[jt]s{,x}";
5166
5171
  const GLOB_SRC = "**/*.{,c,m}[jt]s{,x}";
5172
+ /**
5173
+ * The real TS/JS-family file extensions {@link GLOB_SRC_EXT} encodes (the glob
5174
+ * additionally matches non-existent combinations such as `cjsx`). Kept next to
5175
+ * the glob so the two are maintained together; consumed by the lint CLI to size
5176
+ * its type-aware pass.
5177
+ */
5178
+ const GLOB_SRC_EXTENSIONS = [
5179
+ "ts",
5180
+ "tsx",
5181
+ "mts",
5182
+ "cts",
5183
+ "js",
5184
+ "jsx",
5185
+ "mjs",
5186
+ "cjs"
5187
+ ];
5188
+ [...GLOB_SRC_EXTENSIONS];
5167
5189
  const GLOB_JSX = "**/*.{,c,m}jsx";
5168
5190
  const GLOB_TSX = "**/*.{,c,m}tsx";
5169
5191
  const GLOB_DTS = "**/*.d.{,c,m}ts";
@@ -6742,8 +6764,10 @@ function oxlintDisables({ configDirectory, root, workspaceRoot }) {
6742
6764
  rules: {
6743
6765
  "antfu/no-top-level-await": "off",
6744
6766
  "no-console": "off",
6767
+ "roblox/no-null": "off",
6745
6768
  "small-rules/require-async-suffix": "off",
6746
- "ts/explicit-function-return-type": "off"
6769
+ "ts/explicit-function-return-type": "off",
6770
+ "unicorn/no-null": "off"
6747
6771
  }
6748
6772
  }),
6749
6773
  ...createOxlintConfigs({
@@ -6811,7 +6835,8 @@ function oxlintDisables({ configDirectory, root, workspaceRoot }) {
6811
6835
  "import/no-default-export": "off",
6812
6836
  "small-rules/require-async-suffix": "off",
6813
6837
  "sonar/file-name-differ-from-class": "off",
6814
- "unicorn/filename-case": "off"
6838
+ "unicorn/filename-case": "off",
6839
+ "unicorn/no-null": "off"
6815
6840
  }
6816
6841
  }),
6817
6842
  ...createOxlintConfigs({
@@ -8009,6 +8034,94 @@ function oxlintPerfectionist(config) {
8009
8034
  })];
8010
8035
  }
8011
8036
  //#endregion
8037
+ //#region src/rules/project-structure.ts
8038
+ /** Stands for the extension of the file a rule matched. */
8039
+ const EXTENSION_TOKEN = "{ext}";
8040
+ const DEFAULT_ENFORCE_EXISTENCE = [`{node-name}.spec.${EXTENSION_TOKEN}`];
8041
+ /** Reusable-rule id for "a folder of any name". */
8042
+ const ANY_FOLDER = "isentinelAnyFolder";
8043
+ /** The files the co-location check runs on, before `ignores`. */
8044
+ const PROJECT_STRUCTURE_FILES = [GLOB_SRC];
8045
+ /** The files the co-location check skips. */
8046
+ const PROJECT_STRUCTURE_IGNORES = [
8047
+ ...GLOB_TESTS,
8048
+ GLOB_DTS,
8049
+ ...GLOB_BUILD_CONFIGS
8050
+ ];
8051
+ /**
8052
+ * Co-location rules shared between the ESLint and oxlint factories.
8053
+ *
8054
+ * `folder-structure` is not type-aware and runs unchanged as an oxlint
8055
+ * jsPlugin, so both engines have to hand it the same options or they disagree
8056
+ * about which files are co-located.
8057
+ *
8058
+ * @param options - The templates to enforce, and the roots they resolve
8059
+ * against.
8060
+ * @returns The rule map.
8061
+ */
8062
+ function folderStructureRules({ enforceExistence = DEFAULT_ENFORCE_EXISTENCE, projectRoot = process.cwd(), structureRoot } = {}) {
8063
+ const children = buildChildren(typeof enforceExistence === "string" ? [enforceExistence] : enforceExistence);
8064
+ return { "project-structure/folder-structure": ["error", {
8065
+ longPathsInfo: false,
8066
+ projectRoot,
8067
+ rules: { [ANY_FOLDER]: {
8068
+ name: "*",
8069
+ children
8070
+ } },
8071
+ structure: children,
8072
+ ...structureRoot === void 0 ? {} : { structureRoot }
8073
+ }] };
8074
+ }
8075
+ /**
8076
+ * The rules matching the files inside one folder, in the order
8077
+ * `folder-structure` tries them - first match wins.
8078
+ *
8079
+ * Every entry is permissive: `structure` is required by the rule and an
8080
+ * unmatched node is an error, so anything less would turn the co-location
8081
+ * check into whole-tree naming enforcement.
8082
+ *
8083
+ * @param templates - The `enforceExistence` templates, before substitution.
8084
+ * @returns The child rules, ending in the recursive folder rule.
8085
+ */
8086
+ function buildChildren(templates) {
8087
+ const children = templates.map((template) => {
8088
+ return { name: (template.split("/").pop() ?? template).replaceAll(/\{[^{}]*\}/g, "*") };
8089
+ });
8090
+ for (const extension of GLOB_SRC_EXTENSIONS) children.push({
8091
+ name: `*.${extension}`,
8092
+ enforceExistence: templates.map((template) => {
8093
+ return template.replaceAll(EXTENSION_TOKEN, () => extension);
8094
+ })
8095
+ });
8096
+ children.push({ ruleId: ANY_FOLDER });
8097
+ return children;
8098
+ }
8099
+ //#endregion
8100
+ //#region src/oxlint/configs/project-structure.ts
8101
+ /**
8102
+ * Co-location rules for standalone oxlint.
8103
+ *
8104
+ * `folder-structure` runs as a jsPlugin: it visits `Program` once per file and
8105
+ * answers from the filesystem, so oxlint needs no type information to run it.
8106
+ *
8107
+ * @param options - The templates to enforce, and the roots they resolve
8108
+ * against.
8109
+ * @returns The generated config fragments.
8110
+ */
8111
+ function oxlintProjectStructure({ enforceExistence, files = PROJECT_STRUCTURE_FILES, ignores = PROJECT_STRUCTURE_IGNORES, overrides = {}, projectRoot, structureRoot } = {}) {
8112
+ return createOxlintConfigs({
8113
+ name: "isentinel/project-structure",
8114
+ excludeFiles: ignores,
8115
+ files: files.flat(),
8116
+ overrides,
8117
+ rules: folderStructureRules({
8118
+ enforceExistence,
8119
+ projectRoot,
8120
+ structureRoot
8121
+ })
8122
+ });
8123
+ }
8124
+ //#endregion
8012
8125
  //#region src/rules/promise.ts
8013
8126
  /**
8014
8127
  * Promise rules shared between the ESLint and oxlint factories.
@@ -9226,7 +9339,7 @@ const NATIVE_ONLY_JS_PLUGINS = /* @__PURE__ */ new Set(["oxlint-comments"]);
9226
9339
  */
9227
9340
  function isentinel(factoryOptions, ...userConfigs) {
9228
9341
  const options = factoryOptions ?? { name: "isentinel" };
9229
- const { categories, componentExts: componentExtensions = [], e18e: enableE18e = true, env, eslintPlugin: enableEslintPlugin = false, formatters, gitignore: enableGitignore = true, globals, ignores, jsdoc: enableJsdoc = true, jsPlugins: userJsPlugins, jsx: enableJsx = true, options: linterOptions, oxc: enableOxc = true, react: enableReact = false, root: customRootGlobs, rules = {}, spellCheck: enableSpellCheck, test: enableTest = false, warnDroppedOverrides: enableDroppedOverrideWarning = true } = options;
9342
+ const { categories, componentExts: componentExtensions = [], e18e: enableE18e = true, env, eslintPlugin: enableEslintPlugin = false, formatters, gitignore: enableGitignore = true, globals, ignores, jsdoc: enableJsdoc = true, jsPlugins: userJsPlugins, jsx: enableJsx = true, options: linterOptions, oxc: enableOxc = true, projectStructure: enableProjectStructure = false, react: enableReact = false, root: customRootGlobs, rules = {}, spellCheck: enableSpellCheck, test: enableTest = false, warnDroppedOverrides: enableDroppedOverrideWarning = true } = options;
9230
9343
  const rootGlobs = mergeGlobs(GLOB_ROOT, customRootGlobs);
9231
9344
  const enableRoblox = options.roblox !== false;
9232
9345
  const reactOptions = resolveSubOptions(options, "react");
@@ -9355,6 +9468,7 @@ function isentinel(factoryOptions, ...userConfigs) {
9355
9468
  isInEditor
9356
9469
  }));
9357
9470
  if (enableEslintPlugin !== false) configs.push(oxlintEslintPlugin({ ...getOverrides(options, "eslintPlugin") }));
9471
+ if (enableProjectStructure !== false) configs.push(oxlintProjectStructure(resolveSubOptions(options, "projectStructure")));
9358
9472
  if (enableOxc) {
9359
9473
  configs.push(oxlintOxc({ roblox: enableRoblox }));
9360
9474
  if (needsComplementOverlay) configs.push(oxlintOxc({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/eslint-config",
3
- "version": "6.0.0-beta.42",
3
+ "version": "6.0.0-beta.44",
4
4
  "description": "iSentinel's ESLint config",
5
5
  "keywords": [
6
6
  "eslint-config",
@@ -109,7 +109,7 @@
109
109
  "yargs": "18.0.0"
110
110
  },
111
111
  "devDependencies": {
112
- "@antfu/ni": "30.2.0",
112
+ "@antfu/ni": "30.5.0",
113
113
  "@eslint-react/shared": "5.17.3",
114
114
  "@eslint/config-inspector": "3.1.0",
115
115
  "@isentinel/tsconfig": "1.2.0",
@@ -122,12 +122,13 @@
122
122
  "@types/yargs": "17.0.35",
123
123
  "@vitest/eslint-plugin": "1.6.26",
124
124
  "bumpp": "12.0.0",
125
- "eslint": "10.7.0",
125
+ "eslint": "10.8.1",
126
126
  "eslint-plugin-erasable-syntax-only": "0.4.2",
127
127
  "eslint-plugin-eslint-plugin": "7.5.0",
128
128
  "eslint-plugin-jest": "29.16.0",
129
129
  "eslint-plugin-jest-extended": "3.0.1",
130
130
  "eslint-plugin-n": "18.2.2",
131
+ "eslint-plugin-project-structure": "3.14.3",
131
132
  "eslint-plugin-react-jsx": "5.17.3",
132
133
  "eslint-plugin-react-naming-convention": "5.17.3",
133
134
  "eslint-plugin-react-x": "5.17.3",
@@ -144,7 +145,7 @@
144
145
  "typescript": "6.0.3",
145
146
  "unplugin-unused": "0.5.7",
146
147
  "vitest": "4.1.10",
147
- "@isentinel/eslint-config": "6.0.0-beta.42"
148
+ "@isentinel/eslint-config": "6.0.0-beta.44"
148
149
  },
149
150
  "peerDependencies": {
150
151
  "@vitest/eslint-plugin": "^1.6.4",
@@ -153,6 +154,7 @@
153
154
  "eslint-plugin-jest": "^29.15.0",
154
155
  "eslint-plugin-jest-extended": "^3.0.0",
155
156
  "eslint-plugin-n": "^17.0.0 || ^18.0.0",
157
+ "eslint-plugin-project-structure": "^3.14.0",
156
158
  "eslint-plugin-react-jsx": "^5.10.0",
157
159
  "eslint-plugin-react-naming-convention": "^5.10.0",
158
160
  "eslint-plugin-react-x": "^5.10.0",
@@ -176,6 +178,9 @@
176
178
  "eslint-plugin-n": {
177
179
  "optional": true
178
180
  },
181
+ "eslint-plugin-project-structure": {
182
+ "optional": true
183
+ },
179
184
  "eslint-plugin-react-jsx": {
180
185
  "optional": true
181
186
  },