@isentinel/eslint-config 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oxlint.d.mts CHANGED
@@ -25140,6 +25140,20 @@ interface OptionsConfig extends OptionsComponentExtensions, OptionsProjectType {
25140
25140
  * @requires eslint-plugin-eslint-plugin
25141
25141
  */
25142
25142
  eslintPlugin?: boolean | OptionsOverrides;
25143
+ /**
25144
+ * Where formatting runs.
25145
+ *
25146
+ * - `"eslint"`: oxfmt runs inside the linter as the `oxfmt/oxfmt` rule.
25147
+ * - `"standalone"`: oxfmt runs as its own tool (`oxfmt`, with an
25148
+ * `oxfmt.config.ts` built by `isentinelOxfmt` from
25149
+ * `@isentinel/eslint-config/oxfmt`). The linter keeps the
25150
+ * formatter-conflict disables and still reads the oxfmt config so
25151
+ * width-aware rules agree with it, but enables `oxfmt/oxfmt` nowhere.
25152
+ * Lua formatting (Stylua) stays in ESLint, as oxfmt does not format Lua.
25153
+ *
25154
+ * @default "eslint"
25155
+ */
25156
+ formatter?: "eslint" | "standalone";
25143
25157
  /**
25144
25158
  * Use external formatters to format files.
25145
25159
  *
package/dist/oxlint.mjs CHANGED
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import process from "node:process";
8
8
  import { getStaticYAMLValue, parseYAML } from "yaml-eslint-parser";
9
9
  import { findUpSync } from "find-up-simple";
10
+ import { isMainThread } from "node:worker_threads";
10
11
  import { minVersion } from "semver";
11
12
  import { pathToFileURL } from "node:url";
12
13
  import globals from "globals";
@@ -5236,10 +5237,19 @@ const GLOB_SRC_EXTENSIONS = [
5236
5237
  "cjs"
5237
5238
  ];
5238
5239
  [...GLOB_SRC_EXTENSIONS];
5240
+ const GLOB_JS = "**/*.{,c,m}js";
5239
5241
  const GLOB_JSX = "**/*.{,c,m}jsx";
5242
+ const GLOB_TS = "**/*.{,c,m}ts";
5240
5243
  const GLOB_TSX = "**/*.{,c,m}tsx";
5241
5244
  const GLOB_DTS = "**/*.d.{,c,m}ts";
5242
5245
  const GLOB_BIN = ["**/bin/**/*", `**/bin.${GLOB_SRC_EXT}`];
5246
+ const GLOB_CSS = "**/*.css";
5247
+ const GLOB_POSTCSS = "**/*.{p,post}css";
5248
+ const GLOB_LESS = "**/*.less";
5249
+ const GLOB_SCSS = "**/*.scss";
5250
+ const GLOB_ALL_JSON = "**/*.json{,5,c}";
5251
+ const GLOB_MARKDOWN = "**/*.md";
5252
+ const GLOB_YAML = "**/*.y{,a}ml";
5243
5253
  /**
5244
5254
  * Type tests (`expectTypeOf` / `assertType`), run by Vitest's `typecheck`
5245
5255
  * mode. They are test files, but assert nothing at runtime.
@@ -5414,6 +5424,79 @@ function resolvePrettierSettings(prettierOptions = {}, cwd = process.cwd()) {
5414
5424
  ...prettierOptions
5415
5425
  };
5416
5426
  }
5427
+ /**
5428
+ * Replace `printWidth` and `tabWidth` in the formatting settings with the
5429
+ * values oxfmt really uses.
5430
+ *
5431
+ * `flawless/arrow-return-style` hands `printWidth` to oxfmt to predict its
5432
+ * output (`useOxfmt.printWidth`), so it must match the width oxfmt formats
5433
+ * with, or `--fix` ping-pongs. An oxfmt config can set a width that differs
5434
+ * from the Prettier-shaped settings, so the oxfmt value wins.
5435
+ *
5436
+ * @template T - The settings shape.
5437
+ * @param settings - The resolved formatting settings.
5438
+ * @param oxfmtOptions - The effective oxfmt options.
5439
+ * @returns The settings with oxfmt's widths.
5440
+ */
5441
+ function withOxfmtWidths(settings, { printWidth, tabWidth }) {
5442
+ return {
5443
+ ...settings,
5444
+ ...typeof printWidth === "number" ? { printWidth } : {},
5445
+ ...typeof tabWidth === "number" ? { tabWidth } : {}
5446
+ };
5447
+ }
5448
+ /**
5449
+ * The indent oxfmt writes.
5450
+ *
5451
+ * @param oxfmtOptions - The effective oxfmt options.
5452
+ * @returns `"tab"`, or the number of spaces.
5453
+ */
5454
+ function oxfmtIndent({ tabWidth, useTabs }) {
5455
+ if (useTabs !== false) return "tab";
5456
+ return typeof tabWidth === "number" ? tabWidth : 2;
5457
+ }
5458
+ /**
5459
+ * Default the stylistic `indent` and `quotes` to what oxfmt writes, so the
5460
+ * stylistic rules and oxfmt agree. Explicit `stylistic` values win.
5461
+ *
5462
+ * @param stylistic - The user's stylistic options.
5463
+ * @param oxfmtOptions - The effective oxfmt options.
5464
+ * @returns The stylistic options with oxfmt's indent and quotes.
5465
+ */
5466
+ function withOxfmtStyle(stylistic, oxfmtOptions) {
5467
+ return {
5468
+ indent: oxfmtIndent(oxfmtOptions),
5469
+ quotes: oxfmtOptions["singleQuote"] === true ? "single" : "double",
5470
+ ...stylistic
5471
+ };
5472
+ }
5473
+ /**
5474
+ * Turn the nearest `.editorconfig`'s sections into oxfmt overrides, the way
5475
+ * the `oxfmt/oxfmt` rule's config loader does: `[*]` is skipped (it feeds the
5476
+ * root options), and each other section becomes an override whose globs are
5477
+ * relative to `cwd`. A key set in the oxfmt config root wins over the
5478
+ * sections.
5479
+ *
5480
+ * @param cwd - The config directory.
5481
+ * @param root - The oxfmt config root options.
5482
+ * @returns The overrides, in file order.
5483
+ */
5484
+ function editorConfigOverrides(cwd = process.cwd(), root = {}) {
5485
+ for (const directory of ancestorDirectories(cwd)) {
5486
+ const contents = readFileOrUndefined(path.join(directory, ".editorconfig"));
5487
+ if (contents === void 0) continue;
5488
+ const prefix = path.relative(cwd, directory).split(path.sep).join("/");
5489
+ return parseEditorConfig(contents).sections.flatMap(({ pattern, properties }) => {
5490
+ const options = Object.fromEntries(Object.entries(editorConfigToOxfmt(properties)).filter(([key]) => !Object.hasOwn(root, key)));
5491
+ if (pattern === "*" || Object.keys(options).length === 0) return [];
5492
+ return [{
5493
+ files: [prefix === "" ? pattern : `${prefix}/${pattern.replace(LEADING_SLASH, "")}`],
5494
+ options
5495
+ }];
5496
+ });
5497
+ }
5498
+ return [];
5499
+ }
5417
5500
  function readFileOrUndefined(filePath) {
5418
5501
  try {
5419
5502
  return fs.readFileSync(filePath, "utf-8");
@@ -5593,6 +5676,31 @@ function resolveEditorConfigOptions(cwd) {
5593
5676
  }
5594
5677
  return editorConfigToPrettier(properties);
5595
5678
  }
5679
+ /**
5680
+ * Map one `.editorconfig` section to oxfmt options, as the rule's config
5681
+ * loader does.
5682
+ *
5683
+ * @param properties - The section's properties.
5684
+ * @returns The oxfmt options it sets.
5685
+ */
5686
+ function editorConfigToOxfmt(properties) {
5687
+ const options = {};
5688
+ const endOfLine = properties["end_of_line"];
5689
+ if (endOfLine === "cr" || endOfLine === "crlf" || endOfLine === "lf") options["endOfLine"] = endOfLine;
5690
+ const indentStyle = properties["indent_style"];
5691
+ if (indentStyle === "tab" || indentStyle === "space") options["useTabs"] = indentStyle === "tab";
5692
+ const quoteType = properties["quote_type"];
5693
+ if (quoteType === "single" || quoteType === "double") options["singleQuote"] = quoteType === "single";
5694
+ const indentSize = Number(properties["indent_size"]);
5695
+ const tabWidth = Number(properties["tab_width"]);
5696
+ if (options["useTabs"] === false && properties["indent_size"] && Number.isFinite(indentSize)) options["tabWidth"] = indentSize;
5697
+ else if (properties["tab_width"] && Number.isFinite(tabWidth)) options["tabWidth"] = tabWidth;
5698
+ const printWidth = Number(properties["max_line_length"]);
5699
+ if (properties["max_line_length"] && Number.isFinite(printWidth)) options["printWidth"] = printWidth;
5700
+ const finalNewline = properties["insert_final_newline"];
5701
+ if (finalNewline === "true" || finalNewline === "false") options["insertFinalNewline"] = finalNewline === "true";
5702
+ return options;
5703
+ }
5596
5704
  //#endregion
5597
5705
  //#region src/rules/oxfmt.ts
5598
5706
  const UNSUPPORTED_PRETTIER_KEYS = /* @__PURE__ */ new Set([
@@ -5646,11 +5754,30 @@ function migratePrettierOptions(prettierOptions) {
5646
5754
  for (const [key, value] of Object.entries(prettierOptions)) {
5647
5755
  if (UNSUPPORTED_PRETTIER_KEYS.has(key)) continue;
5648
5756
  if (key === "endOfLine" && value === "auto") continue;
5649
- oxfmtOptions[key] = value;
5757
+ oxfmtOptions[key] = key === "overrides" ? migratePrettierOverrides(value) : value;
5650
5758
  }
5651
5759
  return oxfmtOptions;
5652
5760
  }
5653
5761
  /**
5762
+ * Migrate Prettier `overrides`: oxfmt wants `files` as an array, and the
5763
+ * nested options migrated like the root ones.
5764
+ *
5765
+ * @param overrides - The `overrides` value, of unknown shape.
5766
+ * @returns The overrides in oxfmt's shape; empty when not an array.
5767
+ */
5768
+ function migratePrettierOverrides(overrides) {
5769
+ if (!Array.isArray(overrides)) return [];
5770
+ return overrides.flatMap((override) => {
5771
+ if (typeof override !== "object" || override === null) return [];
5772
+ const { excludeFiles, files, options } = override;
5773
+ return [{
5774
+ ...excludeFiles === void 0 ? {} : { excludeFiles: stringsOf(excludeFiles) },
5775
+ files: stringsOf(files),
5776
+ options: typeof options === "object" && options !== null ? migratePrettierOptions(options) : {}
5777
+ }];
5778
+ });
5779
+ }
5780
+ /**
5654
5781
  * Build the effective oxfmt options shared between the ESLint and oxlint
5655
5782
  * factories.
5656
5783
  *
@@ -5666,6 +5793,9 @@ function buildOxfmtOptions({ oxfmtConfigOptions = {}, oxfmtOptions, prettierOpti
5666
5793
  ...oxfmtOptions
5667
5794
  };
5668
5795
  }
5796
+ function stringsOf(value) {
5797
+ return [value].flat().filter((item) => typeof item === "string");
5798
+ }
5669
5799
  //#endregion
5670
5800
  //#region src/rules/react.ts
5671
5801
  const DOM_IMPORT_MESSAGE = "Import from react-testing-library-lua instead; it re-exports the DOM utilities, and eslint-plugin-testing-library only detects one module per file.";
@@ -5878,7 +6008,9 @@ function isDomImportRestriction(value, domPackage) {
5878
6008
  function isObjectStyleRestriction(value) {
5879
6009
  return isRecord$1(value) && !("name" in value);
5880
6010
  }
5881
- createRequire(import.meta.url);
6011
+ //#endregion
6012
+ //#region src/utils.ts
6013
+ const require$2 = createRequire(import.meta.url);
5882
6014
  /**
5883
6015
  * Synchronous {@link findWorkspaceRoot}, for the oxlint factory.
5884
6016
  *
@@ -6108,27 +6240,168 @@ function environmentMatcher(name, pattern, { noTty = false } = {}) {
6108
6240
  return value !== void 0 && pattern.test(value);
6109
6241
  };
6110
6242
  }
6111
- const OXFMT_CONFIG_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
6243
+ /** JSON oxfmt config files oxfmt discovers. */
6244
+ const OXFMT_RC_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
6112
6245
  /**
6113
- * Resolve oxfmt configuration from `.oxfmtrc.json` or `.oxfmtrc.jsonc`,
6114
- * synchronously. Used by the (synchronous) oxlint factory.
6246
+ * Oxfmt config modules oxfmt discovers. It loads other module extensions only
6247
+ * through `--config`, never by search.
6248
+ */
6249
+ const OXFMT_CONFIG_MODULES = ["oxfmt.config.ts", "oxfmt.config.mts"];
6250
+ OXFMT_CONFIG_MODULES[0];
6251
+ /** Every file name oxfmt discovers. One directory may hold only one of them. */
6252
+ const OXFMT_CONFIG_NAMES = [...OXFMT_RC_FILES, ...OXFMT_CONFIG_MODULES];
6253
+ /** The extensions of the JS/TS files ESLint and oxlint format. */
6254
+ const SCRIPT_EXTENSIONS = [
6255
+ "cjs",
6256
+ "cts",
6257
+ "js",
6258
+ "jsx",
6259
+ "mjs",
6260
+ "mts",
6261
+ "ts",
6262
+ "tsx"
6263
+ ];
6264
+ /** The extensions of the Markdown files ESLint lints code blocks in. */
6265
+ const MARKDOWN_EXTENSIONS = ["md"];
6266
+ const LEADING_DOT_SLASH = /^\.\//;
6267
+ /** Set once a process has warned about a JS/TS width override. */
6268
+ const WARNED_SCRIPT_OVERRIDE = Symbol.for("@isentinel/eslint-config/oxfmt-override-warned");
6269
+ /** Set once a process has warned about options standalone oxfmt ignores. */
6270
+ const WARNED_STANDALONE = Symbol.for("@isentinel/eslint-config/standalone-options-warned");
6271
+ const GLOB_MAGIC = /[*?[\]{}]/;
6272
+ /**
6273
+ * List the oxfmt config files in one directory, without walking up.
6274
+ *
6275
+ * @param directory - Directory to check.
6276
+ * @returns The file names found, sorted.
6277
+ */
6278
+ function oxfmtConfigNamesIn(directory) {
6279
+ return OXFMT_CONFIG_NAMES.filter((name) => fs.existsSync(path.join(directory, name))).sort();
6280
+ }
6281
+ /**
6282
+ * Find the oxfmt config the way oxfmt does: walk up from `cwd` to the
6283
+ * filesystem root and take the first directory that holds one. Like oxfmt,
6284
+ * refuse a directory that holds more than one.
6285
+ *
6286
+ * @param cwd - Directory to start from.
6287
+ * @returns The config file, if any.
6288
+ * @throws When one directory holds more than one oxfmt config file.
6289
+ */
6290
+ function findOxfmtConfig(cwd = process.cwd()) {
6291
+ let directory = path.resolve(cwd);
6292
+ for (;;) {
6293
+ const [first, second] = oxfmtConfigNamesIn(directory);
6294
+ if (second !== void 0) throw new Error(`[@isentinel/eslint-config] Both '${first}' and '${second}' found in ${directory}. Keep one oxfmt config file.`);
6295
+ if (first !== void 0) return {
6296
+ isModule: OXFMT_CONFIG_MODULES.some((name) => name === first),
6297
+ path: path.join(directory, first)
6298
+ };
6299
+ const parent = path.dirname(directory);
6300
+ if (parent === directory) return;
6301
+ directory = parent;
6302
+ }
6303
+ }
6304
+ /**
6305
+ * The overrides that set `printWidth` or `tabWidth` for Markdown files.
6115
6306
  *
6116
- * @returns The oxfmt configuration options, or an empty object if none found.
6307
+ * @param overrides - The config's `overrides`.
6308
+ * @returns Each such override's scope and widths, in order.
6309
+ */
6310
+ function markdownOverridesOf(overrides) {
6311
+ if (!Array.isArray(overrides)) return [];
6312
+ return overrides.flatMap((override) => {
6313
+ if (!isRecord$1(override) || !overrideMatches(override, MARKDOWN_EXTENSIONS)) return [];
6314
+ const widths = widthsOf(override["options"]);
6315
+ return Object.keys(widths).length > 0 ? [{
6316
+ ...scopeOf(override),
6317
+ widths
6318
+ }] : [];
6319
+ });
6320
+ }
6321
+ /**
6322
+ * Where an override applies: its `files`, minus its `excludeFiles` and any
6323
+ * negated glob in `files`.
6324
+ *
6325
+ * @param override - One override.
6326
+ * @returns Its scope.
6327
+ */
6328
+ function scopeOf(override) {
6329
+ const strings = (value) => {
6330
+ return [value].flat().filter((glob) => typeof glob === "string");
6331
+ };
6332
+ const globs = strings(override["files"]);
6333
+ return {
6334
+ excludeFiles: [...strings(override["excludeFiles"]), ...globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1))],
6335
+ files: globs.filter((glob) => !glob.startsWith("!"))
6336
+ };
6337
+ }
6338
+ /**
6339
+ * Warn, at most once per process, about factory options that do not reach
6340
+ * standalone oxfmt: it formats with `oxfmt.config.ts` alone.
6341
+ *
6342
+ * @param options - The factory options.
6343
+ * @param options.componentExts - Oxfmt formats only the extensions
6344
+ * `isentinelOxfmt` is given.
6345
+ * @param options.formatters - Any value but `true` is ignored by oxfmt.
6346
+ * @param options.gitignore - Custom ignore files, or `false`, do not reach
6347
+ * oxfmt.
6348
+ * @param options.ignores - User ignores do not reach oxfmt.
6349
+ */
6350
+ function warnStandaloneOptions({ componentExts, formatters, gitignore, ignores }) {
6351
+ const state = globalThis;
6352
+ if (!isMainThread || state[WARNED_STANDALONE] === true || process.env["ISENTINEL_QUIET_CONFIG_WARNINGS"] === "1") return;
6353
+ const names = [
6354
+ formatters !== void 0 && formatters !== true ? "formatters" : void 0,
6355
+ Array.isArray(componentExts) && componentExts.length > 0 ? "componentExts" : void 0,
6356
+ ignores === void 0 || Array.isArray(ignores) && ignores.length === 0 ? void 0 : "ignores",
6357
+ gitignore === false || isRecord$1(gitignore) ? "gitignore" : void 0
6358
+ ].filter((name) => name !== void 0);
6359
+ if (names.length > 0) {
6360
+ state[WARNED_STANDALONE] = true;
6361
+ console.warn(`[@isentinel/eslint-config] With formatter: "standalone", oxfmt formats with oxfmt.config.ts alone, so these options do not change formatting: ${names.join(", ")}. Set the equivalent in isentinelOxfmt() (formatters, componentExts, ignorePatterns, format options).`);
6362
+ }
6363
+ }
6364
+ /**
6365
+ * Warn, at most once per process, when an oxfmt override changes `printWidth`
6366
+ * or `tabWidth` for JS/TS files: ESLint and oxlint use the root values.
6367
+ *
6368
+ * @param overrides - The config's `overrides`.
6369
+ * @param source - The config file name.
6370
+ */
6371
+ function warnScriptWidthOverrides(overrides, source) {
6372
+ const state = globalThis;
6373
+ if (state[WARNED_SCRIPT_OVERRIDE] === true || !Array.isArray(overrides)) return;
6374
+ if (overrides.some((override) => {
6375
+ if (!isRecord$1(override) || !isRecord$1(override["options"])) return false;
6376
+ const { printWidth, tabWidth } = override["options"];
6377
+ if (printWidth === void 0 && tabWidth === void 0) return false;
6378
+ return overrideMatches(override, SCRIPT_EXTENSIONS);
6379
+ })) {
6380
+ state[WARNED_SCRIPT_OVERRIDE] = true;
6381
+ console.warn(`[@isentinel/eslint-config] ${source} has an override that changes printWidth or tabWidth for JS/TS files. ESLint and oxlint use the root values for those files.`);
6382
+ }
6383
+ }
6384
+ /**
6385
+ * Synchronous {@link resolveOxfmtConfig}, for the (synchronous) oxlint factory.
6386
+ * Loads a config module through jiti's synchronous API. Oxlint does not lint
6387
+ * Markdown code blocks, so it returns the format options alone.
6388
+ *
6389
+ * @returns The oxfmt format options, or an empty object if none found.
6390
+ * @throws When the config is ambiguous or cannot be loaded.
6117
6391
  */
6118
6392
  function resolveOxfmtConfigOptionsSync() {
6119
- for (const filename of OXFMT_CONFIG_FILES) {
6120
- const configPath = path.resolve(process.cwd(), filename);
6121
- try {
6122
- const content = fs.readFileSync(configPath, "utf-8");
6123
- const parsed = JSON.parse(content);
6124
- if (!isRecord$1(parsed)) continue;
6125
- const { $schema: _, ...config } = parsed;
6126
- return config;
6127
- } catch {
6128
- continue;
6129
- }
6393
+ const config = findOxfmtConfig();
6394
+ if (config === void 0) return {};
6395
+ if (!config.isModule) return rcConfig(config.path, fs.readFileSync(config.path, "utf-8")).options;
6396
+ let loaded;
6397
+ try {
6398
+ const { createJiti } = require$2("jiti");
6399
+ const module = createJiti(import.meta.url, { moduleCache: false })(config.path);
6400
+ loaded = isRecord$1(module) && "default" in module ? module["default"] : module;
6401
+ } catch (err) {
6402
+ throw loadError(config.path, err);
6130
6403
  }
6131
- return {};
6404
+ return moduleConfig(config.path, loaded).options;
6132
6405
  }
6133
6406
  /**
6134
6407
  * Override the severity of all rules in a rules object, preserving rule
@@ -6152,6 +6425,105 @@ function overrideRuleSeverity(rules, severity, excludeRules = /* @__PURE__ */ ne
6152
6425
  }));
