@isentinel/eslint-config 6.1.0 → 6.3.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/index.mjs CHANGED
@@ -8,6 +8,7 @@ import process from "node:process";
8
8
  import { getStaticJSONValue, parseJSON } from "jsonc-eslint-parser";
9
9
  import * as yamlESLintParser from "yaml-eslint-parser";
10
10
  import { VisitorKeys, getStaticYAMLValue, parseForESLint, parseYAML, traverseNodes } from "yaml-eslint-parser";
11
+ import { isMainThread } from "node:worker_threads";
11
12
  import { minVersion } from "semver";
12
13
  import fs$1 from "node:fs/promises";
13
14
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -485,6 +486,79 @@ function resolvePrettierSettings(prettierOptions = {}, cwd = process.cwd()) {
485
486
  ...prettierOptions
486
487
  };
487
488
  }
489
+ /**
490
+ * Replace `printWidth` and `tabWidth` in the formatting settings with the
491
+ * values oxfmt really uses.
492
+ *
493
+ * `flawless/arrow-return-style` hands `printWidth` to oxfmt to predict its
494
+ * output (`useOxfmt.printWidth`), so it must match the width oxfmt formats
495
+ * with, or `--fix` ping-pongs. An oxfmt config can set a width that differs
496
+ * from the Prettier-shaped settings, so the oxfmt value wins.
497
+ *
498
+ * @template T - The settings shape.
499
+ * @param settings - The resolved formatting settings.
500
+ * @param oxfmtOptions - The effective oxfmt options.
501
+ * @returns The settings with oxfmt's widths.
502
+ */
503
+ function withOxfmtWidths(settings, { printWidth, tabWidth }) {
504
+ return {
505
+ ...settings,
506
+ ...typeof printWidth === "number" ? { printWidth } : {},
507
+ ...typeof tabWidth === "number" ? { tabWidth } : {}
508
+ };
509
+ }
510
+ /**
511
+ * The indent oxfmt writes.
512
+ *
513
+ * @param oxfmtOptions - The effective oxfmt options.
514
+ * @returns `"tab"`, or the number of spaces.
515
+ */
516
+ function oxfmtIndent({ tabWidth, useTabs }) {
517
+ if (useTabs !== false) return "tab";
518
+ return typeof tabWidth === "number" ? tabWidth : 2;
519
+ }
520
+ /**
521
+ * Default the stylistic `indent` and `quotes` to what oxfmt writes, so the
522
+ * stylistic rules and oxfmt agree. Explicit `stylistic` values win.
523
+ *
524
+ * @param stylistic - The user's stylistic options.
525
+ * @param oxfmtOptions - The effective oxfmt options.
526
+ * @returns The stylistic options with oxfmt's indent and quotes.
527
+ */
528
+ function withOxfmtStyle(stylistic, oxfmtOptions) {
529
+ return {
530
+ indent: oxfmtIndent(oxfmtOptions),
531
+ quotes: oxfmtOptions["singleQuote"] === true ? "single" : "double",
532
+ ...stylistic
533
+ };
534
+ }
535
+ /**
536
+ * Turn the nearest `.editorconfig`'s sections into oxfmt overrides, the way
537
+ * the `oxfmt/oxfmt` rule's config loader does: `[*]` is skipped (it feeds the
538
+ * root options), and each other section becomes an override whose globs are
539
+ * relative to `cwd`. A key set in the oxfmt config root wins over the
540
+ * sections.
541
+ *
542
+ * @param cwd - The config directory.
543
+ * @param root - The oxfmt config root options.
544
+ * @returns The overrides, in file order.
545
+ */
546
+ function editorConfigOverrides(cwd = process.cwd(), root = {}) {
547
+ for (const directory of ancestorDirectories(cwd)) {
548
+ const contents = readFileOrUndefined(path.join(directory, ".editorconfig"));
549
+ if (contents === void 0) continue;
550
+ const prefix = path.relative(cwd, directory).split(path.sep).join("/");
551
+ return parseEditorConfig(contents).sections.flatMap(({ pattern, properties }) => {
552
+ const options = Object.fromEntries(Object.entries(editorConfigToOxfmt(properties)).filter(([key]) => !Object.hasOwn(root, key)));
553
+ if (pattern === "*" || Object.keys(options).length === 0) return [];
554
+ return [{
555
+ files: [prefix === "" ? pattern : `${prefix}/${pattern.replace(LEADING_SLASH, "")}`],
556
+ options
557
+ }];
558
+ });
559
+ }
560
+ return [];
561
+ }
488
562
  function readFileOrUndefined(filePath) {
489
563
  try {
490
564
  return fs.readFileSync(filePath, "utf-8");
@@ -664,6 +738,31 @@ function resolveEditorConfigOptions(cwd) {
664
738
  }
665
739
  return editorConfigToPrettier(properties);
666
740
  }
741
+ /**
742
+ * Map one `.editorconfig` section to oxfmt options, as the rule's config
743
+ * loader does.
744
+ *
745
+ * @param properties - The section's properties.
746
+ * @returns The oxfmt options it sets.
747
+ */
748
+ function editorConfigToOxfmt(properties) {
749
+ const options = {};
750
+ const endOfLine = properties["end_of_line"];
751
+ if (endOfLine === "cr" || endOfLine === "crlf" || endOfLine === "lf") options["endOfLine"] = endOfLine;
752
+ const indentStyle = properties["indent_style"];
753
+ if (indentStyle === "tab" || indentStyle === "space") options["useTabs"] = indentStyle === "tab";
754
+ const quoteType = properties["quote_type"];
755
+ if (quoteType === "single" || quoteType === "double") options["singleQuote"] = quoteType === "single";
756
+ const indentSize = Number(properties["indent_size"]);
757
+ const tabWidth = Number(properties["tab_width"]);
758
+ if (options["useTabs"] === false && properties["indent_size"] && Number.isFinite(indentSize)) options["tabWidth"] = indentSize;
759
+ else if (properties["tab_width"] && Number.isFinite(tabWidth)) options["tabWidth"] = tabWidth;
760
+ const printWidth = Number(properties["max_line_length"]);
761
+ if (properties["max_line_length"] && Number.isFinite(printWidth)) options["printWidth"] = printWidth;
762
+ const finalNewline = properties["insert_final_newline"];
763
+ if (finalNewline === "true" || finalNewline === "false") options["insertFinalNewline"] = finalNewline === "true";
764
+ return options;
765
+ }
667
766
  //#endregion
668
767
  //#region src/guards.ts
669
768
  /**
@@ -681,6 +780,141 @@ function resolveEditorConfigOptions(cwd) {
681
780
  function isRecord(value) {
682
781
  return typeof value === "object" && value !== null && !Array.isArray(value);
683
782
  }
783
+ /**
784
+ * Whether a value is an array whose every element is a string.
785
+ *
786
+ * @param value - The value to test.
787
+ * @returns Whether the value is a `string` array.
788
+ */
789
+ function isStringArray(value) {
790
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
791
+ }
792
+ //#endregion
793
+ //#region src/rules/oxfmt.ts
794
+ const UNSUPPORTED_PRETTIER_KEYS = /* @__PURE__ */ new Set([
795
+ "experimentalOperatorPosition",
796
+ "experimentalTernaries",
797
+ "jsdocPreferCodeFences",
798
+ "jsdocPrintWidth",
799
+ "parser",
800
+ "plugins",
801
+ "tsdoc"
802
+ ]);
803
+ const defaultSortImports = {
804
+ customGroups: [{
805
+ elementNamePattern: ["react"],
806
+ groupName: "react"
807
+ }, {
808
+ elementNamePattern: ["@*/**"],
809
+ groupName: "scoped"
810
+ }],
811
+ groups: [
812
+ "react",
813
+ "scoped",
814
+ [
815
+ "type-builtin",
816
+ "type-external",
817
+ "builtin",
818
+ "external"
819
+ ],
820
+ [
821
+ "type-internal",
822
+ "internal",
823
+ "type-parent",
824
+ "type-sibling",
825
+ "type-index",
826
+ "parent",
827
+ "sibling",
828
+ "index"
829
+ ],
830
+ "unknown"
831
+ ],
832
+ newlinesBetween: true
833
+ };
834
+ /**
835
+ * Migrate Prettier options to oxfmt options, dropping unsupported keys.
836
+ *
837
+ * @param prettierOptions - The Prettier options to migrate.
838
+ * @returns The migrated oxfmt options.
839
+ */
840
+ function migratePrettierOptions(prettierOptions) {
841
+ const oxfmtOptions = {};
842
+ for (const [key, value] of Object.entries(prettierOptions)) {
843
+ if (UNSUPPORTED_PRETTIER_KEYS.has(key)) continue;
844
+ if (key === "endOfLine" && value === "auto") continue;
845
+ oxfmtOptions[key] = key === "overrides" ? migratePrettierOverrides(value) : value;
846
+ }
847
+ return oxfmtOptions;
848
+ }
849
+ /**
850
+ * Migrate Prettier `overrides`: oxfmt wants `files` as an array, and the
851
+ * nested options migrated like the root ones.
852
+ *
853
+ * @param overrides - The `overrides` value, of unknown shape.
854
+ * @returns The overrides in oxfmt's shape; empty when not an array.
855
+ */
856
+ function migratePrettierOverrides(overrides) {
857
+ if (!Array.isArray(overrides)) return [];
858
+ return overrides.flatMap((override) => {
859
+ if (!isRecord(override)) return [];
860
+ const { excludeFiles, files, options } = override;
861
+ return [{
862
+ ...excludeFiles === void 0 ? {} : { excludeFiles: stringsOf(excludeFiles) },
863
+ files: stringsOf(files),
864
+ options: isRecord(options) ? migratePrettierOptions(options) : {}
865
+ }];
866
+ });
867
+ }
868
+ /**
869
+ * Build the effective oxfmt options shared between the ESLint and oxlint
870
+ * factories.
871
+ *
872
+ * @param options - The option sources, in increasing precedence.
873
+ * @returns The effective oxfmt options.
874
+ */
875
+ function buildOxfmtOptions({ oxfmtConfigOptions = {}, oxfmtOptions, prettierOptions = {} }) {
876
+ return {
877
+ sortImports: defaultSortImports,
878
+ sortPackageJson: false,
879
+ ...migratePrettierOptions(prettierOptions),
880
+ ...oxfmtConfigOptions,
881
+ ...oxfmtOptions
882
+ };
883
+ }
884
+ /**
885
+ * Markdown formatting options layered over the base options. Shared by the
886
+ * ESLint config and the standalone oxfmt config so both format Markdown the
887
+ * same way.
888
+ *
889
+ * Oxfmt would format code blocks with these options too, at the prose width.
890
+ * It leaves them alone instead, and ESLint formats each block with the options
891
+ * of its own language (see `isentinel/oxfmt/markdown-code/*`).
892
+ *
893
+ * @param jsdocPrintWidth - Width for Markdown prose; 80 when unset.
894
+ * @returns Options applied on top of the base for Markdown files.
895
+ */
896
+ function markdownOxfmtOptions(jsdocPrintWidth) {
897
+ return {
898
+ embeddedLanguageFormatting: "off",
899
+ printWidth: Number(jsdocPrintWidth) || 80,
900
+ proseWrap: "always",
901
+ tabWidth: 2
902
+ };
903
+ }
904
+ function stringsOf(value) {
905
+ return [value].flat().filter((item) => typeof item === "string");
906
+ }
907
+ /**
908
+ * JSON5 formatting options layered over the base options. `jsonc/quote-props`
909
+ * owns the quoting of JSON5 keys; oxfmt's `consistent` would strip the quotes
910
+ * it adds, and `--fix` would loop.
911
+ */
912
+ const JSON5_OXFMT_OPTIONS = { quoteProps: "preserve" };
913
+ /** YAML formatting options layered over the base options. */
914
+ const YAML_OXFMT_OPTIONS = {
915
+ tabWidth: 2,
916
+ useTabs: false
917
+ };
684
918
  //#endregion
685
919
  //#region src/rules/react.ts
686
920
  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.";
@@ -1266,26 +1500,181 @@ function environmentMatcher(name, pattern, { noTty = false } = {}) {
1266
1500
  return value !== void 0 && pattern.test(value);
1267
1501
  };
1268
1502
  }
