@isentinel/eslint-config 6.0.1 → 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/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,152 @@ 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
+ }
766
+ //#endregion
767
+ //#region src/rules/oxfmt.ts
768
+ const UNSUPPORTED_PRETTIER_KEYS = /* @__PURE__ */ new Set([
769
+ "experimentalOperatorPosition",
770
+ "experimentalTernaries",
771
+ "jsdocPreferCodeFences",
772
+ "jsdocPrintWidth",
773
+ "parser",
774
+ "plugins",
775
+ "tsdoc"
776
+ ]);
777
+ const defaultSortImports = {
778
+ customGroups: [{
779
+ elementNamePattern: ["react"],
780
+ groupName: "react"
781
+ }, {
782
+ elementNamePattern: ["@*/**"],
783
+ groupName: "scoped"
784
+ }],
785
+ groups: [
786
+ "react",
787
+ "scoped",
788
+ [
789
+ "type-builtin",
790
+ "type-external",
791
+ "builtin",
792
+ "external"
793
+ ],
794
+ [
795
+ "type-internal",
796
+ "internal",
797
+ "type-parent",
798
+ "type-sibling",
799
+ "type-index",
800
+ "parent",
801
+ "sibling",
802
+ "index"
803
+ ],
804
+ "unknown"
805
+ ],
806
+ newlinesBetween: true
807
+ };
808
+ /**
809
+ * Migrate Prettier options to oxfmt options, dropping unsupported keys.
810
+ *
811
+ * @param prettierOptions - The Prettier options to migrate.
812
+ * @returns The migrated oxfmt options.
813
+ */
814
+ function migratePrettierOptions(prettierOptions) {
815
+ const oxfmtOptions = {};
816
+ for (const [key, value] of Object.entries(prettierOptions)) {
817
+ if (UNSUPPORTED_PRETTIER_KEYS.has(key)) continue;
818
+ if (key === "endOfLine" && value === "auto") continue;
819
+ oxfmtOptions[key] = key === "overrides" ? migratePrettierOverrides(value) : value;
820
+ }
821
+ return oxfmtOptions;
822
+ }
823
+ /**
824
+ * Migrate Prettier `overrides`: oxfmt wants `files` as an array, and the
825
+ * nested options migrated like the root ones.
826
+ *
827
+ * @param overrides - The `overrides` value, of unknown shape.
828
+ * @returns The overrides in oxfmt's shape; empty when not an array.
829
+ */
830
+ function migratePrettierOverrides(overrides) {
831
+ if (!Array.isArray(overrides)) return [];
832
+ return overrides.flatMap((override) => {
833
+ if (typeof override !== "object" || override === null) return [];
834
+ const { excludeFiles, files, options } = override;
835
+ return [{
836
+ ...excludeFiles === void 0 ? {} : { excludeFiles: stringsOf(excludeFiles) },
837
+ files: stringsOf(files),
838
+ options: typeof options === "object" && options !== null ? migratePrettierOptions(options) : {}
839
+ }];
840
+ });
841
+ }
842
+ /**
843
+ * Build the effective oxfmt options shared between the ESLint and oxlint
844
+ * factories.
845
+ *
846
+ * @param options - The option sources, in increasing precedence.
847
+ * @returns The effective oxfmt options.
848
+ */
849
+ function buildOxfmtOptions({ oxfmtConfigOptions = {}, oxfmtOptions, prettierOptions = {} }) {
850
+ return {
851
+ sortImports: defaultSortImports,
852
+ sortPackageJson: false,
853
+ ...migratePrettierOptions(prettierOptions),
854
+ ...oxfmtConfigOptions,
855
+ ...oxfmtOptions
856
+ };
857
+ }
858
+ /**
859
+ * Markdown formatting options layered over the base options. Shared by the
860
+ * ESLint config and the standalone oxfmt config so both format Markdown the
861
+ * same way.
862
+ *
863
+ * @param jsdocPrintWidth - Width for Markdown prose; 80 when unset.
864
+ * @returns Options applied on top of the base for Markdown files.
865
+ */
866
+ function markdownOxfmtOptions(jsdocPrintWidth) {
867
+ return {
868
+ printWidth: Number(jsdocPrintWidth) || 80,
869
+ proseWrap: "always",
870
+ tabWidth: 2
871
+ };
872
+ }
873
+ function stringsOf(value) {
874
+ return [value].flat().filter((item) => typeof item === "string");
875
+ }
876
+ /**
877
+ * JSON5 formatting options layered over the base options. `jsonc/quote-props`
878
+ * owns the quoting of JSON5 keys; oxfmt's `consistent` would strip the quotes
879
+ * it adds, and `--fix` would loop.
880
+ */
881
+ const JSON5_OXFMT_OPTIONS = { quoteProps: "preserve" };
882
+ /** YAML formatting options layered over the base options. */
883
+ const YAML_OXFMT_OPTIONS = {
884
+ tabWidth: 2,
885
+ useTabs: false
886
+ };
667
887
  //#endregion
668
888
  //#region src/guards.ts