6153
6426
  }
6154
6427
  /**
6428
+ * Whether a glob can match a file with one of `extensions`, judged by the
6429
+ * extension of its last segment (`*.ts`, `*.{ts,tsx}`, `*.?ts`, `*`, ...).
6430
+ *
6431
+ * @param glob - An override `files` glob.
6432
+ * @param extensions - The extensions to look for.
6433
+ * @returns Whether it can match such a file.
6434
+ */
6435
+ function globMatchesExtension(glob, extensions) {
6436
+ const base = glob.split("/").at(-1) ?? glob;
6437
+ const dot = base.lastIndexOf(".");
6438
+ if (dot === -1) return base.includes("*");
6439
+ const extension = base.slice(dot + 1);
6440
+ if (!GLOB_MAGIC.test(extension)) return extensions.includes(extension);
6441
+ return extensions.some((candidate) => path.matchesGlob(candidate, extension));
6442
+ }
6443
+ /**
6444
+ * Whether an oxfmt override can apply to a file with one of `extensions`.
6445
+ *
6446
+ * @param override - One entry of the config's `overrides`.
6447
+ * @param extensions - The extensions to look for.
6448
+ * @returns Whether its `files` can match such a file.
6449
+ */
6450
+ function overrideMatches(override, extensions) {
6451
+ return (Array.isArray(override["files"]) ? override["files"] : [override["files"]]).some((glob) => typeof glob === "string" && globMatchesExtension(glob, extensions));
6452
+ }
6453
+ function loadError(configPath, error) {
6454
+ return new Error(`[@isentinel/eslint-config] Failed to load ${path.basename(configPath)}: ${String(error)}`, { cause: error });
6455
+ }
6456
+ /**
6457
+ * Whether a config override is an `.editorconfig` section that
6458
+ * `isentinelOxfmt` copied in: the same globs (anchored or not) and a subset
6459
+ * of its options.
6460
+ *
6461
+ * @param override - One config override.
6462
+ * @param sections - The `.editorconfig` sections, as overrides.
6463
+ * @returns Whether it mirrors one of them.
6464
+ */
6465
+ function mirrorsSection(override, sections) {
6466
+ if (!isRecord$1(override) || !isRecord$1(override["options"])) return false;
6467
+ const { options } = override;
6468
+ const files = [override["files"]].flat().map((glob) => typeof glob === "string" ? glob.replace(LEADING_DOT_SLASH, "") : glob);
6469
+ return sections.some((section) => {
6470
+ return section.files.length === files.length && section.files.every((glob, index) => glob === files[index]) && Object.entries(options).every(([key, value]) => section.options[key] === value);
6471
+ });
6472
+ }
6473
+ /**
6474
+ * Turn a loaded oxfmt config into `oxfmt/oxfmt` rule options and the Markdown
6475
+ * widths. The rule takes format options only, so `$schema`, `ignorePatterns`
6476
+ * and `overrides` are dropped from the options.
6477
+ *
6478
+ * @param config - The loaded config.
6479
+ * @param config.$schema - Dropped.
6480
+ * @param config.ignorePatterns - Dropped; oxfmt applies them itself.
6481
+ * @param config.overrides - Dropped, after the width check; read for the
6482
+ * Markdown widths.
6483
+ * @param configPath - The config file, for the warning.
6484
+ * @returns The format options and Markdown widths.
6485
+ */
6486
+ function toResolvedConfig({ $schema: _schema, ignorePatterns: _ignorePatterns, overrides, ...options }, configPath) {
6487
+ const sections = editorConfigOverrides(path.dirname(configPath));
6488
+ const list = Array.isArray(overrides) ? overrides : [];
6489
+ const copied = list.filter((override) => mirrorsSection(override, sections));
6490
+ warnScriptWidthOverrides(copied, ".editorconfig");
6491
+ warnScriptWidthOverrides(list.filter((override) => !copied.includes(override)), path.basename(configPath));
6492
+ return {
6493
+ markdownOverrides: markdownOverridesOf(overrides),
6494
+ options,
6495
+ overrides: Array.isArray(overrides) ? overrides : []
6496
+ };
6497
+ }
6498
+ function moduleConfig(configPath, loaded) {
6499
+ if (!isRecord$1(loaded)) throw loadError(configPath, /* @__PURE__ */ new TypeError("the default export is not an object"));
6500
+ return toResolvedConfig(loaded, configPath);
6501
+ }
6502
+ function rcConfig(configPath, content) {
6503
+ let parsed;
6504
+ try {
6505
+ parsed = getStaticJSONValue(parseJSON(content, { jsonSyntax: "jsonc" }));
6506
+ } catch (err) {
6507
+ throw loadError(configPath, err);
6508
+ }
6509
+ if (!isRecord$1(parsed)) throw loadError(configPath, /* @__PURE__ */ new TypeError("the config is not an object"));
6510
+ return toResolvedConfig(parsed, configPath);
6511
+ }
6512
+ /**
6513
+ * Pick `printWidth` and `tabWidth` out of a set of format options.
6514
+ *
6515
+ * @param options - Format options, of unknown shape.
6516
+ * @returns The widths it sets.
6517
+ */
6518
+ function widthsOf(options) {
6519
+ if (!isRecord$1(options)) return {};
6520
+ const { printWidth, tabWidth } = options;
6521
+ return {
6522
+ ...typeof printWidth === "number" ? { printWidth } : {},
6523
+ ...typeof tabWidth === "number" ? { tabWidth } : {}
6524
+ };
6525
+ }
6526
+ /**
6155
6527
  * Resolves the lowest Node major version a semver range can match.
6156
6528
  *
6157
6529
  * @param range - A semver range such as `>=24.12.0` or `^24 || ^26`.
@@ -7707,6 +8079,28 @@ const defaultPluginRenaming = {
7707
8079
  "yml": "yaml"
7708
8080
  };
7709
8081
  //#endregion
8082
+ //#region src/formatted-files.ts
8083
+ /**
8084
+ * The files the preset formats with oxfmt. ESLint mode enables `oxfmt/oxfmt`
8085
+ * on these globs; the standalone oxfmt config ignores every other file. One
8086
+ * list keeps both modes formatting the same files.
8087
+ *
8088
+ * Imported by the standalone oxfmt config, so keep it light.
8089
+ */
8090
+ /** Formatted globs, by `oxfmt/oxfmt` config block. */
8091
+ const FORMATTED_FILES = {
8092
+ css: [GLOB_CSS, GLOB_POSTCSS],
8093
+ graphql: ["**/*.graphql"],
8094
+ html: ["**/*.html"],
8095
+ javascript: [GLOB_JS, GLOB_JSX],
8096
+ json: [GLOB_ALL_JSON],
8097
+ less: [GLOB_LESS],
8098
+ markdown: [GLOB_MARKDOWN],
8099
+ scss: [GLOB_SCSS],
8100
+ typescript: [GLOB_TS, GLOB_TSX],
8101
+ yaml: [GLOB_YAML]
8102
+ };
8103
+ //#endregion
7710
8104
  //#region src/generated/stylistic.ts