1269
- const OXFMT_CONFIG_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
1503
+ /** JSON oxfmt config files oxfmt discovers. */
1504
+ const OXFMT_RC_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
1505
+ /**
1506
+ * Oxfmt config modules oxfmt discovers. It loads other module extensions only
1507
+ * through `--config`, never by search.
1508
+ */
1509
+ const OXFMT_CONFIG_MODULES = ["oxfmt.config.ts", "oxfmt.config.mts"];
1510
+ OXFMT_CONFIG_MODULES[0];
1511
+ /** Every file name oxfmt discovers. One directory may hold only one of them. */
1512
+ const OXFMT_CONFIG_NAMES = [...OXFMT_RC_FILES, ...OXFMT_CONFIG_MODULES];
1513
+ /** The extensions of the JS/TS files ESLint and oxlint format. */
1514
+ const SCRIPT_EXTENSIONS = [
1515
+ "cjs",
1516
+ "cts",
1517
+ "js",
1518
+ "jsx",
1519
+ "mjs",
1520
+ "mts",
1521
+ "ts",
1522
+ "tsx"
1523
+ ];
1524
+ const JSON_EXTENSIONS = [
1525
+ "json",
1526
+ "json5",
1527
+ "jsonc"
1528
+ ];
1529
+ const LEADING_DOT_SLASH = /^\.\//;
1530
+ /** Set once a process has warned about a JS/TS width override. */
1531
+ const WARNED_SCRIPT_OVERRIDE = Symbol.for("@isentinel/eslint-config/oxfmt-override-warned");
1532
+ /** Set once a process has warned about options standalone oxfmt ignores. */
1533
+ const WARNED_STANDALONE = Symbol.for("@isentinel/eslint-config/standalone-options-warned");
1534
+ const GLOB_MAGIC = /[*?[\]{}]/;
1270
1535
  /**
1271
- * Resolve oxfmt configuration from `.oxfmtrc.json` or `.oxfmtrc.jsonc`.
1536
+ * List the oxfmt config files in one directory, without walking up.
1272
1537
  *
1273
- * @returns The oxfmt configuration options, or an empty object if none found.
1538
+ * @param directory - Directory to check.
1539
+ * @returns The file names found, sorted.
1274
1540
  */