669
889
  /**
@@ -1266,26 +1486,194 @@ function environmentMatcher(name, pattern, { noTty = false } = {}) {
1266
1486
  return value !== void 0 && pattern.test(value);
1267
1487
  };
1268
1488
  }
1269
- const OXFMT_CONFIG_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
1489
+ /** JSON oxfmt config files oxfmt discovers. */
1490
+ const OXFMT_RC_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
1491
+ /**
1492
+ * Oxfmt config modules oxfmt discovers. It loads other module extensions only
1493
+ * through `--config`, never by search.
1494
+ */
1495
+ const OXFMT_CONFIG_MODULES = ["oxfmt.config.ts", "oxfmt.config.mts"];
1496
+ OXFMT_CONFIG_MODULES[0];
1497
+ /** Every file name oxfmt discovers. One directory may hold only one of them. */
1498
+ const OXFMT_CONFIG_NAMES = [...OXFMT_RC_FILES, ...OXFMT_CONFIG_MODULES];
1499
+ /** The extensions of the JS/TS files ESLint and oxlint format. */
1500
+ const SCRIPT_EXTENSIONS = [
1501
+ "cjs",
1502
+ "cts",
1503
+ "js",
1504
+ "jsx",
1505
+ "mjs",
1506
+ "mts",
1507
+ "ts",
1508
+ "tsx"
1509
+ ];
1510
+ /** The extensions of the Markdown files ESLint lints code blocks in. */
1511
+ const MARKDOWN_EXTENSIONS = ["md"];
1512
+ const JSON_EXTENSIONS = [
1513
+ "json",
1514
+ "json5",
1515
+ "jsonc"
1516
+ ];
1517
+ const LEADING_DOT_SLASH = /^\.\//;
1518
+ /** Set once a process has warned about a JS/TS width override. */
1519
+ const WARNED_SCRIPT_OVERRIDE = Symbol.for("@isentinel/eslint-config/oxfmt-override-warned");
1520
+ /** Set once a process has warned about options standalone oxfmt ignores. */
1521
+ const WARNED_STANDALONE = Symbol.for("@isentinel/eslint-config/standalone-options-warned");
1522
+ const GLOB_MAGIC = /[*?[\]{}]/;
1270
1523
  /**
1271
- * Resolve oxfmt configuration from `.oxfmtrc.json` or `.oxfmtrc.jsonc`.
1524
+ * List the oxfmt config files in one directory, without walking up.
1272
1525
  *
1273
- * @returns The oxfmt configuration options, or an empty object if none found.
1526
+ * @param directory - Directory to check.
1527
+ * @returns The file names found, sorted.
1274
1528
  */
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
- }
1529
+ function oxfmtConfigNamesIn(directory) {
1530
+ return OXFMT_CONFIG_NAMES.filter((name) => fs.existsSync(path.join(directory, name))).sort();
1531
+ }
1532
+ /**
1533
+ * Find the oxfmt config the way oxfmt does: walk up from `cwd` to the
1534
+ * filesystem root and take the first directory that holds one. Like oxfmt,
1535
+ * refuse a directory that holds more than one.
1536
+ *
1537
+ * @param cwd - Directory to start from.
1538
+ * @returns The config file, if any.
1539
+ * @throws When one directory holds more than one oxfmt config file.
1540
+ */
1541
+ function findOxfmtConfig(cwd = process.cwd()) {
1542
+ let directory = path.resolve(cwd);
1543
+ for (;;) {
1544
+ const [first, second] = oxfmtConfigNamesIn(directory);
1545
+ if (second !== void 0) throw new Error(`[@isentinel/eslint-config] Both '${first}' and '${second}' found in ${directory}. Keep one oxfmt config file.`);
1546
+ if (first !== void 0) return {
1547
+ isModule: OXFMT_CONFIG_MODULES.some((name) => name === first),
1548
+ path: path.join(directory, first)
1549
+ };
1550
+ const parent = path.dirname(directory);
1551
+ if (parent === directory) return;
1552
+ directory = parent;
1287
1553
  }
1288
- return {};
1554
+ }
1555
+ /**
1556
+ * The overrides that set `printWidth` or `tabWidth` for Markdown files.
1557
+ *
1558
+ * @param overrides - The config's `overrides`.
1559
+ * @returns Each such override's scope and widths, in order.
1560
+ */
1561
+ function markdownOverridesOf(overrides) {
1562
+ if (!Array.isArray(overrides)) return [];
1563
+ return overrides.flatMap((override) => {
1564
+ if (!isRecord(override) || !overrideMatches(override, MARKDOWN_EXTENSIONS)) return [];
1565
+ const widths = widthsOf(override["options"]);
1566
+ return Object.keys(widths).length > 0 ? [{
1567
+ ...scopeOf(override),
1568
+ widths
1569
+ }] : [];
1570
+ });
1571
+ }
1572
+ /**
1573
+ * Where an override applies: its `files`, minus its `excludeFiles` and any
1574
+ * negated glob in `files`.
1575
+ *
1576
+ * @param override - One override.
1577
+ * @returns Its scope.
1578
+ */
1579
+ function scopeOf(override) {
1580
+ const strings = (value) => {
1581
+ return [value].flat().filter((glob) => typeof glob === "string");
1582
+ };
1583
+ const globs = strings(override["files"]);
1584
+ return {
1585
+ excludeFiles: [...strings(override["excludeFiles"]), ...globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1))],
1586
+ files: globs.filter((glob) => !glob.startsWith("!"))
1587
+ };
1588
+ }
1589
+ /** Oxfmt options that change the layout the JSON rules check. */
1590
+ const JSON_LAYOUT_KEYS = [
1591
+ "bracketSpacing",
1592
+ "singleQuote",
1593
+ "tabWidth",
1594
+ "useTabs"
1595
+ ];
1596
+ /**
1597
+ * The overrides that change the layout of JSON files, where the JSON layout
1598
+ * rules, set from the root options, would disagree with oxfmt.
1599
+ *
1600
+ * @param overrides - Overrides, of unknown shape.
1601
+ * @returns Each such override's scope, in order.
1602
+ */
1603
+ function jsonLayoutOverrides(overrides) {
1604
+ return overrides.flatMap((override) => {
1605
+ if (!isRecord(override) || !isRecord(override["options"]) || !overrideMatches(override, JSON_EXTENSIONS)) return [];
1606
+ const { options } = override;
1607
+ return JSON_LAYOUT_KEYS.some((key) => key in options) ? [scopeOf(override)] : [];
1608
+ });
1609
+ }
1610
+ /**
1611
+ * Warn, at most once per process, about factory options that do not reach
1612
+ * standalone oxfmt: it formats with `oxfmt.config.ts` alone.
1613
+ *
1614
+ * @param options - The factory options.
1615
+ * @param options.componentExts - Oxfmt formats only the extensions
1616
+ * `isentinelOxfmt` is given.
1617
+ * @param options.formatters - Any value but `true` is ignored by oxfmt.
1618
+ * @param options.gitignore - Custom ignore files, or `false`, do not reach
1619
+ * oxfmt.
1620
+ * @param options.ignores - User ignores do not reach oxfmt.
1621
+ */
1622
+ function warnStandaloneOptions({ componentExts, formatters, gitignore, ignores }) {
1623
+ const state = globalThis;
1624
+ if (!isMainThread || state[WARNED_STANDALONE] === true || process.env["ISENTINEL_QUIET_CONFIG_WARNINGS"] === "1") return;
1625
+ const names = [
1626
+ formatters !== void 0 && formatters !== true ? "formatters" : void 0,
1627
+ Array.isArray(componentExts) && componentExts.length > 0 ? "componentExts" : void 0,
1628
+ ignores === void 0 || Array.isArray(ignores) && ignores.length === 0 ? void 0 : "ignores",
1629
+ gitignore === false || isRecord(gitignore) ? "gitignore" : void 0
1630
+ ].filter((name) => name !== void 0);
1631
+ if (names.length > 0) {
1632
+ state[WARNED_STANDALONE] = true;
1633
+ 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).`);
1634
+ }
1635
+ }
1636
+ /**
1637
+ * Warn, at most once per process, when an oxfmt override changes `printWidth`
1638
+ * or `tabWidth` for JS/TS files: ESLint and oxlint use the root values.
1639
+ *
1640
+ * @param overrides - The config's `overrides`.
1641
+ * @param source - The config file name.
1642
+ */
1643
+ function warnScriptWidthOverrides(overrides, source) {
1644
+ const state = globalThis;
1645
+ if (state[WARNED_SCRIPT_OVERRIDE] === true || !Array.isArray(overrides)) return;
1646
+ if (overrides.some((override) => {
1647
+ if (!isRecord(override) || !isRecord(override["options"])) return false;
1648
+ const { printWidth, tabWidth } = override["options"];
1649
+ if (printWidth === void 0 && tabWidth === void 0) return false;
1650
+ return overrideMatches(override, SCRIPT_EXTENSIONS);
1651
+ })) {
1652
+ state[WARNED_SCRIPT_OVERRIDE] = true;
1653
+ 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.`);
1654
+ }
1655
+ }
1656
+ /**
1657
+ * Resolve oxfmt format options from the config oxfmt itself would use (see
1658
+ * {@link findOxfmtConfig}): `.oxfmtrc.json`, `.oxfmtrc.jsonc`,
1659
+ * `oxfmt.config.ts` or `oxfmt.config.mts`.
1660
+ *
1661
+ * @returns The format options and Markdown widths; empty options if none
1662
+ * found.
1663
+ * @rejects When the config is ambiguous or cannot be loaded.
1664
+ */
1665
+ async function resolveOxfmtConfig() {
1666
+ const config = findOxfmtConfig();
1667
+ if (config === void 0) return { options: {} };
1668
+ if (!config.isModule) return rcConfig(config.path, await fs.promises.readFile(config.path, "utf-8"));
1669
+ let loaded;
1670
+ try {
1671
+ const { createJiti } = await import("jiti");
1672
+ loaded = await createJiti(import.meta.url, { moduleCache: false }).import(config.path, { default: true });
1673
+ } catch (err) {
1674
+ throw loadError(config.path, err);
1675
+ }
1676
+ return moduleConfig(config.path, loaded);
1289
1677
  }