7711
8105
  const stylisticRuleNames = /* @__PURE__ */ new Set([
7712
8106
  "array-bracket-newline",
@@ -7811,7 +8205,7 @@ const stylisticRuleNames = /* @__PURE__ */ new Set([
7811
8205
  //#region src/oxlint/configs/oxfmt.ts
7812
8206
  const require$1 = createRequire(import.meta.url);
7813
8207
  function oxlintOxfmt(options) {
7814
- const { componentExts: componentExtensions = [], files: oxfmtFiles, oxfmtOptions = {} } = options ?? {};
8208
+ const { componentExts: componentExtensions = [], files: oxfmtFiles, oxfmtOptions = {}, standalone = false } = options ?? {};
7815
8209
  const configPrettier = require$1("eslint-config-prettier/flat");
7816
8210
  const configPrettierRules = isRecord$1(configPrettier) && isRecord$1(configPrettier["rules"]) ? configPrettier["rules"] : {};
7817
8211
  const rulesToIgnore = /* @__PURE__ */ new Set(["curly", "style/quotes"]);
@@ -7831,12 +8225,8 @@ function oxlintOxfmt(options) {
7831
8225
  name: "oxfmt",
7832
8226
  specifier: resolveJsPluginSpecifier("eslint-plugin-oxfmt")
7833
8227
  };
7834
- const jsFiles = oxfmtFiles?.flat() ?? ["**/*.{,c,m}js", "**/*.{,c,m}jsx"];
7835
- const tsFiles = oxfmtFiles?.flat() ?? [
7836
- "**/*.{,c,m}ts",
7837
- "**/*.{,c,m}tsx",
7838
- ...componentExtensions.map((extension) => `**/*.${extension}`)
7839
- ];
8228
+ const jsFiles = oxfmtFiles?.flat() ?? [...FORMATTED_FILES.javascript];
8229
+ const tsFiles = oxfmtFiles?.flat() ?? [...FORMATTED_FILES.typescript, ...componentExtensions.map((extension) => `**/*.${extension}`)];
7840
8230
  function createFragments(name, files) {
7841
8231
  return [{
7842
8232
  name,
@@ -7849,10 +8239,13 @@ function oxlintOxfmt(options) {
7849
8239
  }, {
7850
8240
  name: `${name}/js-plugin`,
7851
8241
  files,
7852
- jsPlugins: [...jsPlugins, oxfmtPlugin],
8242
+ jsPlugins: standalone ? jsPlugins : [...jsPlugins, oxfmtPlugin],
7853
8243
  rules: {
7854
8244
  ...jsPluginRules,
7855
- "oxfmt/oxfmt": ["error", oxfmtOptions]
8245
+ ...standalone ? {} : { "oxfmt/oxfmt": ["error", {
8246
+ ...oxfmtOptions,
8247
+ disableNestedConfig: true
8248
+ }] }
7856
8249
  }
7857
8250
  }];
7858
8251
  }
@@ -9420,7 +9813,7 @@ const NATIVE_ONLY_JS_PLUGINS = /* @__PURE__ */ new Set(["oxlint-comments"]);
9420
9813
  */
9421
9814
  function isentinel(factoryOptions, ...userConfigs) {
9422
9815
  const options = factoryOptions ?? { name: "isentinel" };
9423
- const { antiSlop = false, 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;
9816
+ const { antiSlop = false, categories, componentExts: componentExtensions = [], e18e: enableE18e = true, env, eslintPlugin: enableEslintPlugin = false, formatter = "eslint", 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;
9424
9817
  const rootGlobs = mergeGlobs(GLOB_ROOT, customRootGlobs);
9425
9818
  const enableRoblox = options.roblox !== false;
9426
9819
  const reactOptions = resolveSubOptions(options, "react");
@@ -9441,14 +9834,22 @@ function isentinel(factoryOptions, ...userConfigs) {
9441
9834
  if (options.type !== void 0) return options.type;
9442
9835
  return enableRoblox ? "game" : "package";
9443
9836
  })();
9837
+ if (formatter === "standalone") warnStandaloneOptions(options);
9838
+ const formatterOptions = formatter !== "standalone" && typeof formatters === "object" ? formatters : {};
9839
+ const oxfmtFormats = formatter === "standalone" || formatters !== false && options.stylistic !== false;
9840
+ const prettierSettings = resolvePrettierSettings(formatterOptions.prettierOptions);
9841
+ const oxfmtOptions = buildOxfmtOptions({
9842
+ oxfmtConfigOptions: oxfmtFormats ? resolveOxfmtConfigOptionsSync() : {},
9843
+ oxfmtOptions: formatterOptions.oxfmtOptions,
9844
+ prettierOptions: prettierSettings
9845
+ });
9444
9846
  const stylisticOptions = (() => {
9445
9847
  if (options.stylistic === false) return false;
9446
9848
  if (typeof options.stylistic === "object") return options.stylistic;
9447
9849
  return {};
9448
9850
  })();
9449
9851
  if (stylisticOptions !== false && !("jsx" in stylisticOptions)) stylisticOptions.jsx = enableJsx;
9450
- const formatterOptions = typeof formatters === "object" ? formatters : {};
9451
- const prettierSettings = resolvePrettierSettings(formatterOptions.prettierOptions);
9852
+ const widthSettings = withOxfmtWidths(prettierSettings, oxfmtOptions);
9452
9853
  const configs = [
9453
9854
  oxlintJavascript({
9454
9855
  ...getOverrides(options, "javascript"),
@@ -9540,7 +9941,7 @@ function isentinel(factoryOptions, ...userConfigs) {
9540
9941
  if (stylisticOptions !== false) configs.push(oxlintPerfectionist({
9541
9942
  ...resolveSubOptions(options, "perfectionist"),
9542
9943
  type: projectType
9543
- }), oxlintStylistic(stylisticOptions));
9944
+ }), oxlintStylistic(oxfmtFormats ? withOxfmtStyle(stylisticOptions, oxfmtOptions) : stylisticOptions));
9544
9945
  if (enableReact !== false) configs.push(oxlintReact({
9545
9946
  roblox: enableRoblox,
9546
9947
  settings: options.settings,
@@ -9569,30 +9970,24 @@ function isentinel(factoryOptions, ...userConfigs) {
9569
9970
  antiSlop,
9570
9971
  roblox: enableRoblox,
9571
9972
  stylistic: stylisticOptions
9572
- }, prettierSettings), ...needsComplementOverlay ? [oxlintFlawless({
9973
+ }, widthSettings), ...needsComplementOverlay ? [oxlintFlawless({
9573
9974
  antiSlop,
9574
9975
  excludeFiles: robloxScopedFiles,
9575
9976
  roblox: false,
9576
9977
  stylistic: stylisticOptions
9577
- }, prettierSettings)] : [], oxlintComments({
9578
- prettierOptions: prettierSettings,
9978
+ }, widthSettings)] : [], oxlintComments({
9979
+ prettierOptions: widthSettings,
9579
9980
  stylistic: stylisticOptions
9580
9981
  }), oxlintDisables({
9581
9982
  configDirectory: options.configDirectory,
9582
9983
  root: rootGlobs,
9583
9984
  workspaceRoot: findWorkspaceRootSync()
9584
9985
  }));
9585
- if (stylisticOptions !== false && formatters !== false) {
9586
- const oxfmtConfigOptions = resolveOxfmtConfigOptionsSync();
9587
- configs.push(oxlintOxfmt({
9588
- componentExts: componentExtensions,
9589
- oxfmtOptions: buildOxfmtOptions({
9590
- oxfmtConfigOptions,
9591
- oxfmtOptions: formatterOptions.oxfmtOptions,
9592
- prettierOptions: prettierSettings
9593
- })
9594
- }));
9595
- }
9986
+ if (stylisticOptions !== false && formatters !== false) configs.push(oxlintOxfmt({
9987
+ componentExts: componentExtensions,
9988
+ oxfmtOptions,
9989
+ standalone: formatter === "standalone"
9990
+ }));
9596
9991
  const jsPlugins = /* @__PURE__ */ new Map();
9597
9992
  const nativePlugins = /* @__PURE__ */ new Set();
9598
9993
  const globalPlugins = /* @__PURE__ */ new Set();