1275
- async function resolveOxfmtConfigOptions() {
1276
- for (const filename of OXFMT_CONFIG_FILES) {
1277
- const configPath = path.resolve(process.cwd(), filename);
1278
- try {
1279
- const content = await fs.promises.readFile(configPath, "utf-8");
1280
- const parsed = JSON.parse(content);
1281
- if (!isRecord(parsed)) continue;
1282
- const { $schema: _, ...config } = parsed;
1283
- return config;
1284
- } catch {
1285
- continue;
1286
- }
1541
+ function oxfmtConfigNamesIn(directory) {
1542
+ return OXFMT_CONFIG_NAMES.filter((name) => fs.existsSync(path.join(directory, name))).sort();
1543
+ }
1544
+ /**
1545
+ * Find the oxfmt config the way oxfmt does: walk up from `cwd` to the
1546
+ * filesystem root and take the first directory that holds one. Like oxfmt,
1547
+ * refuse a directory that holds more than one.
1548
+ *
1549
+ * @param cwd - Directory to start from.
1550
+ * @returns The config file, if any.
1551
+ * @throws When one directory holds more than one oxfmt config file.
1552
+ */
1553
+ function findOxfmtConfig(cwd = process.cwd()) {
1554
+ let directory = path.resolve(cwd);
1555
+ for (;;) {
1556
+ const [first, second] = oxfmtConfigNamesIn(directory);
1557
+ if (second !== void 0) throw new Error(`[@isentinel/eslint-config] Both '${first}' and '${second}' found in ${directory}. Keep one oxfmt config file.`);
1558
+ if (first !== void 0) return {
1559
+ isModule: OXFMT_CONFIG_MODULES.some((name) => name === first),
1560
+ path: path.join(directory, first)
1561
+ };
1562
+ const parent = path.dirname(directory);
1563
+ if (parent === directory) return;
1564
+ directory = parent;
1565
+ }
1566
+ }
1567
+ /**
1568
+ * The string globs of an override's `files` or `excludeFiles`.
1569
+ *
1570
+ * @param value - One glob or a list of them, of unknown shape.
1571
+ * @returns The string globs.
1572
+ */
1573
+ function globsOf(value) {
1574
+ return [value].flat().filter((glob) => typeof glob === "string");
1575
+ }
1576
+ /**
1577
+ * Where an override applies: its `files`, minus its `excludeFiles` and any
1578
+ * negated glob in `files`.
1579
+ *
1580
+ * @param override - One override.
1581
+ * @returns Its scope.
1582
+ */
1583
+ function scopeOf(override) {
1584
+ const globs = globsOf(override["files"]);
1585
+ return {
1586
+ excludeFiles: [...globsOf(override["excludeFiles"]), ...globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1))],
1587
+ files: globs.filter((glob) => !glob.startsWith("!"))
1588
+ };
1589
+ }
1590
+ /** Oxfmt options that change the layout the JSON rules check. */
1591
+ const JSON_LAYOUT_KEYS = [
1592
+ "bracketSpacing",
1593
+ "singleQuote",
1594
+ "tabWidth",
1595
+ "useTabs"
1596
+ ];
1597
+ /**
1598
+ * The overrides that change the layout of JSON files, where the JSON layout
1599
+ * rules, set from the root options, would disagree with oxfmt.
1600
+ *
1601
+ * @param overrides - Overrides, of unknown shape.
1602
+ * @returns Each such override's scope, in order.
1603
+ */
1604
+ function jsonLayoutOverrides(overrides) {
1605
+ return overrides.flatMap((override) => {
1606
+ if (!isRecord(override) || !isRecord(override["options"]) || !overrideMatches(override, JSON_EXTENSIONS)) return [];
1607
+ const { options } = override;
1608
+ return JSON_LAYOUT_KEYS.some((key) => key in options) ? [scopeOf(override)] : [];
1609
+ });
1610
+ }
1611
+ /**
1612
+ * Warn, at most once per process, about factory options that do not reach
1613
+ * standalone oxfmt: it formats with `oxfmt.config.ts` alone. They still choose
1614
+ * which Markdown code blocks ESLint formats.
1615
+ *
1616
+ * @param options - The factory options.
1617
+ * @param options.componentExts - Oxfmt formats only the extensions
1618
+ * `isentinelOxfmt` is given.
1619
+ * @param options.formatters - Any value but `true` is ignored by oxfmt.
1620
+ * @param options.gitignore - Custom ignore files, or `false`, do not reach
1621
+ * oxfmt.
1622
+ * @param options.ignores - User ignores do not reach oxfmt.
1623
+ */
1624
+ function warnStandaloneOptions({ componentExts, formatters, gitignore, ignores }) {
1625
+ const state = globalThis;
1626
+ if (!isMainThread || state[WARNED_STANDALONE] === true || process.env["ISENTINEL_QUIET_CONFIG_WARNINGS"] === "1") return;
1627
+ const names = [
1628
+ formatters !== void 0 && formatters !== true ? "formatters" : void 0,
1629
+ Array.isArray(componentExts) && componentExts.length > 0 ? "componentExts" : void 0,
1630
+ ignores === void 0 || Array.isArray(ignores) && ignores.length === 0 ? void 0 : "ignores",
1631
+ gitignore === false || isRecord(gitignore) ? "gitignore" : void 0
1632
+ ].filter((name) => name !== void 0);
1633
+ if (names.length > 0) {
1634
+ state[WARNED_STANDALONE] = true;
1635
+ console.warn(`[@isentinel/eslint-config] With formatter: "standalone", oxfmt formats with oxfmt.config.ts alone, so these options do not change what oxfmt formats: ${names.join(", ")}. They change only which Markdown code blocks ESLint formats. Set the equivalent in isentinelOxfmt() (formatters, componentExts, ignorePatterns, format options).`);
1636
+ }
1637
+ }
1638
+ /**
1639
+ * Warn, at most once per process, when an oxfmt override changes `printWidth`
1640
+ * or `tabWidth` for JS/TS files: ESLint and oxlint use the root values.
1641
+ *
1642
+ * @param overrides - The config's `overrides`.
1643
+ * @param source - The config file name.
1644
+ */
1645
+ function warnScriptWidthOverrides(overrides, source) {
1646
+ const state = globalThis;
1647
+ if (state[WARNED_SCRIPT_OVERRIDE] === true || !Array.isArray(overrides)) return;
1648
+ if (overrides.some((override) => {
1649
+ if (!isRecord(override) || !isRecord(override["options"])) return false;
1650
+ const { printWidth, tabWidth } = override["options"];
1651
+ if (printWidth === void 0 && tabWidth === void 0) return false;
1652
+ return overrideMatches(override, SCRIPT_EXTENSIONS);
1653
+ })) {
1654
+ state[WARNED_SCRIPT_OVERRIDE] = true;
1655
+ 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.`);
1287
1656
  }
1288
- return {};
1657
+ }
1658
+ /**
1659
+ * Resolve oxfmt format options from the config oxfmt itself would use (see
1660
+ * {@link findOxfmtConfig}): `.oxfmtrc.json`, `.oxfmtrc.jsonc`,
1661
+ * `oxfmt.config.ts` or `oxfmt.config.mts`.
1662
+ *
1663
+ * @returns The format options and overrides; empty options if none found.
1664
+ * @rejects When the config is ambiguous or cannot be loaded.
1665
+ */
1666
+ async function resolveOxfmtConfig() {
1667
+ const config = findOxfmtConfig();
1668
+ if (config === void 0) return { options: {} };
1669
+ if (!config.isModule) return rcConfig(config.path, await fs.promises.readFile(config.path, "utf-8"));
1670
+ let loaded;
1671
+ try {
1672
+ const { createJiti } = await import("jiti");
1673
+ loaded = await createJiti(import.meta.url, { moduleCache: false }).import(config.path, { default: true });
1674
+ } catch (err) {
1675
+ throw loadError(config.path, err);
1676
+ }
1677
+ return moduleConfig(config.path, loaded);
1289
1678
  }
1290
1679
  /**
1291
1680
  * Override the severity of all rules in a rules object, preserving rule
@@ -1314,6 +1703,91 @@ function shouldEnableFeature(options, key, defaultValue = true) {
1314
1703
  return options[key] !== false;
1315
1704
  }
1316
1705
  /**
1706
+ * Whether a glob can match a file with one of `extensions`, judged by the
1707
+ * extension of its last segment (`*.ts`, `*.{ts,tsx}`, `*.?ts`, `*`, ...).
1708
+ *
1709
+ * @param glob - An override `files` glob.
1710
+ * @param extensions - The extensions to look for.
1711
+ * @returns Whether it can match such a file.
1712
+ */
1713
+ function globMatchesExtension(glob, extensions) {
1714
+ const base = glob.split("/").at(-1) ?? glob;
1715
+ const dot = base.lastIndexOf(".");
1716
+ if (dot === -1) return base.includes("*");
1717
+ const extension = base.slice(dot + 1);
1718
+ if (!GLOB_MAGIC.test(extension)) return extensions.includes(extension);
1719
+ return extensions.some((candidate) => path.matchesGlob(candidate, extension));
1720
+ }
1721
+ /**
1722
+ * Whether an oxfmt override can apply to a file with one of `extensions`.
1723
+ *
1724
+ * @param override - One entry of the config's `overrides`.
1725
+ * @param extensions - The extensions to look for.
1726
+ * @returns Whether its `files` can match such a file.
1727
+ */
1728
+ function overrideMatches(override, extensions) {
1729
+ return (Array.isArray(override["files"]) ? override["files"] : [override["files"]]).some((glob) => typeof glob === "string" && globMatchesExtension(glob, extensions));
1730
+ }
1731
+ function loadError(configPath, error) {
1732
+ return new Error(`[@isentinel/eslint-config] Failed to load ${path.basename(configPath)}: ${String(error)}`, { cause: error });
1733
+ }
1734
+ /**
1735
+ * Whether a config override is an `.editorconfig` section that
1736
+ * `isentinelOxfmt` copied in: the same globs (anchored or not) and a subset
1737
+ * of its options.
1738
+ *
1739
+ * @param override - One config override.
1740
+ * @param sections - The `.editorconfig` sections, as overrides.
1741
+ * @returns Whether it mirrors one of them.
1742
+ */
1743
+ function mirrorsSection(override, sections) {
1744
+ if (!isRecord(override) || !isRecord(override["options"])) return false;
1745
+ const { options } = override;
1746
+ const files = [override["files"]].flat().map((glob) => typeof glob === "string" ? glob.replace(LEADING_DOT_SLASH, "") : glob);
1747
+ return sections.some((section) => {
1748
+ return section.files.length === files.length && section.files.every((glob, index) => glob === files[index]) && Object.entries(options).every(([key, value]) => section.options[key] === value);
1749
+ });
1750
+ }
1751
+ /**
1752
+ * Turn a loaded oxfmt config into `oxfmt/oxfmt` rule options, its ignore
1753
+ * patterns and its overrides. The rule takes format options only, so
1754
+ * `$schema`, `ignorePatterns` and `overrides` are dropped from the options.
1755
+ *
1756
+ * @param config - The loaded config.
1757
+ * @param config.$schema - Dropped.
1758
+ * @param config.ignorePatterns - Dropped from the options; oxfmt applies them
1759
+ * itself, except in Markdown code blocks.
1760
+ * @param config.overrides - Dropped from the options, after the width check.
1761
+ * @param configPath - The config file, for the warning.
1762
+ * @returns The format options, ignore patterns and overrides.
1763
+ */
1764
+ function toResolvedConfig({ $schema: _schema, ignorePatterns, overrides, ...options }, configPath) {
1765
+ const sections = editorConfigOverrides(path.dirname(configPath));
1766
+ const list = Array.isArray(overrides) ? overrides : [];
1767
+ const copied = list.filter((override) => mirrorsSection(override, sections));
1768
+ warnScriptWidthOverrides(copied, ".editorconfig");
1769
+ warnScriptWidthOverrides(list.filter((override) => !copied.includes(override)), path.basename(configPath));
1770
+ return {
1771
+ ignorePatterns: isStringArray(ignorePatterns) ? ignorePatterns : [],
1772
+ options,
1773
+ overrides: Array.isArray(overrides) ? overrides : []
1774
+ };
1775
+ }
1776
+ function moduleConfig(configPath, loaded) {
1777
+ if (!isRecord(loaded)) throw loadError(configPath, /* @__PURE__ */ new TypeError("the default export is not an object"));
1778
+ return toResolvedConfig(loaded, configPath);
1779
+ }
1780
+ function rcConfig(configPath, content) {
1781
+ let parsed;
1782
+ try {
1783
+ parsed = getStaticJSONValue(parseJSON(content, { jsonSyntax: "jsonc" }));
1784
+ } catch (err) {
1785
+ throw loadError(configPath, err);
1786
+ }
1787
+ if (!isRecord(parsed)) throw loadError(configPath, /* @__PURE__ */ new TypeError("the config is not an object"));
1788
+ return toResolvedConfig(parsed, configPath);
1789
+ }
1790
+ /**
1317
1791
  * Resolves the lowest Node major version a semver range can match.
1318
1792
  *
1319
1793
  * @param range - A semver range such as `>=24.12.0` or `^24 || ^26`.
@@ -1798,24 +2272,6 @@ async function eslintPlugin({ files = [GLOB_SRC], overrides = {} } = {}) {
1798
2272
  //#endregion
1799
2273
  //#region src/rules/flawless.ts
1800
2274
  /**
1801
- * Arrow return style rule on its own, for the Markdown code block override
1802
- * where only the line width differs.
1803
- *
1804
- * @param options - Shared rule options.
1805
- * @returns The rule map.
1806
- */
1807
- function arrowStyleRules({ maxLen, maxLength, printWidth, tabWidth } = {}) {
1808
- return { "flawless/arrow-return-style": ["error", {
1809
- jsxAlwaysUseExplicitReturn: true,
1810
- maxLen: maxLength ?? maxLen ?? printWidth ?? 100,
1811
- maxObjectProperties: 2,
1812
- namedExportsAlwaysUseExplicitReturn: true,
1813
- objectReturnStyle: "complex-explicit",
1814
- ...tabWidth !== void 0 ? { tabWidth } : {},
1815
- ...printWidth !== void 0 ? { useOxfmt: { printWidth } } : {}
1816
- }] };
1817
- }
1818
- /**
1819
2275
  * Base (non-React) flawless rules shared between the ESLint and oxlint
1820
2276
  * factories. The React flawless rules live in the react rule map; the
1821
2277
  * type-aware rules live in {@link flawlessTypeAwareRules}, plus the type-aware
@@ -1876,6 +2332,23 @@ function antiSlopRules({ roblox = true }) {
1876
2332
  };
1877
2333
  }
1878
2334
  /**
2335
+ * The arrow return style rule.
2336
+ *
2337
+ * @param options - Shared rule options.
2338
+ * @returns The rule map.
2339
+ */
2340
+ function arrowStyleRules({ maxLen, printWidth, tabWidth } = {}) {
2341
+ return { "flawless/arrow-return-style": ["error", {
2342
+ jsxAlwaysUseExplicitReturn: true,
2343
+ maxLen: maxLen ?? printWidth ?? 100,
2344
+ maxObjectProperties: 2,
2345
+ namedExportsAlwaysUseExplicitReturn: true,
2346
+ objectReturnStyle: "complex-explicit",
2347
+ ...tabWidth !== void 0 ? { tabWidth } : {},
2348
+ ...printWidth !== void 0 ? { useOxfmt: { printWidth } } : {}
2349
+ }] };
2350
+ }
2351
+ /**
1879
2352
  * Type-aware anti-slop rules.
1880
2353
  *
1881
2354
  * `flawless/no-reflect-set` is complement-only, like `flawless/no-reflect-get`.
@@ -1924,15 +2397,6 @@ async function flawless(options = {}, prettierOptions = {}) {
1924
2397
  roblox
1925
2398
  })
1926
2399
  },
1927
- ...stylistic !== false ? [{
1928
- name: "isentinel/flawless/markdown-code",
1929
- files: [GLOB_MARKDOWN_CODE],
1930
- rules: arrowStyleRules({
1931
- maxLength: Number(prettierOptions["jsdocPrintWidth"]) || 80,
1932
- printWidth,
1933
- tabWidth
1934
- })
1935
- }] : [],
1936
2400
  ...complementIgnores ? [{
1937
2401
  name: "isentinel/flawless/complement",
1938
2402
  files: [GLOB_SRC],
@@ -2596,13 +3060,13 @@ function jsdoc({ full = false, stylistic = true, type = "game" } = {}) {
2596
3060
  //#endregion
2597
3061
  //#region src/eslint/configs/jsonc.ts
2598
3062
  init_globs();
2599
- async function jsonc({ files = [
3063
+ async function jsonc({ bracketSpacing = true, files = [
2600
3064
  GLOB_JSON,
2601
3065
  GLOB_JSON5,
2602
3066
  GLOB_JSONC
2603
- ], overrides = {}, stylistic = true } = {}) {
3067
+ ], formatIndent, layoutOverrides = [], overrides = {}, singleQuote = false, stylistic = true } = {}) {
2604
3068
  const defaults = resolveWithDefaults(stylistic, {});
2605
- const { indent = "tab" } = defaults === false ? {} : defaults;
3069
+ const { indent = formatIndent ?? "tab" } = defaults === false ? {} : defaults;
2606
3070
  const indentValue = typeof indent === "number" || indent === "tab" ? indent : "tab";
2607
3071
  const pluginJsonc = lazyPlugin("eslint-plugin-jsonc");
2608
3072
  const parserJsonc = await interopDefault(import("jsonc-eslint-parser"));
@@ -2654,7 +3118,7 @@ async function jsonc({ files = [
2654
3118
  consistent: true,
2655
3119
  multiline: true
2656
3120
  }],
2657
- "jsonc/object-curly-spacing": ["error", "always"],
3121
+ "jsonc/object-curly-spacing": ["error", bracketSpacing ? "always" : "never"],
2658
3122
  "jsonc/object-property-newline": ["error", { allowMultiplePropertiesPerLine: true }],
2659
3123
  "jsonc/quote-props": "error",
2660
3124
  "jsonc/quotes": "error"
@@ -2662,6 +3126,23 @@ async function jsonc({ files = [
2662
3126
  ...overrides
2663
3127
  }
2664
3128
  },
3129
+ ...stylistic !== false && singleQuote ? [{
3130
+ name: "isentinel/jsonc/json5-quotes",
3131
+ files: [GLOB_JSON5],
3132
+ rules: { "jsonc/quotes": ["error", "single"] }
3133
+ }] : [],
3134
+ ...stylistic === false ? [] : layoutOverrides.map(({ excludeFiles, files: overrideFiles }, index) => {
3135
+ return {
3136
+ name: `isentinel/jsonc/oxfmt-override-${index}`,
3137
+ files: overrideFiles,
3138
+ ignores: excludeFiles,
3139
+ rules: {
3140
+ "jsonc/indent": "off",
3141
+ "jsonc/object-curly-spacing": "off",
3142
+ "jsonc/quotes": "off"
3143
+ }
3144
+ };
3145
+ }),
2665
3146
  {
2666
3147
  name: "isentinel/jsonc/tsconfig",
2667
3148
  files: [GLOB_TSCONFIG],
@@ -3350,76 +3831,182 @@ async function node({ files = [GLOB_SRC], ignores } = {}) {
3350
3831
  }];
3351
3832
  }
3352
3833
  //#endregion
3353
- //#region src/rules/oxfmt.ts
3354
- const UNSUPPORTED_PRETTIER_KEYS = /* @__PURE__ */ new Set([
3355
- "experimentalOperatorPosition",
3356
- "experimentalTernaries",
3357
- "jsdocPreferCodeFences",
3358
- "jsdocPrintWidth",
3359
- "parser",
3360
- "plugins",
3361
- "tsdoc"
3362
- ]);
3363
- const defaultSortImports = {
3364
- customGroups: [{
3365
- elementNamePattern: ["react"],
3366
- groupName: "react"
3367
- }, {
3368
- elementNamePattern: ["@*/**"],
3369
- groupName: "scoped"
3370
- }],
3371
- groups: [
3372
- "react",
3373
- "scoped",
3374
- [
3375
- "type-builtin",
3376
- "type-external",
3377
- "builtin",
3378
- "external"
3379
- ],
3380
- [
3381
- "type-internal",
3382
- "internal",
3383
- "type-parent",
3384
- "type-sibling",
3385
- "type-index",
3386
- "parent",
3387
- "sibling",
3388
- "index"
3389
- ],
3390
- "unknown"
3391
- ],
3392
- newlinesBetween: true
3834
+ //#region src/formatted-files.ts
3835
+ init_globs();
3836
+ /** Formatted globs, by `oxfmt/oxfmt` config block. */
3837
+ const FORMATTED_FILES = {
3838
+ css: [GLOB_CSS, GLOB_POSTCSS],
3839
+ graphql: ["**/*.graphql"],
3840
+ html: ["**/*.html"],
3841
+ javascript: [GLOB_JS, GLOB_JSX],
3842
+ json: [GLOB_ALL_JSON],
3843
+ less: [GLOB_LESS],
3844
+ markdown: [GLOB_MARKDOWN],
3845
+ scss: [GLOB_SCSS],
3846
+ typescript: [GLOB_TS, GLOB_TSX],
3847
+ yaml: [GLOB_YAML]
3393
3848
  };
3394
3849
  /**
3395
- * Migrate Prettier options to oxfmt options, dropping unsupported keys.
3850
+ * Resolve `formatters` to one flag per language.
3396
3851
  *
3397
- * @param prettierOptions - The Prettier options to migrate.
3398
- * @returns The migrated oxfmt options.
3852
+ * @param formatters - The `formatters` option.
3853
+ * @returns Whether each language is formatted.
3399
3854
  */
3400
- function migratePrettierOptions(prettierOptions) {
3401
- const oxfmtOptions = {};
3402
- for (const [key, value] of Object.entries(prettierOptions)) {
3403
- if (UNSUPPORTED_PRETTIER_KEYS.has(key)) continue;
3404
- if (key === "endOfLine" && value === "auto") continue;
3405
- oxfmtOptions[key] = value;
3855
+ function resolveFormattedLanguages(formatters = true) {
3856
+ const enabled = formatters !== false;
3857
+ const toggles = typeof formatters === "object" ? formatters : {};
3858
+ return {
3859
+ css: enabled && toggles.css !== false,
3860
+ graphql: enabled && toggles.graphql !== false,
3861
+ html: enabled && toggles.html !== false,
3862
+ json: enabled && toggles.json !== false,
3863
+ markdown: enabled && toggles.markdown !== false,
3864
+ yaml: enabled && toggles.yaml !== false
3865
+ };
3866
+ }
3867
+ //#endregion
3868
+ //#region src/eslint/oxfmt-markdown-code.ts
3869
+ /** As `GLOB_MARKDOWN`, case-sensitive. */
3870
+ const MARKDOWN_FILE = /\.md$/;
3871
+ /**
3872
+ * Wrap eslint-plugin-oxfmt so `oxfmt/oxfmt` formats Markdown code blocks.
3873
+ *
3874
+ * The rule skips every virtual file, so a code block is formatted only by
3875
+ * the formatter of the file it is in. Oxfmt formats Markdown code blocks at
3876
+ * the prose width, so the preset turns that off and lets the rule format each
3877
+ * block with the options of its own language. Virtual files of other
3878
+ * processors (Vue blocks, say) stay skipped: oxfmt formats those files whole.
3879
+ *
3880
+ * @param plugin - The loaded plugin.
3881
+ * @returns The plugin with the wrapped rule.
3882
+ */
3883
+ function oxfmtMarkdownCode(plugin) {
3884
+ const { rules } = plugin;
3885
+ const rule = rules?.["oxfmt"];
3886
+ if (rule === void 0) return plugin;
3887
+ return {
3888
+ ...plugin,
3889
+ rules: {
3890
+ ...rules,
3891
+ oxfmt: markdownCodeRule(rule)
3892
+ }
3893
+ };
3894
+ }
3895
+ /**
3896
+ * Whether a file is a code block of a Markdown file.
3897
+ *
3898
+ * @param context - The rule context.
3899
+ * @returns Whether the file is a Markdown code block.
3900
+ */
3901
+ function isMarkdownCodeBlock({ filename, physicalFilename }) {
3902
+ return filename !== physicalFilename && MARKDOWN_FILE.test(physicalFilename);
3903
+ }
3904
+ function markdownCodeRule(rule) {
3905
+ return {
3906
+ ...rule,
3907
+ create(context) {
3908
+ if (!isMarkdownCodeBlock(context)) return rule.create(context);
3909
+ const proxy = Object.create(context, { physicalFilename: { value: context.filename } });
3910
+ return rule.create(proxy);
3911
+ }
3912
+ };
3913
+ }
3914
+ //#endregion
3915
+ //#region src/eslint/oxfmt-single-fix.ts
3916
+ /** Records the edit a fix makes, instead of making it. */
3917
+ const recordingFixer = { replaceTextRange(range, text) {
3918
+ return {
3919
+ range,
3920
+ text
3921
+ };
3922
+ } };
3923
+ /**
3924
+ * Wrap eslint-plugin-oxfmt so `oxfmt/oxfmt` reports one fix per file.
3925
+ *
3926
+ * @param plugin - The loaded plugin.
3927
+ * @returns The plugin with the wrapped rule.
3928
+ */
3929
+ function oxfmtSingleFix(plugin) {
3930
+ const { rules } = plugin;
3931
+ const rule = rules?.["oxfmt"];
3932
+ if (rule === void 0) return plugin;
3933
+ return {
3934
+ ...plugin,
3935
+ rules: {
3936
+ ...rules,
3937
+ oxfmt: singleFixRule(rule)
3938
+ }
3939
+ };
3940
+ }
3941
+ function editOf({ fix }) {
3942
+ if (typeof fix !== "function") return;
3943
+ return fix(recordingFixer) ?? void 0;
3944
+ }
3945
+ /**
3946
+ * Merge non-overlapping edits into one that spans them all.
3947
+ *
3948
+ * @param source - The file text.
3949
+ * @param edits - Edits that do not overlap, in any order.
3950
+ * @returns One edit with the same effect.
3951
+ */
3952
+ function mergeEdits(source, edits) {
3953
+ const sorted = edits.toSorted((first, second) => first.range[0] - second.range[0]);
3954
+ const start = sorted[0].range[0];
3955
+ let end = start;
3956
+ let text = "";
3957
+ for (const { range, text: insert } of sorted) {
3958
+ text += source.slice(end, range[0]) + insert;
3959
+ [, end] = range;
3406
3960
  }
3407
- return oxfmtOptions;
3961
+ return {
3962
+ range: [start, end],
3963
+ text
3964
+ };
3408
3965
  }
3409
3966
  /**
3410
- * Build the effective oxfmt options shared between the ESLint and oxlint
3411
- * factories.
3967
+ * Wrap the `oxfmt/oxfmt` rule so one file's formatting is one fix.
3412
3968
  *
3413
- * @param options - The option sources, in increasing precedence.
3414
- * @returns The effective oxfmt options.
3969
+ * The rule reports one fix per diff hunk. ESLint skips a fix that overlaps
3970
+ * another rule's fix in the same pass but applies the rest, so a delete hunk
3971
+ * can be dropped while its insert hunk lands (a moved comment is then kept
3972
+ * twice). One fix applies whole or not at all.
3973
+ *
3974
+ * @param rule - The original rule.
3975
+ * @returns The wrapped rule.
3415
3976
  */
3416
- function buildOxfmtOptions({ oxfmtConfigOptions = {}, oxfmtOptions, prettierOptions = {} }) {
3977
+ function singleFixRule(rule) {
3417
3978
  return {
3418
- sortImports: defaultSortImports,
3419
- sortPackageJson: false,
3420
- ...migratePrettierOptions(prettierOptions),
3421
- ...oxfmtConfigOptions,
3422
- ...oxfmtOptions
3979
+ ...rule,
3980
+ create(context) {
3981
+ const reports = [];
3982
+ const proxy = Object.create(context, { report: { value: (descriptor) => {
3983
+ reports.push(descriptor);
3984
+ } } });
3985
+ function flush() {
3986
+ const edits = reports.map((descriptor) => editOf(descriptor));
3987
+ const fixable = edits.filter((edit) => edit !== void 0);
3988
+ const merged = fixable.length > 0 ? mergeEdits(context.sourceCode.text, fixable) : void 0;
3989
+ let fixAttached = false;
3990
+ for (const [index, descriptor] of reports.entries()) {
3991
+ const { fix: _fix, ...rest } = descriptor;
3992
+ if (merged !== void 0 && !fixAttached && edits[index] !== void 0) {
3993
+ fixAttached = true;
3994
+ context.report({
3995
+ ...rest,
3996
+ fix: (fixer) => fixer.replaceTextRange(merged.range, merged.text)
3997
+ });
3998
+ } else context.report(rest);
3999
+ }
4000
+ reports.length = 0;
4001
+ }
4002
+ const listeners = rule.create(proxy);
4003
+ return Object.fromEntries(Object.entries(listeners).map(([selector, listener]) => {
4004
+ return [selector, (...args) => {
4005
+ listener(...args);
4006
+ flush();
4007
+ }];
4008
+ }));
4009
+ }
3423
4010
  };
3424
4011
  }
3425
4012
  //#endregion
@@ -3438,22 +4025,49 @@ const defaultPluginRenaming = {
3438
4025
  //#region src/eslint/configs/oxfmt.ts
3439
4026
  init_globs();
3440
4027
  async function oxfmt(options) {
3441
- const { componentExts: componentExtensions = [], files: oxfmtFiles, formatters = {}, oxfmtConfigOptions = {}, oxfmtOptions: userOxfmtOptions, oxlint: enableOxlint = false, prettierOptions = {} } = options ?? {};
3442
- const formattingOptions = {
3443
- css: true,
3444
- graphql: true,
3445
- html: true,
3446
- json: true,
3447
- markdown: true,
3448
- yaml: true,
3449
- ...resolveWithDefaults(formatters, {})
3450
- };
4028
+ const { componentExts: componentExtensions = [], files: oxfmtFiles, formatters = {}, oxfmtConfigOptions = {}, oxfmtIgnorePatterns = [], oxfmtOptions: userOxfmtOptions, oxlint: enableOxlint = false, prettierOptions = {}, standalone = false } = options ?? {};
4029
+ const languages = resolveFormattedLanguages(formatters);
3451
4030
  const oxfmtOptions = buildOxfmtOptions({
3452
4031
  oxfmtConfigOptions,
3453
4032
  oxfmtOptions: userOxfmtOptions,
3454
4033
  prettierOptions
3455
4034
  });
3456
- const pluginOxfmt = lazyPlugin("eslint-plugin-oxfmt");
4035
+ /**
4036
+ * The `oxfmt/oxfmt` entry for a block, with an options layer over the
4037
+ * base; none when oxfmt runs on its own.
4038
+ *
4039
+ * @param layer - Options applied over the base options.
4040
+ * @returns The rule entry, or no rules.
4041
+ */
4042
+ function formatRule(layer = {}) {
4043
+ return standalone ? {} : { "oxfmt/oxfmt": ["error", {
4044
+ ...oxfmtOptions,
4045
+ ...layer,
4046
+ disableNestedConfig: true
4047
+ }] };
4048
+ }
4049
+ /**
4050
+ * The `oxfmt/oxfmt` entry for Markdown code blocks, in every mode: oxfmt
4051
+ * leaves them alone, and oxlint cannot read Markdown. A code block takes
4052
+ * the root options of its language only: the rule does not load the
4053
+ * config, so its overrides and the `.editorconfig` sections, which match
4054
+ * by path, do not reach the block. The config's ignore patterns still do.
4055
+ *
4056
+ * @param layer - Options applied over the base options.
4057
+ * @returns The rule entry.
4058
+ */
4059
+ function codeBlockRule(layer = {}) {
4060
+ return { "oxfmt/oxfmt": ["error", {
4061
+ ...oxfmtOptions,
4062
+ ...layer,
4063
+ ignorePatterns: oxfmtIgnorePatterns,
4064
+ overrides: [],
4065
+ useConfig: false
4066
+ }] };
4067
+ }
4068
+ const pluginOxfmt = lazyPlugin("eslint-plugin-oxfmt", (plugin) => {
4069
+ return oxfmtSingleFix(oxfmtMarkdownCode(plugin));
4070
+ });
3457
4071
  const configPrettier = await interopDefault(import("eslint-config-prettier/flat"));
3458
4072
  const rulesToIgnore = ["curly", "style/quotes"];
3459
4073
  const rules = renameRules(configPrettier.rules, defaultPluginRenaming);
@@ -3462,103 +4076,130 @@ async function oxfmt(options) {
3462
4076
  name: "isentinel/oxfmt/setup",
3463
4077
  plugins: { oxfmt: pluginOxfmt }
3464
4078
  }];
3465
- const jsFiles = oxfmtFiles ?? [
3466
- "**/*.{,c,m}js",
3467
- "**/*.{,c,m}jsx",
3468
- `**/*.md/**/*.{,c,m}js`,
3469
- `**/*.md/**/*.{,c,m}jsx`
3470
- ];
3471
- configs.push({
3472
- name: "isentinel/oxfmt/javascript",
3473
- files: jsFiles,
3474
- rules: {
3475
- ...rules,
3476
- "arrow-body-style": "off",
3477
- ...enableOxlint ? {} : { "oxfmt/oxfmt": ["error", oxfmtOptions] },
3478
- "prefer-arrow-callback": "off"
3479
- }
3480
- });
3481
- const tsFiles = oxfmtFiles ?? [
3482
- "**/*.{,c,m}ts",
3483
- "**/*.{,c,m}tsx",
3484
- `**/*.md/**/*.{,c,m}ts`,
3485
- `**/*.md/**/*.{,c,m}tsx`,
3486
- ...componentExtensions.map((extension) => `**/*.${extension}`)
3487
- ];
3488
- configs.push({
3489
- name: "isentinel/oxfmt/typescript",
3490
- files: tsFiles,
4079
+ const scriptBlocks = [["javascript", oxfmtFiles ?? [...FORMATTED_FILES.javascript]], ["typescript", oxfmtFiles ?? [...FORMATTED_FILES.typescript, ...componentExtensions.map((extension) => `**/*.${extension}`)]]];
4080
+ for (const [name, files] of scriptBlocks) configs.push({
4081
+ name: `isentinel/oxfmt/${name}`,
4082
+ files,
3491
4083
  rules: {
3492
4084
  ...rules,
3493
4085
  "arrow-body-style": "off",
3494
- ...enableOxlint ? {} : { "oxfmt/oxfmt": ["error", oxfmtOptions] },
4086
+ ...enableOxlint ? {} : formatRule(),
3495
4087
  "prefer-arrow-callback": "off"
3496
4088
  }
3497
4089
  });
3498
- if (enableOxlint) configs.push({
3499
- name: "isentinel/oxfmt/markdown-code",
3500
- files: [
3501
- `${GLOB_MARKDOWN}/${GLOB_JS}`,
3502
- `${GLOB_MARKDOWN}/${GLOB_JSX}`,
3503
- `${GLOB_MARKDOWN}/${GLOB_TS}`,
3504
- `${GLOB_MARKDOWN}/${GLOB_TSX}`
3505
- ],
3506
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
3507
- });
3508
- if (formattingOptions.css) configs.push({
3509
- name: "isentinel/oxfmt/css",
3510
- files: [GLOB_CSS, GLOB_POSTCSS],
3511
- languageOptions: { parser: parserPlain },
3512
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
3513
- }, {
3514
- name: "isentinel/oxfmt/scss",
3515
- files: [GLOB_SCSS],
3516
- languageOptions: { parser: parserPlain },
3517
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
3518
- }, {
3519
- name: "isentinel/oxfmt/less",
3520
- files: [GLOB_LESS],
3521
- languageOptions: { parser: parserPlain },
3522
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
3523
- });
3524
- if (formattingOptions.html) configs.push({
3525
- name: "isentinel/oxfmt/html",
3526
- files: ["**/*.html"],
3527
- languageOptions: { parser: parserPlain },
3528
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
3529
- });
3530
- if (formattingOptions.markdown) configs.push({
4090
+ if (languages.css) configs.push(plainBlock("css", [...FORMATTED_FILES.css], formatRule()), plainBlock("scss", [...FORMATTED_FILES.scss], formatRule()), plainBlock("less", [...FORMATTED_FILES.less], formatRule()));
4091
+ if (languages.html) configs.push(plainBlock("html", [...FORMATTED_FILES.html], formatRule()));
4092
+ if (!standalone && languages.markdown) configs.push({
3531
4093
  name: "isentinel/oxfmt/markdown",
3532
- files: [GLOB_MARKDOWN],
3533
- rules: { "oxfmt/oxfmt": ["error", {
3534
- ...oxfmtOptions,
3535
- printWidth: Number(prettierOptions["jsdocPrintWidth"]) || 80,
3536
- proseWrap: "always",
3537
- tabWidth: 2
3538
- }] }
4094
+ files: [...FORMATTED_FILES.markdown],
4095
+ rules: formatRule(markdownOxfmtOptions(prettierOptions["jsdocPrintWidth"]))
3539
4096
  });
3540
- if (formattingOptions.graphql) configs.push({
3541
- name: "isentinel/oxfmt/graphql",
3542
- files: ["**/*.graphql"],
3543
- languageOptions: { parser: parserPlain },
3544
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
3545
- });
3546
- if (formattingOptions.json) configs.push({
4097
+ if (languages.graphql) configs.push(plainBlock("graphql", [...FORMATTED_FILES.graphql], formatRule()));
4098
+ if (!standalone && languages.json) configs.push({
3547
4099
  name: "isentinel/oxfmt/json",
3548
- files: [GLOB_ALL_JSON],
3549
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
4100
+ files: [...FORMATTED_FILES.json],
4101
+ rules: formatRule()
4102
+ }, {
4103
+ name: "isentinel/oxfmt/json5",
4104
+ files: [GLOB_JSON5],
4105
+ rules: formatRule(JSON5_OXFMT_OPTIONS)
3550
4106
  });
3551
- if (formattingOptions.yaml) configs.push({
4107
+ if (!standalone && languages.yaml) configs.push({
3552
4108
  name: "isentinel/oxfmt/yaml",
3553
- files: [GLOB_YAML],
3554
- rules: { "oxfmt/oxfmt": ["error", {
3555
- ...oxfmtOptions,
3556
- tabWidth: 2,
3557
- useTabs: false
3558
- }] }
4109
+ files: [...FORMATTED_FILES.yaml],
4110
+ rules: formatRule(YAML_OXFMT_OPTIONS)
4111
+ });
4112
+ const codeBlocks = markdownCodeBlocks(languages, componentExtensions).map(([name, files, layer]) => {
4113
+ return {
4114
+ name: `isentinel/oxfmt/markdown-code/${name}`,
4115
+ files: files.map((glob) => `${GLOB_MARKDOWN}/${glob}`),
4116
+ rules: codeBlockRule(layer)
4117
+ };
4118
+ });
4119
+ if (languages.markdown) configs.push(...codeBlocks);
4120
+ else configs.push({
4121
+ name: "isentinel/oxfmt/markdown-code",
4122
+ files: codeBlocks.flatMap((block) => block.files),
4123
+ rules: { "oxfmt/oxfmt": "off" }
3559
4124
  });
3560
4125
  return configs;
3561
4126
  }
4127
+ /**
4128
+ * The code-block languages to format, with the options each one uses: the
4129
+ * same as a file of that language. Markdown blocks are left out: a config
4130
+ * that matches their virtual `*.md/*.md` files makes ESLint fail.
4131
+ *
4132
+ * @param languages - Which languages are formatted.
4133
+ * @param componentExtensions - Component extensions formatted as TypeScript.
4134
+ * @returns The code-block languages.
4135
+ */
4136
+ function markdownCodeBlocks(languages, componentExtensions) {
4137
+ const blocks = [[
4138
+ "javascript",
4139
+ [...FORMATTED_FILES.javascript],
4140
+ {}
4141
+ ], [
4142
+ "typescript",
4143
+ [...FORMATTED_FILES.typescript, ...componentExtensions.map((extension) => `**/*.${extension}`)],
4144
+ {}
4145
+ ]];
4146
+ if (languages.css) blocks.push([
4147
+ "css",
4148
+ [...FORMATTED_FILES.css],
4149
+ {}
4150
+ ], [
4151
+ "scss",
4152
+ [...FORMATTED_FILES.scss],
4153
+ {}
4154
+ ], [
4155
+ "less",
4156
+ [...FORMATTED_FILES.less],
4157
+ {}
4158
+ ]);
4159
+ if (languages.html) blocks.push([
4160
+ "html",
4161
+ [...FORMATTED_FILES.html],
4162
+ {}
4163
+ ]);
4164
+ if (languages.graphql) blocks.push([
4165
+ "graphql",
4166
+ [...FORMATTED_FILES.graphql],
4167
+ {}
4168
+ ]);
4169
+ if (languages.json) blocks.push([
4170
+ "json",
4171
+ [...FORMATTED_FILES.json],
4172
+ {}
4173
+ ], [
4174
+ "json5",
4175
+ [GLOB_JSON5],
4176
+ JSON5_OXFMT_OPTIONS
4177
+ ]);
4178
+ if (languages.yaml) blocks.push([
4179
+ "yaml",
4180
+ [...FORMATTED_FILES.yaml],
4181
+ YAML_OXFMT_OPTIONS
4182
+ ]);
4183
+ return blocks;
4184
+ }
4185
+ /**
4186
+ * A block for a language ESLint has no parser for: the plain parser lets
4187
+ * `oxfmt/oxfmt` see the file, and keeps ESLint from parsing Markdown code
4188
+ * blocks in it as JavaScript.
4189
+ *
4190
+ * @param name - Appended to `isentinel/oxfmt/` to name the block.
4191
+ * @param files - Globs of the files the block applies to.
4192
+ * @param rules - Rules to enable for those files.
4193
+ * @returns The config block.
4194
+ */
4195
+ function plainBlock(name, files, rules) {
4196
+ return {
4197
+ name: `isentinel/oxfmt/${name}`,
4198
+ files,
4199
+ languageOptions: { parser: parserPlain },
4200
+ rules
4201
+ };
4202
+ }
3562
4203
  //#endregion
3563
4204
  //#region src/eslint/configs/package-json.ts
3564
4205
  init_globs();
@@ -20251,7 +20892,7 @@ const flatConfigProps = [
20251
20892
  "settings"
20252
20893
  ];
20253
20894
  async function isentinel(options, ...userConfigs) {
20254
- const { antiSlop = false, 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;
20895
+ const { antiSlop = false, autoRenamePlugins = true, componentExts: componentExtensions = [], e18e: enableE18e = true, eslintPlugin: enableEslintPlugin = false, formatter = "eslint", 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;
20255
20896
  const rootGlobs = mergeGlobs(GLOB_ROOT, customRootGlobs);
20256
20897
  const enableRoblox = options.roblox !== false;
20257
20898
  const reactOptions = resolveSubOptions(options, "react");
@@ -20281,15 +20922,26 @@ async function isentinel(options, ...userConfigs) {
20281
20922
  if (options.type !== void 0) return options.type;
20282
20923
  return enableRoblox ? "game" : "package";
20283
20924
  })();
20925
+ if (formatter === "standalone") warnStandaloneOptions(options);
20926
+ const formatterOptions = formatter !== "standalone" && typeof formatters === "object" ? formatters : {};
20927
+ const oxfmtFormats = formatter === "standalone" || formatters !== false && options.stylistic !== false;
20928
+ const oxfmtConfig = oxfmtFormats ? await resolveOxfmtConfig() : { options: {} };
20929
+ const oxfmtConfigOptions = oxfmtConfig.options;
20930
+ const prettierSettings = resolvePrettierSettings(formatterOptions.prettierOptions);
20931
+ const effectiveOxfmtOptions = buildOxfmtOptions({
20932
+ oxfmtConfigOptions,
20933
+ oxfmtOptions: formatterOptions.oxfmtOptions,
20934
+ prettierOptions: prettierSettings
20935
+ });
20936
+ const widthSettings = withOxfmtWidths(prettierSettings, effectiveOxfmtOptions);
20284
20937
  const stylisticOptions = (() => {
20285
20938
  if (options.stylistic === false) return false;
20286
20939
  if (typeof options.stylistic === "object") return options.stylistic;
20287
20940
  return {};
20288
20941
  })();
20289
20942
  if (stylisticOptions !== false && !("jsx" in stylisticOptions)) stylisticOptions.jsx = enableJsx;
20290
- const formatterOptions = typeof formatters === "object" ? formatters : {};
20291
- const oxfmtConfigOptions = formatters !== false ? await resolveOxfmtConfigOptions() : {};
20292
- const prettierSettings = resolvePrettierSettings(formatterOptions.prettierOptions);
20943
+ const sectionOverrides = oxfmtFormats ? editorConfigOverrides(void 0, oxfmtConfigOptions) : [];
20944
+ warnScriptWidthOverrides(sectionOverrides, ".editorconfig");
20293
20945
  const configs = [];
20294
20946
  if (enableGitignore !== false) configs.push(gitignore({
20295
20947
  config: enableGitignore,
@@ -20299,7 +20951,7 @@ async function isentinel(options, ...userConfigs) {
20299
20951
  isInEditor,
20300
20952
  stylistic: stylisticOptions
20301
20953
  }), comments({
20302
- prettierOptions: prettierSettings,
20954
+ prettierOptions: widthSettings,
20303
20955
  stylistic: stylisticOptions
20304
20956
  }), flawless({
20305
20957
  ...needsComplementOverlay ? { complementIgnores: robloxScopedFiles } : {},
@@ -20308,7 +20960,7 @@ async function isentinel(options, ...userConfigs) {
20308
20960
  antiSlop,
20309
20961
  roblox: enableRoblox,
20310
20962
  stylistic: stylisticOptions
20311
- }, prettierSettings), ignores(options.ignores), imports({
20963
+ }, widthSettings), ignores(options.ignores), imports({
20312
20964
  stylistic: stylisticOptions,
20313
20965
  type: projectType
20314
20966
  }), packageJson({
@@ -20368,7 +21020,7 @@ async function isentinel(options, ...userConfigs) {
20368
21020
  stylistic: stylisticOptions
20369
21021
  }, shouldFormatLua));
20370
21022
  }
20371
- if (stylisticOptions !== false) configs.push(stylistic(stylisticOptions), perfectionist({
21023
+ if (stylisticOptions !== false) configs.push(stylistic(oxfmtFormats ? withOxfmtStyle(stylisticOptions, effectiveOxfmtOptions) : stylisticOptions), perfectionist({
20372
21024
  ...resolveSubOptions(options, "perfectionist"),
20373
21025
  type: projectType
20374
21026
  }));
@@ -20391,6 +21043,16 @@ async function isentinel(options, ...userConfigs) {
20391
21043
  if (enableJsonc !== false && !typeAwareOnly) {
20392
21044
  configs.push(jsonc({
20393
21045
  ...getOverrides(options, "jsonc"),
21046
+ bracketSpacing: !oxfmtFormats || effectiveOxfmtOptions["bracketSpacing"] !== false,
21047
+ ...oxfmtFormats ? {
21048
+ formatIndent: oxfmtIndent(effectiveOxfmtOptions),
21049
+ layoutOverrides: jsonLayoutOverrides([
21050
+ ...sectionOverrides,
21051
+ ...oxfmtConfig.overrides ?? [],
21052
+ ...migratePrettierOverrides(prettierSettings["overrides"])
21053
+ ])
21054
+ } : {},
21055
+ singleQuote: oxfmtFormats && effectiveOxfmtOptions["singleQuote"] === true,
20394
21056
  stylistic: stylisticOptions
20395
21057
  }));
20396
21058
  if (stylisticOptions !== false) configs.push(sortTsconfig());
@@ -20450,9 +21112,11 @@ async function isentinel(options, ...userConfigs) {
20450
21112
  yaml: false
20451
21113
  } : formatters,
20452
21114
  oxfmtConfigOptions,
21115
+ oxfmtIgnorePatterns: oxfmtConfig.ignorePatterns,
20453
21116
  oxfmtOptions: formatterOptions.oxfmtOptions,
20454
21117
  oxlint: oxlintMode === "full",
20455
- prettierOptions: prettierSettings
21118
+ prettierOptions: prettierSettings,
21119
+ standalone: formatter === "standalone"
20456
21120
  }));
20457
21121
  if ("files" in options) throw new Error("[@isentinel/eslint-config] The first argument should not contain the \"files\" property as the options are supposed to be global. Place it in the second or later config instead.");
20458
21122
  const fusedConfig = flatConfigProps.reduce((accumulator, key) => {