1290
1678
  /**
1291
1679
  * Override the severity of all rules in a rules object, preserving rule
@@ -1314,6 +1702,105 @@ function shouldEnableFeature(options, key, defaultValue = true) {
1314
1702
  return options[key] !== false;
1315
1703
  }
1316
1704
  /**
1705
+ * Whether a glob can match a file with one of `extensions`, judged by the
1706
+ * extension of its last segment (`*.ts`, `*.{ts,tsx}`, `*.?ts`, `*`, ...).
1707
+ *
1708
+ * @param glob - An override `files` glob.
1709
+ * @param extensions - The extensions to look for.
1710
+ * @returns Whether it can match such a file.
1711
+ */
1712
+ function globMatchesExtension(glob, extensions) {
1713
+ const base = glob.split("/").at(-1) ?? glob;
1714
+ const dot = base.lastIndexOf(".");
1715
+ if (dot === -1) return base.includes("*");
1716
+ const extension = base.slice(dot + 1);
1717
+ if (!GLOB_MAGIC.test(extension)) return extensions.includes(extension);
1718
+ return extensions.some((candidate) => path.matchesGlob(candidate, extension));
1719
+ }
1720
+ /**
1721
+ * Whether an oxfmt override can apply to a file with one of `extensions`.
1722
+ *
1723
+ * @param override - One entry of the config's `overrides`.
1724
+ * @param extensions - The extensions to look for.
1725
+ * @returns Whether its `files` can match such a file.
1726
+ */
1727
+ function overrideMatches(override, extensions) {
1728
+ return (Array.isArray(override["files"]) ? override["files"] : [override["files"]]).some((glob) => typeof glob === "string" && globMatchesExtension(glob, extensions));
1729
+ }
1730
+ function loadError(configPath, error) {
1731
+ return new Error(`[@isentinel/eslint-config] Failed to load ${path.basename(configPath)}: ${String(error)}`, { cause: error });
1732
+ }
1733
+ /**
1734
+ * Whether a config override is an `.editorconfig` section that
1735
+ * `isentinelOxfmt` copied in: the same globs (anchored or not) and a subset
1736
+ * of its options.
1737
+ *
1738
+ * @param override - One config override.
1739
+ * @param sections - The `.editorconfig` sections, as overrides.
1740
+ * @returns Whether it mirrors one of them.
1741
+ */
1742
+ function mirrorsSection(override, sections) {
1743
+ if (!isRecord(override) || !isRecord(override["options"])) return false;
1744
+ const { options } = override;
1745
+ const files = [override["files"]].flat().map((glob) => typeof glob === "string" ? glob.replace(LEADING_DOT_SLASH, "") : glob);
1746
+ return sections.some((section) => {
1747
+ return section.files.length === files.length && section.files.every((glob, index) => glob === files[index]) && Object.entries(options).every(([key, value]) => section.options[key] === value);
1748
+ });
1749
+ }
1750
+ /**
1751
+ * Turn a loaded oxfmt config into `oxfmt/oxfmt` rule options and the Markdown
1752
+ * widths. The rule takes format options only, so `$schema`, `ignorePatterns`
1753
+ * and `overrides` are dropped from the options.
1754
+ *
1755
+ * @param config - The loaded config.
1756
+ * @param config.$schema - Dropped.
1757
+ * @param config.ignorePatterns - Dropped; oxfmt applies them itself.
1758
+ * @param config.overrides - Dropped, after the width check; read for the
1759
+ * Markdown widths.
1760
+ * @param configPath - The config file, for the warning.
1761
+ * @returns The format options and Markdown widths.
1762
+ */
1763
+ function toResolvedConfig({ $schema: _schema, ignorePatterns: _ignorePatterns, overrides, ...options }, configPath) {
1764
+ const sections = editorConfigOverrides(path.dirname(configPath));
1765
+ const list = Array.isArray(overrides) ? overrides : [];
1766
+ const copied = list.filter((override) => mirrorsSection(override, sections));
1767
+ warnScriptWidthOverrides(copied, ".editorconfig");
1768
+ warnScriptWidthOverrides(list.filter((override) => !copied.includes(override)), path.basename(configPath));
1769
+ return {
1770
+ markdownOverrides: markdownOverridesOf(overrides),
1771
+ options,
1772
+ overrides: Array.isArray(overrides) ? overrides : []
1773
+ };
1774
+ }
1775
+ function moduleConfig(configPath, loaded) {
1776
+ if (!isRecord(loaded)) throw loadError(configPath, /* @__PURE__ */ new TypeError("the default export is not an object"));
1777
+ return toResolvedConfig(loaded, configPath);
1778
+ }
1779
+ function rcConfig(configPath, content) {
1780
+ let parsed;
1781
+ try {
1782
+ parsed = getStaticJSONValue(parseJSON(content, { jsonSyntax: "jsonc" }));
1783
+ } catch (err) {
1784
+ throw loadError(configPath, err);
1785
+ }
1786
+ if (!isRecord(parsed)) throw loadError(configPath, /* @__PURE__ */ new TypeError("the config is not an object"));
1787
+ return toResolvedConfig(parsed, configPath);
1788
+ }
1789
+ /**
1790
+ * Pick `printWidth` and `tabWidth` out of a set of format options.
1791
+ *
1792
+ * @param options - Format options, of unknown shape.
1793
+ * @returns The widths it sets.
1794
+ */
1795
+ function widthsOf(options) {
1796
+ if (!isRecord(options)) return {};
1797
+ const { printWidth, tabWidth } = options;
1798
+ return {
1799
+ ...typeof printWidth === "number" ? { printWidth } : {},
1800
+ ...typeof tabWidth === "number" ? { tabWidth } : {}
1801
+ };
1802
+ }
1803
+ /**
1317
1804
  * Resolves the lowest Node major version a semver range can match.
1318
1805
  *
1319
1806
  * @param range - A semver range such as `>=24.12.0` or `^24 || ^26`.
@@ -1492,7 +1979,15 @@ function hydrate(specifier, transform) {
1492
1979
  //#endregion
1493
1980
  //#region src/eslint/configs/comments.ts
1494
1981
  init_globs();
1495
- async function comments({ prettierOptions = {}, stylistic = true } = {}) {
1982
+ async function comments({ markdownCode, markdownCodeOverrides = [], prettierOptions = {}, stylistic = true } = {}) {
1983
+ function lengthRules(tabSize) {
1984
+ return commentLengthRules({
1985
+ maxLength: (Number(prettierOptions["jsdocPrintWidth"]) || 80) + 2,
1986
+ multiLineMaxLength: Number(prettierOptions["jsdocPrintWidth"]) || 80,
1987
+ semanticComments: ["oxlint-disable", "oxlint-enable"],
1988
+ tabSize: tabSize ?? prettierOptions.tabWidth ?? 4
1989
+ });
1990
+ }
1496
1991
  const pluginCommentLength = lazyPlugin("eslint-plugin-comment-length");
1497
1992
  const pluginStylistic = lazyPlugin("@stylistic/eslint-plugin");
1498
1993
  return [{
@@ -1514,13 +2009,19 @@ async function comments({ prettierOptions = {}, stylistic = true } = {}) {
1514
2009
  }, ...stylistic !== false ? [{
1515
2010
  name: "isentinel/eslint/comments/src",
1516
2011
  files: [GLOB_SRC],
1517
- rules: commentLengthRules({
1518
- maxLength: (Number(prettierOptions["jsdocPrintWidth"]) || 80) + 2,
1519
- multiLineMaxLength: Number(prettierOptions["jsdocPrintWidth"]) || 80,
1520
- semanticComments: ["oxlint-disable", "oxlint-enable"],
1521
- tabSize: prettierOptions.tabWidth ?? 4
1522
- })
1523
- }] : []];
2012
+ rules: lengthRules(void 0)
2013
+ }, ...markdownCode === void 0 ? [] : [{
2014
+ name: "isentinel/eslint/comments/markdown-code",
2015
+ files: [GLOB_MARKDOWN_CODE],
2016
+ rules: lengthRules(markdownCode.tabWidth)
2017
+ }, ...markdownCodeOverrides.map(({ excludeFiles, files, widths }, index) => {
2018
+ return {
2019
+ name: `isentinel/eslint/comments/markdown-code/override-${index}`,
2020
+ files: files.map((glob) => `${glob}/${GLOB_SRC}`),
2021
+ ignores: excludeFiles.map((glob) => `${glob}/**`),
2022
+ rules: lengthRules(widths.tabWidth ?? markdownCode.tabWidth)
2023
+ };
2024
+ })]] : []];
1524
2025
  }
1525
2026
  //#endregion
1526
2027
  //#region src/eslint/configs/disables.ts
@@ -1896,7 +2397,7 @@ function antiSlopTypeAwareRules({ roblox = true }) {
1896
2397
  //#region src/eslint/configs/flawless.ts
1897
2398
  init_globs();
1898
2399
  async function flawless(options = {}, prettierOptions = {}) {
1899
- const { antiSlop = false, complementIgnores, overridesTypeAware = {}, roblox = true, stylistic = true, typeAware = true } = options;
2400
+ const { antiSlop = false, complementIgnores, markdownCode = {}, markdownCodeOverrides = [], overridesTypeAware = {}, roblox = true, stylistic = true, typeAware = true } = options;
1900
2401
  const eslintPluginFlawless = await interopDefault(import("eslint-plugin-flawless"));
1901
2402
  const filesTypeAware = [GLOB_TS, GLOB_TSX];
1902
2403
  const ignoresTypeAware = options.ignoresTypeAware ?? [`**/*.md/**`, "**/*.d.{,c,m}ts"];
@@ -1911,6 +2412,13 @@ async function flawless(options = {}, prettierOptions = {}) {
1911
2412
  stylistic,
1912
2413
  tabWidth
1913
2414
  };
2415
+ function markdownCodeRules(widths) {
2416
+ return arrowStyleRules({
2417
+ maxLength: Number(prettierOptions["jsdocPrintWidth"]) || 80,
2418
+ printWidth: widths.printWidth ?? printWidth,
2419
+ tabWidth: widths.tabWidth ?? tabWidth
2420
+ });
2421
+ }
1914
2422
  return [
1915
2423
  {
1916
2424
  name: "isentinel/flawless/setup",
@@ -1927,12 +2435,18 @@ async function flawless(options = {}, prettierOptions = {}) {
1927
2435
  ...stylistic !== false ? [{
1928
2436
  name: "isentinel/flawless/markdown-code",
1929
2437
  files: [GLOB_MARKDOWN_CODE],
1930
- rules: arrowStyleRules({
1931
- maxLength: Number(prettierOptions["jsdocPrintWidth"]) || 80,
1932
- printWidth,
1933
- tabWidth
1934
- })
1935
- }] : [],
2438
+ rules: markdownCodeRules(markdownCode)
2439
+ }, ...markdownCodeOverrides.map(({ excludeFiles, files, widths }, index) => {
2440
+ return {
2441
+ name: `isentinel/flawless/markdown-code/override-${index}`,
2442
+ files: files.map((glob) => `${glob}/${GLOB_SRC}`),
2443
+ ignores: excludeFiles.map((glob) => `${glob}/**`),
2444
+ rules: markdownCodeRules({
2445
+ ...markdownCode,
2446
+ ...widths
2447
+ })
2448
+ };
2449
+ })] : [],
1936
2450
  ...complementIgnores ? [{
1937
2451
  name: "isentinel/flawless/complement",
1938
2452
  files: [GLOB_SRC],
@@ -2596,13 +3110,13 @@ function jsdoc({ full = false, stylistic = true, type = "game" } = {}) {
2596
3110
  //#endregion
2597
3111
  //#region src/eslint/configs/jsonc.ts
2598
3112
  init_globs();
2599
- async function jsonc({ files = [
3113
+ async function jsonc({ bracketSpacing = true, files = [
2600
3114
  GLOB_JSON,
2601
3115
  GLOB_JSON5,
2602
3116
  GLOB_JSONC
2603
- ], overrides = {}, stylistic = true } = {}) {
3117
+ ], formatIndent, layoutOverrides = [], overrides = {}, singleQuote = false, stylistic = true } = {}) {
2604
3118
  const defaults = resolveWithDefaults(stylistic, {});
2605
- const { indent = "tab" } = defaults === false ? {} : defaults;
3119
+ const { indent = formatIndent ?? "tab" } = defaults === false ? {} : defaults;
2606
3120
  const indentValue = typeof indent === "number" || indent === "tab" ? indent : "tab";
2607
3121
  const pluginJsonc = lazyPlugin("eslint-plugin-jsonc");
2608
3122
  const parserJsonc = await interopDefault(import("jsonc-eslint-parser"));
@@ -2654,7 +3168,7 @@ async function jsonc({ files = [
2654
3168
  consistent: true,
2655
3169
  multiline: true
2656
3170
  }],
2657
- "jsonc/object-curly-spacing": ["error", "always"],
3171
+ "jsonc/object-curly-spacing": ["error", bracketSpacing ? "always" : "never"],
2658
3172
  "jsonc/object-property-newline": ["error", { allowMultiplePropertiesPerLine: true }],
2659
3173
  "jsonc/quote-props": "error",
2660
3174
  "jsonc/quotes": "error"
@@ -2662,6 +3176,23 @@ async function jsonc({ files = [
2662
3176
  ...overrides
2663
3177
  }
2664
3178
  },
3179
+ ...stylistic !== false && singleQuote ? [{
3180
+ name: "isentinel/jsonc/json5-quotes",
3181
+ files: [GLOB_JSON5],
3182
+ rules: { "jsonc/quotes": ["error", "single"] }
3183
+ }] : [],
3184
+ ...stylistic === false ? [] : layoutOverrides.map(({ excludeFiles, files: overrideFiles }, index) => {
3185
+ return {
3186
+ name: `isentinel/jsonc/oxfmt-override-${index}`,
3187
+ files: overrideFiles,
3188
+ ignores: excludeFiles,
3189
+ rules: {
3190
+ "jsonc/indent": "off",
3191
+ "jsonc/object-curly-spacing": "off",
3192
+ "jsonc/quotes": "off"
3193
+ }
3194
+ };
3195
+ }),
2665
3196
  {
2666
3197
  name: "isentinel/jsonc/tsconfig",
2667
3198
  files: [GLOB_TSCONFIG],
@@ -3350,76 +3881,135 @@ async function node({ files = [GLOB_SRC], ignores } = {}) {
3350
3881
  }];
3351
3882
  }
3352
3883
  //#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
3884
+ //#region src/formatted-files.ts
3885
+ init_globs();
3886
+ /** Formatted globs, by `oxfmt/oxfmt` config block. */
3887
+ const FORMATTED_FILES = {
3888
+ css: [GLOB_CSS, GLOB_POSTCSS],
3889
+ graphql: ["**/*.graphql"],
3890
+ html: ["**/*.html"],
3891
+ javascript: [GLOB_JS, GLOB_JSX],
3892
+ json: [GLOB_ALL_JSON],
3893
+ less: [GLOB_LESS],
3894
+ markdown: [GLOB_MARKDOWN],
3895
+ scss: [GLOB_SCSS],
3896
+ typescript: [GLOB_TS, GLOB_TSX],
3897
+ yaml: [GLOB_YAML]
3393
3898
  };
3394
3899
  /**
3395
- * Migrate Prettier options to oxfmt options, dropping unsupported keys.
3900
+ * Resolve `formatters` to one flag per language.
3396
3901
  *
3397
- * @param prettierOptions - The Prettier options to migrate.
3398
- * @returns The migrated oxfmt options.
3902
+ * @param formatters - The `formatters` option.
3903
+ * @returns Whether each language is formatted.
3399
3904
  */
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;
3905
+ function resolveFormattedLanguages(formatters = true) {
3906
+ const enabled = formatters !== false;
3907
+ const toggles = typeof formatters === "object" ? formatters : {};
3908
+ return {
3909
+ css: enabled && toggles.css !== false,
3910
+ graphql: enabled && toggles.graphql !== false,
3911
+ html: enabled && toggles.html !== false,
3912
+ json: enabled && toggles.json !== false,
3913
+ markdown: enabled && toggles.markdown !== false,
3914
+ yaml: enabled && toggles.yaml !== false
3915
+ };
3916
+ }
3917
+ //#endregion
3918
+ //#region src/eslint/oxfmt-single-fix.ts
3919
+ /** Records the edit a fix makes, instead of making it. */
3920
+ const recordingFixer = { replaceTextRange(range, text) {
3921
+ return {
3922
+ range,
3923
+ text
3924
+ };
3925
+ } };
3926
+ /**
3927
+ * Wrap eslint-plugin-oxfmt so `oxfmt/oxfmt` reports one fix per file.
3928
+ *
3929
+ * @param plugin - The loaded plugin.
3930
+ * @returns The plugin with the wrapped rule.
3931
+ */
3932
+ function oxfmtSingleFix(plugin) {
3933
+ const { rules } = plugin;
3934
+ const rule = rules?.["oxfmt"];
3935
+ if (rule === void 0) return plugin;
3936
+ return {
3937
+ ...plugin,
3938
+ rules: {
3939
+ ...rules,
3940
+ oxfmt: singleFixRule(rule)
3941
+ }
3942
+ };
3943
+ }
3944
+ function editOf({ fix }) {
3945
+ if (typeof fix !== "function") return;
3946
+ return fix(recordingFixer) ?? void 0;
3947
+ }
3948
+ /**
3949
+ * Merge non-overlapping edits into one that spans them all.
3950
+ *
3951
+ * @param source - The file text.
3952
+ * @param edits - Edits that do not overlap, in any order.
3953
+ * @returns One edit with the same effect.
3954
+ */
3955
+ function mergeEdits(source, edits) {
3956
+ const sorted = edits.toSorted((first, second) => first.range[0] - second.range[0]);
3957
+ const start = sorted[0].range[0];
3958
+ let end = start;
3959
+ let text = "";
3960
+ for (const { range, text: insert } of sorted) {
3961
+ text += source.slice(end, range[0]) + insert;
3962
+ [, end] = range;
3406
3963
  }
3407
- return oxfmtOptions;
3964
+ return {
3965
+ range: [start, end],
3966
+ text
3967
+ };
3408
3968
  }
3409
3969
  /**
3410
- * Build the effective oxfmt options shared between the ESLint and oxlint
3411
- * factories.
3970
+ * Wrap the `oxfmt/oxfmt` rule so one file's formatting is one fix.
3412
3971
  *
3413
- * @param options - The option sources, in increasing precedence.
3414
- * @returns The effective oxfmt options.
3972
+ * The rule reports one fix per diff hunk. ESLint skips a fix that overlaps
3973
+ * another rule's fix in the same pass but applies the rest, so a delete hunk
3974
+ * can be dropped while its insert hunk lands (a moved comment is then kept
3975
+ * twice). One fix applies whole or not at all.
3976
+ *
3977
+ * @param rule - The original rule.
3978
+ * @returns The wrapped rule.
3415
3979
  */
3416
- function buildOxfmtOptions({ oxfmtConfigOptions = {}, oxfmtOptions, prettierOptions = {} }) {
3980
+ function singleFixRule(rule) {
3417
3981
  return {
3418
- sortImports: defaultSortImports,
3419
- sortPackageJson: false,
3420
- ...migratePrettierOptions(prettierOptions),
3421
- ...oxfmtConfigOptions,
3422
- ...oxfmtOptions
3982
+ ...rule,
3983
+ create(context) {
3984
+ const reports = [];
3985
+ const proxy = Object.create(context, { report: { value: (descriptor) => {
3986
+ reports.push(descriptor);
3987
+ } } });
3988
+ function flush() {
3989
+ const edits = reports.map((descriptor) => editOf(descriptor));
3990
+ const fixable = edits.filter((edit) => edit !== void 0);
3991
+ const merged = fixable.length > 0 ? mergeEdits(context.sourceCode.text, fixable) : void 0;
3992
+ let fixAttached = false;
3993
+ for (const [index, descriptor] of reports.entries()) {
3994
+ const { fix: _fix, ...rest } = descriptor;
3995
+ if (merged !== void 0 && !fixAttached && edits[index] !== void 0) {
3996
+ fixAttached = true;
3997
+ context.report({
3998
+ ...rest,
3999
+ fix: (fixer) => fixer.replaceTextRange(merged.range, merged.text)
4000
+ });
4001
+ } else context.report(rest);
4002
+ }
4003
+ reports.length = 0;
4004
+ }
4005
+ const listeners = rule.create(proxy);
4006
+ return Object.fromEntries(Object.entries(listeners).map(([selector, listener]) => {
4007
+ return [selector, (...args) => {
4008
+ listener(...args);
4009
+ flush();
4010
+ }];
4011
+ }));
4012
+ }
3423
4013
  };
3424
4014
  }
3425
4015
  //#endregion
@@ -3438,22 +4028,28 @@ const defaultPluginRenaming = {
3438
4028
  //#region src/eslint/configs/oxfmt.ts
3439
4029
  init_globs();
3440
4030
  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
- };
4031
+ const { componentExts: componentExtensions = [], files: oxfmtFiles, formatters = {}, oxfmtConfigOptions = {}, oxfmtOptions: userOxfmtOptions, oxlint: enableOxlint = false, prettierOptions = {}, standalone = false } = options ?? {};
4032
+ const languages = resolveFormattedLanguages(formatters);
3451
4033
  const oxfmtOptions = buildOxfmtOptions({
3452
4034
  oxfmtConfigOptions,
3453
4035
  oxfmtOptions: userOxfmtOptions,
3454
4036
  prettierOptions
3455
4037
  });
3456
- const pluginOxfmt = lazyPlugin("eslint-plugin-oxfmt");
4038
+ /**
4039
+ * The `oxfmt/oxfmt` entry for a block, with an options layer over the
4040
+ * base; none when oxfmt runs on its own.
4041
+ *
4042
+ * @param layer - Options applied over the base options.
4043
+ * @returns The rule entry, or no rules.
4044
+ */
4045
+ function formatRule(layer = {}) {
4046
+ return standalone ? {} : { "oxfmt/oxfmt": ["error", {
4047
+ ...oxfmtOptions,
4048
+ ...layer,
4049
+ disableNestedConfig: true
4050
+ }] };
4051
+ }
4052
+ const pluginOxfmt = lazyPlugin("eslint-plugin-oxfmt", oxfmtSingleFix);
3457
4053
  const configPrettier = await interopDefault(import("eslint-config-prettier/flat"));
3458
4054
  const rulesToIgnore = ["curly", "style/quotes"];
3459
4055
  const rules = renameRules(configPrettier.rules, defaultPluginRenaming);
@@ -3462,103 +4058,59 @@ async function oxfmt(options) {
3462
4058
  name: "isentinel/oxfmt/setup",
3463
4059
  plugins: { oxfmt: pluginOxfmt }
3464
4060
  }];
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,
4061
+ const scriptBlocks = [["javascript", oxfmtFiles ?? [...FORMATTED_FILES.javascript]], ["typescript", oxfmtFiles ?? [...FORMATTED_FILES.typescript, ...componentExtensions.map((extension) => `**/*.${extension}`)]]];
4062
+ for (const [name, files] of scriptBlocks) configs.push({
4063
+ name: `isentinel/oxfmt/${name}`,
4064
+ files,
3491
4065
  rules: {
3492
4066
  ...rules,
3493
4067
  "arrow-body-style": "off",
3494
- ...enableOxlint ? {} : { "oxfmt/oxfmt": ["error", oxfmtOptions] },
4068
+ ...enableOxlint ? {} : formatRule(),
3495
4069
  "prefer-arrow-callback": "off"
3496
4070
  }
3497
4071
  });
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({
4072
+ if (languages.css) configs.push(plainBlock("css", [...FORMATTED_FILES.css], formatRule()), plainBlock("scss", [...FORMATTED_FILES.scss], formatRule()), plainBlock("less", [...FORMATTED_FILES.less], formatRule()));
4073
+ if (languages.html) configs.push(plainBlock("html", [...FORMATTED_FILES.html], formatRule()));
4074
+ if (!standalone && languages.markdown) configs.push({
3531
4075
  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
- }] }
4076
+ files: [...FORMATTED_FILES.markdown],
4077
+ rules: formatRule(markdownOxfmtOptions(prettierOptions["jsdocPrintWidth"]))
3539
4078
  });
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({
4079
+ if (languages.graphql) configs.push(plainBlock("graphql", [...FORMATTED_FILES.graphql], formatRule()));
4080
+ if (!standalone && languages.json) configs.push({
3547
4081
  name: "isentinel/oxfmt/json",
3548
- files: [GLOB_ALL_JSON],
3549
- rules: { "oxfmt/oxfmt": ["error", oxfmtOptions] }
4082
+ files: [...FORMATTED_FILES.json],
4083
+ rules: formatRule()
4084
+ }, {
4085
+ name: "isentinel/oxfmt/json5",
4086
+ files: [GLOB_JSON5],
4087
+ rules: formatRule(JSON5_OXFMT_OPTIONS)
3550
4088
  });
3551
- if (formattingOptions.yaml) configs.push({
4089
+ if (!standalone && languages.yaml) configs.push({
3552
4090
  name: "isentinel/oxfmt/yaml",
3553
- files: [GLOB_YAML],
3554
- rules: { "oxfmt/oxfmt": ["error", {
3555
- ...oxfmtOptions,
3556
- tabWidth: 2,
3557
- useTabs: false
3558
- }] }
4091
+ files: [...FORMATTED_FILES.yaml],
4092
+ rules: formatRule(YAML_OXFMT_OPTIONS)
3559
4093
  });
3560
4094
  return configs;
3561
4095
  }
4096
+ /**
4097
+ * A block for a language ESLint has no parser for: the plain parser lets
4098
+ * `oxfmt/oxfmt` see the file, and keeps ESLint from parsing Markdown code
4099
+ * blocks in it as JavaScript.
4100
+ *
4101
+ * @param name - Appended to `isentinel/oxfmt/` to name the block.
4102
+ * @param files - Globs of the files the block applies to.
4103
+ * @param rules - Rules to enable for those files.
4104
+ * @returns The config block.
4105
+ */
4106
+ function plainBlock(name, files, rules) {
4107
+ return {
4108
+ name: `isentinel/oxfmt/${name}`,
4109
+ files,
4110
+ languageOptions: { parser: parserPlain },
4111
+ rules
4112
+ };
4113
+ }
3562
4114
  //#endregion
3563
4115
  //#region src/eslint/configs/package-json.ts
3564
4116
  init_globs();
@@ -4523,7 +5075,8 @@ function sonarjsTestRules({ jest = false, roblox = true } = {}) {
4523
5075
  }
4524
5076
  //#endregion
4525
5077
  //#region src/eslint/configs/sonarjs.ts
4526
- async function sonarjs({ isInEditor, roblox }) {
5078
+ init_globs();
5079
+ async function sonarjs({ complementIgnores, isInEditor, roblox }) {
4527
5080
  return [{
4528
5081
  name: "isentinel/sonarjs",
4529
5082
  plugins: { sonar: await interopDefault(import("eslint-plugin-sonarjs")) },
@@ -4531,7 +5084,15 @@ async function sonarjs({ isInEditor, roblox }) {
4531
5084
  isInEditor,
4532
5085
  roblox
4533
5086
  })
4534
- }];
5087
+ }, ...complementIgnores ? [{
5088
+ name: "isentinel/sonarjs/complement",
5089
+ files: [GLOB_SRC],
5090
+ ignores: complementIgnores,
5091
+ rules: sonarjsRules({
5092
+ isInEditor,
5093
+ roblox: false
5094
+ })
5095
+ }] : []];
4535
5096
  }
4536
5097
  //#endregion
4537
5098
  //#region src/eslint/configs/sort.ts
@@ -5938,7 +6499,6 @@ function typescriptTypeAwareRules({ roblox = true } = {}) {
5938
6499
  "ts/no-confusing-void-expression": "error",
5939
6500
  "ts/no-deprecated": "error",
5940
6501
  "ts/no-duplicate-type-constituents": "error",
5941
- "ts/no-empty-object-type": "error",
5942
6502
  "ts/no-floating-promises": ["error", {
5943
6503
  checkThenables: roblox,
5944
6504
  ignoreVoid: true
@@ -20243,7 +20803,7 @@ const flatConfigProps = [
20243
20803
  "settings"
20244
20804
  ];
20245
20805
  async function isentinel(options, ...userConfigs) {
20246
- 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;
20806
+ 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;
20247
20807
  const rootGlobs = mergeGlobs(GLOB_ROOT, customRootGlobs);
20248
20808
  const enableRoblox = options.roblox !== false;
20249
20809
  const reactOptions = resolveSubOptions(options, "react");
@@ -20273,34 +20833,55 @@ async function isentinel(options, ...userConfigs) {
20273
20833
  if (options.type !== void 0) return options.type;
20274
20834
  return enableRoblox ? "game" : "package";
20275
20835
  })();
20836
+ if (formatter === "standalone") warnStandaloneOptions(options);
20837
+ const formatterOptions = formatter !== "standalone" && typeof formatters === "object" ? formatters : {};
20838
+ const oxfmtFormats = formatter === "standalone" || formatters !== false && options.stylistic !== false;
20839
+ const oxfmtConfig = oxfmtFormats ? await resolveOxfmtConfig() : { options: {} };
20840
+ const oxfmtConfigOptions = oxfmtConfig.options;
20841
+ const prettierSettings = resolvePrettierSettings(formatterOptions.prettierOptions);
20842
+ const effectiveOxfmtOptions = buildOxfmtOptions({
20843
+ oxfmtConfigOptions,
20844
+ oxfmtOptions: formatterOptions.oxfmtOptions,
20845
+ prettierOptions: prettierSettings
20846
+ });
20847
+ const widthSettings = withOxfmtWidths(prettierSettings, effectiveOxfmtOptions);
20276
20848
  const stylisticOptions = (() => {
20277
20849
  if (options.stylistic === false) return false;
20278
20850
  if (typeof options.stylistic === "object") return options.stylistic;
20279
20851
  return {};
20280
20852
  })();
20281
20853
  if (stylisticOptions !== false && !("jsx" in stylisticOptions)) stylisticOptions.jsx = enableJsx;
20282
- const formatterOptions = typeof formatters === "object" ? formatters : {};
20283
- const oxfmtConfigOptions = formatters !== false ? await resolveOxfmtConfigOptions() : {};
20284
- const prettierSettings = resolvePrettierSettings(formatterOptions.prettierOptions);
20854
+ const sectionOverrides = oxfmtFormats ? editorConfigOverrides(void 0, oxfmtConfigOptions) : [];
20855
+ warnScriptWidthOverrides(sectionOverrides, ".editorconfig");
20285
20856
  const configs = [];
20286
20857
  if (enableGitignore !== false) configs.push(gitignore({
20287
20858
  config: enableGitignore,
20288
20859
  explicit: "gitignore" in options
20289
20860
  }));
20861
+ const markdownCodeWidths = formatter === "standalone" || formatters !== false && shouldEnableFeature(formatters, "markdown") ? {
20862
+ markdownCode: markdownOxfmtOptions(prettierSettings["jsdocPrintWidth"]),
20863
+ markdownCodeOverrides: [
20864
+ ...markdownOverridesOf(sectionOverrides),
20865
+ ...oxfmtConfig.markdownOverrides ?? [],
20866
+ ...markdownOverridesOf(prettierSettings["overrides"])
20867
+ ]
20868
+ } : {};
20290
20869
  configs.push(smallRules({
20291
20870
  isInEditor,
20292
20871
  stylistic: stylisticOptions
20293
20872
  }), comments({
20294
- prettierOptions: prettierSettings,
20873
+ ...markdownCodeWidths,
20874
+ prettierOptions: widthSettings,
20295
20875
  stylistic: stylisticOptions
20296
20876
  }), flawless({
20297
20877
  ...needsComplementOverlay ? { complementIgnores: robloxScopedFiles } : {},
20298
20878
  ..."tsconfigPath" in typescriptOptions ? { tsconfigPath: typescriptOptions.tsconfigPath } : {},
20299
20879
  ..."typeAware" in typescriptOptions ? { typeAware: typescriptOptions.typeAware } : {},
20300
20880
  antiSlop,
20881
+ ...markdownCodeWidths,
20301
20882
  roblox: enableRoblox,
20302
20883
  stylistic: stylisticOptions
20303
- }, prettierSettings), ignores(options.ignores), imports({
20884
+ }, widthSettings), ignores(options.ignores), imports({
20304
20885
  stylistic: stylisticOptions,
20305
20886
  type: projectType
20306
20887
  }), packageJson({
@@ -20314,6 +20895,7 @@ async function isentinel(options, ...userConfigs) {
20314
20895
  roblox: enableRoblox,
20315
20896
  stylistic: stylisticOptions
20316
20897
  }), promise(), sonarjs({
20898
+ ...needsComplementOverlay ? { complementIgnores: robloxScopedFiles } : {},
20317
20899
  isInEditor,
20318
20900
  roblox: enableRoblox
20319
20901
  }), typescript({
@@ -20359,7 +20941,7 @@ async function isentinel(options, ...userConfigs) {
20359
20941
  stylistic: stylisticOptions
20360
20942
  }, shouldFormatLua));
20361
20943
  }
20362
- if (stylisticOptions !== false) configs.push(stylistic(stylisticOptions), perfectionist({
20944
+ if (stylisticOptions !== false) configs.push(stylistic(oxfmtFormats ? withOxfmtStyle(stylisticOptions, effectiveOxfmtOptions) : stylisticOptions), perfectionist({
20363
20945
  ...resolveSubOptions(options, "perfectionist"),
20364
20946
  type: projectType
20365
20947
  }));
@@ -20382,6 +20964,16 @@ async function isentinel(options, ...userConfigs) {
20382
20964
  if (enableJsonc !== false && !typeAwareOnly) {
20383
20965
  configs.push(jsonc({
20384
20966
  ...getOverrides(options, "jsonc"),
20967
+ bracketSpacing: !oxfmtFormats || effectiveOxfmtOptions["bracketSpacing"] !== false,
20968
+ ...oxfmtFormats ? {
20969
+ formatIndent: oxfmtIndent(effectiveOxfmtOptions),
20970
+ layoutOverrides: jsonLayoutOverrides([
20971
+ ...sectionOverrides,
20972
+ ...oxfmtConfig.overrides ?? [],
20973
+ ...migratePrettierOverrides(prettierSettings["overrides"])
20974
+ ])
20975
+ } : {},
20976
+ singleQuote: oxfmtFormats && effectiveOxfmtOptions["singleQuote"] === true,
20385
20977
  stylistic: stylisticOptions
20386
20978
  }));
20387
20979
  if (stylisticOptions !== false) configs.push(sortTsconfig());
@@ -20443,7 +21035,8 @@ async function isentinel(options, ...userConfigs) {
20443
21035
  oxfmtConfigOptions,
20444
21036
  oxfmtOptions: formatterOptions.oxfmtOptions,
20445
21037
  oxlint: oxlintMode === "full",
20446
- prettierOptions: prettierSettings
21038
+ prettierOptions: prettierSettings,
21039
+ standalone: formatter === "standalone"
20447
21040
  }));
20448
21041
  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.");
20449
21042
  const fusedConfig = flatConfigProps.reduce((accumulator, key) => {