@useinsider/eslint-config 3.0.2 → 4.0.0-beta.2

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.
Files changed (60) hide show
  1. package/README.md +376 -64
  2. package/dist/builders/configFiles.d.ts +13 -0
  3. package/dist/builders/oxlint.d.ts +51 -0
  4. package/dist/builders/oxlintOwnership.d.ts +43 -0
  5. package/dist/builders/scopeBlocks.d.ts +6 -0
  6. package/dist/cli.cjs +1028 -86
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/configs/dependencies.d.ts +3 -0
  9. package/dist/{javascript-VBXqjVi8.cjs → javascript-DH8-N38w.cjs} +5 -5
  10. package/dist/{javascript-VBXqjVi8.cjs.map → javascript-DH8-N38w.cjs.map} +1 -1
  11. package/dist/{javascript-dom-5jRL_yKP.cjs → javascript-dom-CvtQ6_9W.cjs} +4 -4
  12. package/dist/{javascript-dom-5jRL_yKP.cjs.map → javascript-dom-CvtQ6_9W.cjs.map} +1 -1
  13. package/dist/{javascript-node-C1AU_4X2.cjs → javascript-node-BNsy3loY.cjs} +4 -4
  14. package/dist/{javascript-node-C1AU_4X2.cjs.map → javascript-node-BNsy3loY.cjs.map} +1 -1
  15. package/dist/{jest-CHzjzBtf.cjs → jest-4Es9Ggaa.cjs} +3 -3
  16. package/dist/{jest-CHzjzBtf.cjs.map → jest-4Es9Ggaa.cjs.map} +1 -1
  17. package/dist/main.cjs +763 -78
  18. package/dist/main.cjs.map +1 -1
  19. package/dist/main.d.ts +9 -2
  20. package/dist/main.js +746 -61
  21. package/dist/main.js.map +1 -1
  22. package/dist/oxlintOwnership-7hpdB3d1.cjs +199 -0
  23. package/dist/oxlintOwnership-7hpdB3d1.cjs.map +1 -0
  24. package/dist/oxlintOwnership-Cx0hgA_J.js +127 -0
  25. package/dist/oxlintOwnership-Cx0hgA_J.js.map +1 -0
  26. package/dist/removePluginsFromConfigs-D0oSUj2C.cjs.map +1 -1
  27. package/dist/removePluginsFromConfigs-DzqX2Ju2.js.map +1 -1
  28. package/dist/{stylistic-Dio0AM2E.cjs → stylistic-CoP3MT1Y.cjs} +4 -4
  29. package/dist/{stylistic-Dio0AM2E.cjs.map → stylistic-CoP3MT1Y.cjs.map} +1 -1
  30. package/dist/{typescript-C1icmOxY.cjs → typescript-D3VeLnpn.cjs} +7 -7
  31. package/dist/{typescript-C1icmOxY.cjs.map → typescript-D3VeLnpn.cjs.map} +1 -1
  32. package/dist/{typescript-dom-RUJn2WCZ.cjs → typescript-dom-iB_xBTAD.cjs} +4 -4
  33. package/dist/{typescript-dom-RUJn2WCZ.cjs.map → typescript-dom-iB_xBTAD.cjs.map} +1 -1
  34. package/dist/{typescript-node-CBEzcc56.cjs → typescript-node-Cq6qhj53.cjs} +4 -4
  35. package/dist/{typescript-node-CBEzcc56.cjs.map → typescript-node-Cq6qhj53.cjs.map} +1 -1
  36. package/dist/utils/ensureConfigStructure.d.ts +15 -6
  37. package/dist/utils/ensureOxlintConfig.d.ts +10 -0
  38. package/dist/utils/layoutDiagnosis.d.ts +63 -0
  39. package/dist/utils/offerMigration.d.ts +42 -0
  40. package/dist/utils/silenceRules.d.ts +9 -0
  41. package/dist/{vitest-BomI8gtu.cjs → vitest-BlGQswK2.cjs} +3 -3
  42. package/dist/{vitest-BomI8gtu.cjs.map → vitest-BlGQswK2.cjs.map} +1 -1
  43. package/dist/{vue2-azKwRgaJ.cjs → vue2-Da1i_oVn.cjs} +8 -8
  44. package/dist/{vue2-azKwRgaJ.cjs.map → vue2-Da1i_oVn.cjs.map} +1 -1
  45. package/dist/{vue2-options-api-ma3J-bvM.cjs → vue2-options-api-BSdrUm8H.cjs} +8 -8
  46. package/dist/{vue2-options-api-ma3J-bvM.cjs.map → vue2-options-api-BSdrUm8H.cjs.map} +1 -1
  47. package/dist/{vue2-typescript-CZSHzp7I.cjs → vue2-typescript-dXLRSGNP.cjs} +10 -10
  48. package/dist/{vue2-typescript-CZSHzp7I.cjs.map → vue2-typescript-dXLRSGNP.cjs.map} +1 -1
  49. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs → vue2-typescript-options-api-LM7U5dWq.cjs} +10 -10
  50. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs.map → vue2-typescript-options-api-LM7U5dWq.cjs.map} +1 -1
  51. package/dist/{vue3-Df5UQ1Ys.cjs → vue3-621EM2Ii.cjs} +8 -8
  52. package/dist/{vue3-Df5UQ1Ys.cjs.map → vue3-621EM2Ii.cjs.map} +1 -1
  53. package/dist/{vue3-options-api-Bx7yYHza.cjs → vue3-options-api-CC9_fAiq.cjs} +8 -8
  54. package/dist/{vue3-options-api-Bx7yYHza.cjs.map → vue3-options-api-CC9_fAiq.cjs.map} +1 -1
  55. package/dist/{vue3-typescript-C0O1JZlk.cjs → vue3-typescript-DONe-IKz.cjs} +10 -10
  56. package/dist/{vue3-typescript-C0O1JZlk.cjs.map → vue3-typescript-DONe-IKz.cjs.map} +1 -1
  57. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs → vue3-typescript-options-api-D4pqpDFs.cjs} +10 -10
  58. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs.map → vue3-typescript-options-api-D4pqpDFs.cjs.map} +1 -1
  59. package/package.json +8 -5
  60. package/dist/rolldown-runtime-VH7oDXx4.cjs +0 -28
package/dist/main.cjs CHANGED
@@ -1,18 +1,19 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
2
+ const require_oxlintOwnership = require("./oxlintOwnership-7hpdB3d1.cjs");
3
3
  const require_merge = require("./merge-DJQU2y2U.cjs");
4
4
  const require_mergeAllConfig = require("./mergeAllConfig-DjMWjBNg.cjs");
5
5
  let eslint_config = require("eslint/config");
6
6
  let globals = require("globals");
7
- globals = require_rolldown_runtime.__toESM(globals, 1);
7
+ globals = require_oxlintOwnership.__toESM(globals, 1);
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
10
10
  let node_path = require("node:path");
11
11
  let node_url = require("node:url");
12
12
  let _typescript_eslint_types = require("@typescript-eslint/types");
13
+ let node_child_process = require("node:child_process");
13
14
  let _antfu_install_pkg = require("@antfu/install-pkg");
14
15
  let semver = require("semver");
15
- semver = require_rolldown_runtime.__toESM(semver, 1);
16
+ semver = require_oxlintOwnership.__toESM(semver, 1);
16
17
  let node_console = require("node:console");
17
18
  //#region src/builders/configFiles.ts
18
19
  /**
@@ -33,6 +34,33 @@ var configFileRules = {
33
34
  "import-x/no-extraneous-dependencies": "off",
34
35
  "import-x/prefer-default-export": "off"
35
36
  };
37
+ var LINT_TOPOLOGY_NAME = "insider/lint-topology";
38
+ /**
39
+ * The three files `useInsider()` requires a consumer to own. They are always linted with
40
+ * {@link buildLintTopologyBlocks}, independently of the `configFiles` option.
41
+ */
42
+ var LINT_TOPOLOGY_GLOBS = [
43
+ "**/insider.lint.{ts,mts,cts,js,mjs,cjs}",
44
+ "**/eslint.config.{ts,mts,cts,js,mjs,cjs}",
45
+ "**/oxlint.config.{ts,mts,cts,js,mjs,cjs}"
46
+ ];
47
+ var lintTopologyRules = {
48
+ ...configFileRules,
49
+ "import-x/extensions": "off"
50
+ };
51
+ /**
52
+ * Relaxations the mandated three-file layout needs in order to lint clean. Emitted unconditionally
53
+ * because the layout exists whether or not the consumer passes `configFiles`.
54
+ * @example
55
+ * buildLintTopologyBlocks()[0].rules['import-x/extensions']; // 'off'
56
+ */
57
+ function buildLintTopologyBlocks() {
58
+ return [{
59
+ name: LINT_TOPOLOGY_NAME,
60
+ files: [...LINT_TOPOLOGY_GLOBS],
61
+ rules: { ...lintTopologyRules }
62
+ }];
63
+ }
36
64
  function mergeGlobs(userGlobs) {
37
65
  const seen = /* @__PURE__ */ new Set();
38
66
  const merged = [];
@@ -62,17 +90,459 @@ function buildConfigFilesBlocks(value) {
62
90
  function buildJqueryBlocks(entries) {
63
91
  if (!entries || entries.length === 0) return [];
64
92
  return entries.map((entry) => ({
65
- files: entry.files,
93
+ files: [...entry.files],
66
94
  languageOptions: { globals: { ...globals.default.jquery } }
67
95
  }));
68
96
  }
69
97
  //#endregion
70
- //#region src/utils/ensureConfigStructure.ts
71
- var CONFIG_FILE_REGEX = /eslint\.config\.[mc]?[jt]s(?:$|\?)/;
98
+ //#region src/builders/oxlint.ts
99
+ var OXLINT_SUPPRESSION_NAME = "insider/oxlint-suppression";
100
+ var OXLINT_SFC_EXCEPTION_NAME = "insider/oxlint-suppression-sfc-exception";
101
+ var UNUSED_DIRECTIVE_BLOCK_NAME = "insider/unused-disable-directives";
102
+ var UNUSED_DIRECTIVE_SEVERITY = "warn";
103
+ var OXLINT_DIRECTIVE_OPTIONS = {
104
+ respectEslintDisableDirectives: false,
105
+ reportUnusedDisableDirectives: UNUSED_DIRECTIVE_SEVERITY
106
+ };
107
+ /**
108
+ * The always-on ESLint block that makes a stale `eslint-disable` visible. ESLint 9 defaults this to
109
+ * `warn`, but the split relies on it, so it is pinned rather than inherited.
110
+ * @example
111
+ * buildUnusedDirectiveBlocks()[0].linterOptions; // { reportUnusedDisableDirectives: 'warn' }
112
+ */
113
+ function buildUnusedDirectiveBlocks() {
114
+ return [{
115
+ name: UNUSED_DIRECTIVE_BLOCK_NAME,
116
+ linterOptions: { reportUnusedDisableDirectives: UNUSED_DIRECTIVE_SEVERITY }
117
+ }];
118
+ }
119
+ function ownedRuleNames(rules) {
120
+ return Object.keys(rules).filter((name) => require_oxlintOwnership.isOxlintOwned(name) && !require_oxlintOwnership.isRuleDisabled(rules[name]));
121
+ }
122
+ function translatableRuleNames(rules) {
123
+ return Object.keys(rules).filter(require_oxlintOwnership.isOxlintOwned);
124
+ }
125
+ function toOffRecord(ruleNames) {
126
+ return Object.fromEntries(ruleNames.map((name) => [name, "off"]));
127
+ }
128
+ function suppressionFor(block) {
129
+ if (!block.rules) return [];
130
+ const owned = ownedRuleNames(block.rules);
131
+ if (owned.length === 0) return [];
132
+ const files = block.files ? { files: [...block.files] } : {};
133
+ const blockIgnores = block.ignores ? [...block.ignores] : [];
134
+ const vueSafe = owned.filter((name) => !require_oxlintOwnership.isVueUnsafeRule(name));
135
+ const vueUnsafe = owned.filter((name) => require_oxlintOwnership.isVueUnsafeRule(name));
136
+ const suppressions = [];
137
+ if (vueSafe.length > 0) suppressions.push({
138
+ name: OXLINT_SUPPRESSION_NAME,
139
+ ...files,
140
+ ...blockIgnores.length > 0 ? { ignores: blockIgnores } : {},
141
+ rules: toOffRecord(vueSafe)
142
+ });
143
+ if (vueUnsafe.length > 0) suppressions.push({
144
+ name: OXLINT_SFC_EXCEPTION_NAME,
145
+ ...files,
146
+ ignores: [...blockIgnores, ...require_oxlintOwnership.VUE_LIKE_GLOBS],
147
+ rules: toOffRecord(vueUnsafe)
148
+ });
149
+ return suppressions;
150
+ }
151
+ /**
152
+ * ESLint blocks that turn off every rule the emitted oxlint config takes over. Rules oxlint cannot
153
+ * check inside an SFC are suppressed everywhere except `.vue`/`.svelte`/`.astro`, so a Vue consumer
154
+ * keeps them.
155
+ * @example
156
+ * buildOxlintSuppressionBlocks([{ files: ['src/**\/*.ts'], rules: { 'no-var': 'error' } }]);
157
+ * // [{ files: ['src/**\/*.ts'], rules: { 'no-var': 'off' } }]
158
+ */
159
+ function buildOxlintSuppressionBlocks(blocks) {
160
+ return blocks.flatMap(suppressionFor);
161
+ }
162
+ function translateRules(rules, plugins) {
163
+ return translatableRuleNames(rules).reduce((accumulator, name) => {
164
+ plugins.add(require_oxlintOwnership.toOxlintPluginName(name));
165
+ accumulator[require_oxlintOwnership.toOxlintRuleName(name)] = rules[name];
166
+ return accumulator;
167
+ }, {});
168
+ }
169
+ function anchorOxlintGlob(glob) {
170
+ return glob.startsWith("**") || glob.startsWith("./") || glob.startsWith("/") ? glob : `./${glob}`;
171
+ }
172
+ function anchorGlobs(globs) {
173
+ return globs.map(anchorOxlintGlob);
174
+ }
175
+ function anchorOxlintIgnorePattern(pattern) {
176
+ const negated = pattern.startsWith("!");
177
+ const body = negated ? pattern.slice(1) : pattern;
178
+ const anchored = body.startsWith("/") || body.startsWith("**") ? body : `/${body}`;
179
+ return negated ? `!${anchored}` : anchored;
180
+ }
181
+ function overrideFor(block, plugins) {
182
+ if (!block.files || !block.rules) return [];
183
+ const translated = translateRules(block.rules, plugins);
184
+ if (Object.keys(translated).length === 0) return [];
185
+ return [{
186
+ files: anchorGlobs(block.files),
187
+ ...block.ignores && block.ignores.length > 0 ? { excludeFiles: anchorGlobs(block.ignores) } : {},
188
+ rules: translated
189
+ }];
190
+ }
191
+ /**
192
+ * The oxlint half of the split, derived from the same emitted ESLint blocks the suppression layer
193
+ * reads. Every oxlint category is pinned off so the config enables exactly the preset's own rules
194
+ * and nothing oxlint would otherwise turn on by default.
195
+ * @example
196
+ * buildOxlintConfig([{ files: ['src/**\/*.ts'], rules: { 'no-var': 'error' } }], ['dist']);
197
+ * // { plugins: ['eslint'], categories: { correctness: 'off', ... }, ignorePatterns: ['/dist'], ... }
198
+ */
199
+ function buildOxlintConfig(blocks, ignores) {
200
+ const plugins = /* @__PURE__ */ new Set(["eslint"]);
201
+ const overrides = blocks.flatMap((block) => overrideFor(block, plugins));
202
+ const globalRules = blocks.filter((block) => !block.files && block.rules).reduce((accumulator, block) => ({
203
+ ...accumulator,
204
+ ...translateRules(block.rules, plugins)
205
+ }), {});
206
+ return {
207
+ plugins: [...plugins].sort((left, right) => left.localeCompare(right)),
208
+ categories: Object.fromEntries(require_oxlintOwnership.OXLINT_CATEGORIES.map((name) => [name, "off"])),
209
+ ...ignores && ignores.length > 0 ? { ignorePatterns: ignores.map(anchorOxlintIgnorePattern) } : {},
210
+ options: { ...OXLINT_DIRECTIVE_OPTIONS },
211
+ rules: globalRules,
212
+ overrides
213
+ };
214
+ }
215
+ //#endregion
216
+ //#region src/utils/layoutDiagnosis.ts
217
+ var MODULE_EXTENSIONS = [
218
+ "ts",
219
+ "mts",
220
+ "cts",
221
+ "js",
222
+ "mjs",
223
+ "cjs"
224
+ ];
225
+ var LINT_MODULE_FILENAMES = MODULE_EXTENSIONS.map((ext) => `insider.lint.${ext}`);
226
+ var ESLINT_CONFIG_FILENAMES = MODULE_EXTENSIONS.map((ext) => `eslint.config.${ext}`);
227
+ var OXLINT_CONFIG_FILENAMES = ["oxlint.config.ts", "oxlint.config.mts"];
72
228
  var OPT_OUT_ENV = "INSIDER_SKIP_CONFIG_VALIDATION";
73
- var FILE_URL_PREFIX = "file://";
74
- var TARGET_CALLEE_NAME = "useInsider";
229
+ var MIGRATE_COMMAND = "npx @useinsider/eslint-config migrate";
230
+ var SCAFFOLD_COMMAND = "npx @useinsider/eslint-config";
75
231
  var INSIDER_IMPORT_SOURCE = "@useinsider/eslint-config";
232
+ var TARGET_CALLEE_NAME = "useInsider";
233
+ var ESLINT_RENDERER_NAME = "useInsiderESLint";
234
+ var OXC_RENDERER_NAME = "useInsiderOXC";
235
+ var RENDERER_EXPORT = `export const ${`{ ${ESLINT_RENDERER_NAME}, ${OXC_RENDERER_NAME} }`} = ${TARGET_CALLEE_NAME}({ ... })`;
236
+ var EXPECTED_LAYOUT = [
237
+ "Expected layout (three sibling files):",
238
+ "",
239
+ " // insider.lint.ts",
240
+ ` import { ${TARGET_CALLEE_NAME} } from '${INSIDER_IMPORT_SOURCE}';`,
241
+ "",
242
+ ` ${RENDERER_EXPORT};`,
243
+ "",
244
+ " // eslint.config.ts",
245
+ ` import { ${ESLINT_RENDERER_NAME} } from './insider.lint.ts';`,
246
+ "",
247
+ ` export default ${ESLINT_RENDERER_NAME}();`,
248
+ "",
249
+ " // oxlint.config.ts",
250
+ ` import { ${OXC_RENDERER_NAME} } from './insider.lint.ts';`,
251
+ "",
252
+ ` export default await ${OXC_RENDERER_NAME}();`
253
+ ].join("\n");
254
+ function findSibling(directory, filenames, fileExists) {
255
+ return filenames.find((filename) => fileExists((0, node_path.join)(directory, filename))) ?? null;
256
+ }
257
+ var lazyRequire$1 = (0, node_module.createRequire)({}.url);
258
+ function parseProgram(source) {
259
+ try {
260
+ const { parse } = lazyRequire$1("@typescript-eslint/parser");
261
+ return parse(source, {
262
+ sourceType: "module",
263
+ range: true,
264
+ loc: true
265
+ });
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ function collectUseInsiderAliases(program) {
271
+ const aliases = /* @__PURE__ */ new Set();
272
+ program.body.forEach((node) => {
273
+ if (node.type !== _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration) return;
274
+ node.specifiers.forEach((specifier) => {
275
+ if (specifier.type === _typescript_eslint_types.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === _typescript_eslint_types.AST_NODE_TYPES.Identifier && specifier.imported.name === "useInsider") aliases.add(specifier.local.name);
276
+ });
277
+ });
278
+ return aliases;
279
+ }
280
+ function callsAny(node, aliases) {
281
+ if (!node || typeof node !== "object") return false;
282
+ if (Array.isArray(node)) return node.some((child) => callsAny(child, aliases));
283
+ const candidate = node;
284
+ if (candidate.type === _typescript_eslint_types.AST_NODE_TYPES.CallExpression) {
285
+ const { callee } = candidate;
286
+ if (callee.type === _typescript_eslint_types.AST_NODE_TYPES.Identifier && aliases.has(callee.name)) return true;
287
+ }
288
+ return Object.entries(node).some(([key, value]) => key !== "parent" && callsAny(value, aliases));
289
+ }
290
+ /**
291
+ * Whether a flat-config source is still the removed v3 shape: it imports `useInsider` and calls it
292
+ * itself, instead of importing a renderer from a sibling `insider.lint.*` module. Both the plain
293
+ * `export default useInsider({...})` form and the spread form are recognized.
294
+ * @example
295
+ * isV3EslintConfigSource("import { useInsider } from '@useinsider/eslint-config';\nexport default useInsider({});");
296
+ * // true
297
+ */
298
+ function isV3EslintConfigSource(source) {
299
+ const program = parseProgram(source);
300
+ if (!program) return false;
301
+ const aliases = collectUseInsiderAliases(program);
302
+ if (aliases.size === 0) return false;
303
+ return program.body.some((node) => callsAny(node, aliases));
304
+ }
305
+ function classify(lintModule, eslintConfig, oxlintConfig, eslintConfigIsV3) {
306
+ if (lintModule && eslintConfig && oxlintConfig) return "complete";
307
+ if (!lintModule && !eslintConfig && !oxlintConfig) return "noConfig";
308
+ if (!lintModule && eslintConfig && eslintConfigIsV3) return "unmigratedV3";
309
+ return "partial";
310
+ }
311
+ /**
312
+ * What of the three-file layout is on disk in a directory, and which of the four guidance branches
313
+ * applies. `unmigratedV3` is the only branch the in-lint migration offer acts on, because it is the
314
+ * only one {@link import('../cli/migrate').runMigrate} can actually rewrite.
315
+ * @example
316
+ * diagnoseLayout('/repo').kind; // 'unmigratedV3' when eslint.config.js still calls useInsider()
317
+ */
318
+ function diagnoseLayout(directory, options = {}) {
319
+ const fileExists = options.fileExists ?? node_fs.existsSync;
320
+ const readSource = options.readSource ?? ((path) => (0, node_fs.readFileSync)(path, "utf8"));
321
+ const lintModule = findSibling(directory, LINT_MODULE_FILENAMES, fileExists);
322
+ const eslintConfig = findSibling(directory, ESLINT_CONFIG_FILENAMES, fileExists);
323
+ const oxlintConfig = findSibling(directory, OXLINT_CONFIG_FILENAMES, fileExists);
324
+ let eslintConfigIsV3 = false;
325
+ if (eslintConfig && !lintModule) try {
326
+ eslintConfigIsV3 = isV3EslintConfigSource(readSource((0, node_path.join)(directory, eslintConfig)));
327
+ } catch {
328
+ eslintConfigIsV3 = false;
329
+ }
330
+ const kind = classify(lintModule, eslintConfig, oxlintConfig, eslintConfigIsV3);
331
+ return {
332
+ kind,
333
+ directory,
334
+ lintModule,
335
+ eslintConfig,
336
+ oxlintConfig,
337
+ missing: [
338
+ lintModule ? null : "insider.lint.ts",
339
+ eslintConfig ? null : "eslint.config.ts",
340
+ oxlintConfig ? null : "oxlint.config.ts"
341
+ ].filter((name) => name !== null),
342
+ autoMigratable: kind === "unmigratedV3"
343
+ };
344
+ }
345
+ /**
346
+ * Import specifier for the consumer's lint module, naming the extension that is actually on disk so
347
+ * the suggested snippet resolves as printed.
348
+ * @example
349
+ * lintModuleSpecifier('/repo'); // './insider.lint.mts' when that is the file present
350
+ */
351
+ function lintModuleSpecifier(directory, fileExists = node_fs.existsSync) {
352
+ return `./${findSibling(directory, LINT_MODULE_FILENAMES, fileExists) ?? "insider.lint.ts"}`;
353
+ }
354
+ function inventoryLines(diagnosis) {
355
+ const v3Note = diagnosis.kind === "unmigratedV3" ? " (still calls `useInsider()` itself)" : "";
356
+ return [
357
+ diagnosis.lintModule ? ` present: ${diagnosis.lintModule}` : " MISSING: insider.lint.{ts,mts}",
358
+ diagnosis.eslintConfig ? ` present: ${diagnosis.eslintConfig}${v3Note}` : " MISSING: eslint.config.{ts,mts}",
359
+ diagnosis.oxlintConfig ? ` present: ${diagnosis.oxlintConfig}` : ` MISSING: ${OXLINT_CONFIG_FILENAMES.join(" or ")}`
360
+ ];
361
+ }
362
+ function guidanceBody(diagnosis) {
363
+ if (diagnosis.kind === "unmigratedV3") return [
364
+ "`useInsider()` no longer returns a flat-config array, so the config it is called from has",
365
+ "to become a module both linters can import. The migration does that rewrite for you:",
366
+ "",
367
+ ` ${MIGRATE_COMMAND}`
368
+ ];
369
+ if (diagnosis.kind === "noConfig") return [
370
+ "There is no linter configuration here yet. Scaffold the three-file layout:",
371
+ "",
372
+ ` ${SCAFFOLD_COMMAND}`
373
+ ];
374
+ return [
375
+ `The layout is incomplete — missing ${diagnosis.missing.join(", ")}.`,
376
+ "Add the missing file, or let the migration regenerate the whole layout:",
377
+ "",
378
+ ` ${MIGRATE_COMMAND}`
379
+ ];
380
+ }
381
+ function detailLines(detail) {
382
+ if (!detail || detail.length === 0) return [];
383
+ return ["", ...detail];
384
+ }
385
+ /**
386
+ * The consumer-facing failure text: a site-specific headline, then the branch that matches what is
387
+ * on disk, then the layout dump, then the bypass. The command to run always comes before the dump,
388
+ * because a consumer who reads only the first screen still has to be told about `migrate`.
389
+ * @example
390
+ * formatLayoutGuidance({ headline: 'no `insider.lint.*` module found in /repo.', diagnosis });
391
+ */
392
+ function formatLayoutGuidance(input) {
393
+ return [
394
+ `@useinsider/eslint-config: ${input.headline}`,
395
+ "",
396
+ ...inventoryLines(input.diagnosis),
397
+ "",
398
+ ...guidanceBody(input.diagnosis),
399
+ ...detailLines(input.detail),
400
+ "",
401
+ EXPECTED_LAYOUT,
402
+ "",
403
+ `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
404
+ ].join("\n");
405
+ }
406
+ //#endregion
407
+ //#region src/utils/offerMigration.ts
408
+ var PACKAGE_NAME = "@useinsider/eslint-config";
409
+ var CLI_RELATIVE_PATH = "dist/cli.cjs";
410
+ var NO_PROMPT_ENV = "INSIDER_NO_PROMPT";
411
+ var IN_FLIGHT_ENV = "INSIDER_MIGRATION_IN_PROGRESS";
412
+ var MIGRATE_FROM_LINT_ARGS = ["migrate", "--from-lint"];
413
+ var BINARY_EXTENSIONS = /\.[mc]?js$/u;
414
+ function binaryName(argument) {
415
+ return (0, node_path.basename)(argument).replace(BINARY_EXTENSIONS, "");
416
+ }
417
+ /**
418
+ * Which linter is hosting the current process. oxlint evaluates `oxlint.config.ts` inside the Node
419
+ * process that runs its own `bin/oxlint` wrapper, so `useInsider()` genuinely executes under both
420
+ * binaries and the two have to be told apart before anything is written to stdout.
421
+ * @example
422
+ * detectLintHost(['node', '/app/node_modules/oxlint/bin/oxlint', '.']); // 'oxlint'
423
+ */
424
+ function detectLintHost(argv = process.argv) {
425
+ const names = argv.slice(1).map(binaryName);
426
+ if (names.includes("oxlint")) return "oxlint";
427
+ if (names.includes("eslint")) return "eslint";
428
+ return "unknown";
429
+ }
430
+ function defaultInteractive() {
431
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
432
+ }
433
+ function skipReasonFor(env, argv, interactive) {
434
+ if (env[IN_FLIGHT_ENV] === "true") return "already-offered";
435
+ if (env.VITEST || env.NODE_ENV === "test") return "running-under-test";
436
+ if (env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true" || env[NO_PROMPT_ENV] === "true") return "suppressed";
437
+ if (env.CI) return "running-in-ci";
438
+ if (detectLintHost(argv) === "oxlint") return "host-is-oxlint";
439
+ if (!interactive) return "no-tty";
440
+ return null;
441
+ }
442
+ function manifestNameAt(directory) {
443
+ const manifestPath = (0, node_path.join)(directory, "package.json");
444
+ if (!(0, node_fs.existsSync)(manifestPath)) return null;
445
+ try {
446
+ return JSON.parse((0, node_fs.readFileSync)(manifestPath, "utf8")).name ?? null;
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+ function packageRootFrom(startDir) {
452
+ let current = startDir;
453
+ for (let depth = 0; depth < 10; depth += 1) {
454
+ if (manifestNameAt(current) === PACKAGE_NAME) return current;
455
+ const parent = (0, node_path.dirname)(current);
456
+ if (parent === current) return null;
457
+ current = parent;
458
+ }
459
+ return null;
460
+ }
461
+ /**
462
+ * Absolute path to this package's own built CLI, or `null` when it is not on disk. Resolved by
463
+ * walking up from `startDir` — this module's own directory by default — rather than through
464
+ * `require.resolve`, so it works from `dist/` and from `src/` alike.
465
+ * @example
466
+ * resolveCliEntry(); // '/app/node_modules/@useinsider/eslint-config/dist/cli.cjs'
467
+ */
468
+ function resolveCliEntry(startDir) {
469
+ const root = packageRootFrom(startDir ?? (0, node_path.dirname)((0, node_url.fileURLToPath)({}.url)));
470
+ if (!root) return null;
471
+ const entry = (0, node_path.resolve)(root, CLI_RELATIVE_PATH);
472
+ return (0, node_fs.existsSync)(entry) ? entry : null;
473
+ }
474
+ function defaultRunCli(entry, args, cwd, env) {
475
+ const result = (0, node_child_process.spawnSync)(process.execPath, [entry, ...args], {
476
+ cwd,
477
+ env,
478
+ stdio: "inherit"
479
+ });
480
+ if (result.error) return 1;
481
+ return result.status ?? 1;
482
+ }
483
+ function defaultWarn(message) {
484
+ console.error(message);
485
+ }
486
+ function outdatedNotice(diagnosis) {
487
+ const config = diagnosis.eslintConfig ?? "eslint.config.*";
488
+ return [
489
+ `@useinsider/eslint-config: ${config} still uses the v3 single-file layout.`,
490
+ ` ${(0, node_path.join)(diagnosis.directory, config)} calls \`useInsider()\` itself, and`,
491
+ ` ${diagnosis.missing.join(" and ")} ${diagnosis.missing.length === 1 ? "is" : "are"} missing.`,
492
+ "",
493
+ "Starting the guided migration. It shows every change before applying it.",
494
+ `Skip it with ${NO_PROMPT_ENV}=true.`,
495
+ ""
496
+ ].join("\n");
497
+ }
498
+ var offerAttempted = false;
499
+ function withDefaults(deps) {
500
+ return {
501
+ env: deps.env ?? process.env,
502
+ argv: deps.argv ?? process.argv,
503
+ interactive: deps.interactive ?? defaultInteractive(),
504
+ resolveCliEntry: deps.resolveCliEntry ?? resolveCliEntry,
505
+ runCli: deps.runCli ?? defaultRunCli,
506
+ warn: deps.warn ?? defaultWarn
507
+ };
508
+ }
509
+ /**
510
+ * Offers the guided migration for an outdated layout and runs it when the consumer accepts. Declines
511
+ * silently — returning a skip reason instead of prompting — whenever writing to stdout would be
512
+ * wrong: under oxlint, in CI, without a TTY, or when already inside a migration.
513
+ * @example
514
+ * offerMigration(diagnoseLayout('/repo')).accepted; // true after the consumer confirmed
515
+ */
516
+ function offerMigration(diagnosis, deps = {}) {
517
+ const resolved = withDefaults(deps);
518
+ if (offerAttempted) return {
519
+ offered: false,
520
+ skipReason: "already-offered"
521
+ };
522
+ const skipReason = skipReasonFor(resolved.env, resolved.argv, resolved.interactive);
523
+ if (skipReason) return {
524
+ offered: false,
525
+ skipReason
526
+ };
527
+ const entry = resolved.resolveCliEntry();
528
+ if (!entry) return {
529
+ offered: false,
530
+ skipReason: "cli-not-found"
531
+ };
532
+ offerAttempted = true;
533
+ resolved.warn(outdatedNotice(diagnosis));
534
+ return {
535
+ offered: true,
536
+ accepted: resolved.runCli(entry, MIGRATE_FROM_LINT_ARGS, diagnosis.directory, {
537
+ ...resolved.env,
538
+ [IN_FLIGHT_ENV]: "true"
539
+ }) === 0
540
+ };
541
+ }
542
+ //#endregion
543
+ //#region src/utils/ensureConfigStructure.ts
544
+ var ANCHOR_FILE_REGEX = /(?:insider\.lint|eslint\.config)\.[mc]?[jt]s(?:$|\?)/;
545
+ var FILE_URL_PREFIX = "file://";
76
546
  var bypassWarnEmitted = false;
77
547
  function computeSelfModulePath() {
78
548
  const raw = (0, node_url.fileURLToPath)({}.url);
@@ -87,7 +557,7 @@ function isPathSpecifier(source) {
87
557
  return source.startsWith(".") || source.startsWith("/");
88
558
  }
89
559
  function defaultIsInsiderImportSource(source, configPath) {
90
- if (source === INSIDER_IMPORT_SOURCE) return true;
560
+ if (source === "@useinsider/eslint-config") return true;
91
561
  if (!configPath || !isPathSpecifier(source)) return false;
92
562
  try {
93
563
  const absolute = (0, node_path.resolve)((0, node_path.dirname)(configPath), source);
@@ -102,24 +572,24 @@ function locOf(node) {
102
572
  column: node.loc.start.column
103
573
  };
104
574
  }
105
- function validateCallExpression(callExpr) {
575
+ function validateCallExpression(callExpr, expectedCallee) {
106
576
  const { callee } = callExpr;
107
- if (callee.type !== _typescript_eslint_types.AST_NODE_TYPES.Identifier || callee.name !== TARGET_CALLEE_NAME) return [{
577
+ if (callee.type !== _typescript_eslint_types.AST_NODE_TYPES.Identifier || callee.name !== expectedCallee) return [{
108
578
  ...locOf(callExpr),
109
- reason: "Default export must call `useInsider(...)` directly."
579
+ reason: `Default export must call \`${expectedCallee}(...)\` directly.`
110
580
  }];
111
581
  return [];
112
582
  }
113
- function validateExportedCall(callee, fallbackLoc) {
583
+ function validateExportedCall(callee, fallbackLoc, expectedCallee) {
114
584
  let target = callee;
115
585
  if (target.type === _typescript_eslint_types.AST_NODE_TYPES.AwaitExpression) target = target.argument;
116
- if (target.type === _typescript_eslint_types.AST_NODE_TYPES.CallExpression) return validateCallExpression(target);
586
+ if (target.type === _typescript_eslint_types.AST_NODE_TYPES.CallExpression) return validateCallExpression(target, expectedCallee);
117
587
  if (target.type === _typescript_eslint_types.AST_NODE_TYPES.ArrayExpression) return [{
118
588
  ...locOf(target),
119
- reason: "Default export must not wrap `useInsider(...)` in an array. Use the typed options object instead."
589
+ reason: `Default export must not wrap \`${expectedCallee}(...)\` in an array.`
120
590
  }];
121
591
  if (target.type === _typescript_eslint_types.AST_NODE_TYPES.LogicalExpression || target.type === _typescript_eslint_types.AST_NODE_TYPES.ConditionalExpression) {
122
- const reason = ["Default export must not gate `useInsider(...)` behind a logical/conditional expression.", "Use the typed options object instead."].join(" ");
592
+ const reason = [`Default export must not gate \`${expectedCallee}(...)\` behind a logical/conditional expression.`, "Use the typed options object instead."].join(" ");
123
593
  return [{
124
594
  ...locOf(target),
125
595
  reason
@@ -127,7 +597,7 @@ function validateExportedCall(callee, fallbackLoc) {
127
597
  }
128
598
  return [{
129
599
  ...fallbackLoc,
130
- reason: "Default export must be a direct call to `useInsider(...)`."
600
+ reason: `Default export must be a direct call to \`${expectedCallee}(...)\`.`
131
601
  }];
132
602
  }
133
603
  function collectInsiderBindings(node, bindings, isInsiderSource) {
@@ -148,7 +618,7 @@ function classifyExpressionStatement(node, insiderBindings) {
148
618
  }
149
619
  };
150
620
  }
151
- function classifyStatement(node, exportSeen, insiderBindings) {
621
+ function classifyStatement(node, exportSeen, insiderBindings, expectedCallee) {
152
622
  if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration || node.type === _typescript_eslint_types.AST_NODE_TYPES.VariableDeclaration) return { type: "ignore" };
153
623
  if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ExpressionStatement) return classifyExpressionStatement(node, insiderBindings);
154
624
  if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ExportDefaultDeclaration) {
@@ -162,13 +632,13 @@ function classifyStatement(node, exportSeen, insiderBindings) {
162
632
  const declaration = node.declaration;
163
633
  return {
164
634
  type: "export",
165
- violations: validateExportedCall(declaration, locOf(node))
635
+ violations: validateExportedCall(declaration, locOf(node), expectedCallee)
166
636
  };
167
637
  }
168
638
  const reason = [
169
639
  `Disallowed top-level statement \`${node.type}\`.`,
170
640
  "Only imports, variable declarations, helper calls from `@useinsider/eslint-config`,",
171
- "and one `export default useInsider(...)` are allowed."
641
+ `and one \`export default ${expectedCallee}()\` are allowed.`
172
642
  ].join(" ");
173
643
  return {
174
644
  type: "disallowed",
@@ -183,18 +653,112 @@ function validateProgramStructure(program, configPath = "", options = {}) {
183
653
  const insiderBindings = /* @__PURE__ */ new Set();
184
654
  const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
185
655
  const isInsiderSource = (source) => resolver(source, configPath);
656
+ const expectedCallee = options.expectedCallee ?? "useInsiderESLint";
186
657
  let exportSeen = false;
187
658
  program.body.forEach((node) => {
188
659
  if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
189
- const result = classifyStatement(node, exportSeen, insiderBindings);
660
+ const result = classifyStatement(node, exportSeen, insiderBindings, expectedCallee);
190
661
  if (result.type === "ignore") return;
191
662
  if (result.type === "export") {
192
663
  exportSeen = true;
193
664
  violations.push(...result.violations);
194
665
  return;
195
666
  }
196
- if (result.type === "duplicateExport") {
197
- violations.push(result.violation);
667
+ violations.push(result.violation);
668
+ });
669
+ if (!exportSeen) violations.push({
670
+ line: 1,
671
+ column: 0,
672
+ reason: `Missing \`export default ${options.expectedCallee ?? "useInsiderESLint"}()\`.`
673
+ });
674
+ return violations;
675
+ }
676
+ function validateRendererPattern(pattern) {
677
+ const bound = /* @__PURE__ */ new Set();
678
+ const violations = [];
679
+ pattern.properties.forEach((property) => {
680
+ if (property.type !== _typescript_eslint_types.AST_NODE_TYPES.Property || property.computed || !property.shorthand) {
681
+ violations.push({
682
+ ...locOf(property),
683
+ reason: `Destructure \`${ESLINT_RENDERER_NAME}\` and \`${OXC_RENDERER_NAME}\` by name, without renaming or spreading.`
684
+ });
685
+ return;
686
+ }
687
+ bound.add(property.key.name);
688
+ });
689
+ [ESLINT_RENDERER_NAME, OXC_RENDERER_NAME].forEach((name) => {
690
+ if (!bound.has(name)) violations.push({
691
+ ...locOf(pattern),
692
+ reason: `Missing \`${name}\` in the destructured \`${TARGET_CALLEE_NAME}(...)\` result.`
693
+ });
694
+ });
695
+ return violations;
696
+ }
697
+ function validateRendererExport(node) {
698
+ const { declaration } = node;
699
+ if (!declaration || declaration.type !== _typescript_eslint_types.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") return [{
700
+ ...locOf(node),
701
+ reason: `Renderer export must be \`${RENDERER_EXPORT};\`.`
702
+ }];
703
+ if (declaration.declarations.length !== 1) return [{
704
+ ...locOf(declaration),
705
+ reason: "Renderer export must declare exactly one binding."
706
+ }];
707
+ const [declarator] = declaration.declarations;
708
+ if (declarator.id.type !== _typescript_eslint_types.AST_NODE_TYPES.ObjectPattern) return [{
709
+ ...locOf(declarator.id),
710
+ reason: `Renderer export must destructure \`${ESLINT_RENDERER_NAME}\` and \`${OXC_RENDERER_NAME}\`.`
711
+ }];
712
+ const patternViolations = validateRendererPattern(declarator.id);
713
+ const { init } = declarator;
714
+ if (!init) return [...patternViolations, {
715
+ ...locOf(declarator),
716
+ reason: `Renderer export must be initialized by a direct \`${TARGET_CALLEE_NAME}(...)\` call.`
717
+ }];
718
+ return [...patternViolations, ...validateExportedCall(init, locOf(declarator), TARGET_CALLEE_NAME)];
719
+ }
720
+ function classifyLintModuleStatement(node, exportSeen, insiderBindings) {
721
+ if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration || node.type === _typescript_eslint_types.AST_NODE_TYPES.VariableDeclaration) return { type: "ignore" };
722
+ if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ExpressionStatement) return classifyExpressionStatement(node, insiderBindings);
723
+ if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ExportNamedDeclaration) {
724
+ if (exportSeen) return {
725
+ type: "duplicateExport",
726
+ violation: {
727
+ ...locOf(node),
728
+ reason: "Only one export is allowed."
729
+ }
730
+ };
731
+ return {
732
+ type: "export",
733
+ violations: validateRendererExport(node)
734
+ };
735
+ }
736
+ const reason = [
737
+ `Disallowed top-level statement \`${node.type}\`.`,
738
+ "Only imports, variable declarations, helper calls from `@useinsider/eslint-config`,",
739
+ `and one \`${RENDERER_EXPORT}\` are allowed.`
740
+ ].join(" ");
741
+ return {
742
+ type: "disallowed",
743
+ violation: {
744
+ ...locOf(node),
745
+ reason
746
+ }
747
+ };
748
+ }
749
+ function validateLintModuleStructure(program, configPath = "", options = {}) {
750
+ const violations = [];
751
+ const insiderBindings = /* @__PURE__ */ new Set();
752
+ const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
753
+ const isInsiderSource = (source) => resolver(source, configPath);
754
+ let exportSeen = false;
755
+ program.body.forEach((node) => {
756
+ if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
757
+ const result = classifyLintModuleStatement(node, exportSeen, insiderBindings);
758
+ if (result.type === "ignore") return;
759
+ if (result.type === "export") {
760
+ exportSeen = true;
761
+ violations.push(...result.violations);
198
762
  return;
199
763
  }
200
764
  violations.push(result.violation);
@@ -202,7 +766,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
202
766
  if (!exportSeen) violations.push({
203
767
  line: 1,
204
768
  column: 0,
205
- reason: "Missing `export default useInsider(...)`."
769
+ reason: `Missing \`${RENDERER_EXPORT};\`.`
206
770
  });
207
771
  return violations;
208
772
  }
@@ -226,7 +790,7 @@ function extractMatchesFromStack(frames) {
226
790
  const matches = /* @__PURE__ */ new Set();
227
791
  frames.forEach((frame) => {
228
792
  const name = normalizeFrameFilename(frame.getFileName());
229
- if (name && CONFIG_FILE_REGEX.test(name)) matches.add(name);
793
+ if (name && ANCHOR_FILE_REGEX.test(name)) matches.add(name);
230
794
  });
231
795
  return Array.from(matches);
232
796
  }
@@ -239,13 +803,16 @@ function argvCandidateAt(argv, index) {
239
803
  function extractConfigFromArgv(argv) {
240
804
  for (let i = 0; i < argv.length; i += 1) {
241
805
  const candidate = argvCandidateAt(argv, i);
242
- if (candidate && CONFIG_FILE_REGEX.test(candidate)) return candidate;
806
+ if (candidate && ANCHOR_FILE_REGEX.test(candidate)) return candidate;
243
807
  }
244
808
  return null;
245
809
  }
810
+ var LINT_MODULE_REGEX = /insider\.lint\.[mc]?[jt]s(?:$|\?)/;
246
811
  function getConsumerConfigPath(frames = readStackFrames(), argv = process.argv) {
247
812
  const stackMatches = extractMatchesFromStack(frames);
248
813
  if (stackMatches.length === 1) return stackMatches[0];
814
+ const lintModules = stackMatches.filter((match) => LINT_MODULE_REGEX.test(match));
815
+ if (lintModules.length === 1) return lintModules[0];
249
816
  const fromArgv = extractConfigFromArgv(argv);
250
817
  if (fromArgv) return fromArgv;
251
818
  return null;
@@ -255,11 +822,12 @@ function captureConsumerConfigDir(configPath) {
255
822
  consumerConfigDir = configPath === null ? null : (0, node_path.dirname)((0, node_path.resolve)(configPath));
256
823
  }
257
824
  /**
258
- * Directory holding the consumer's `eslint.config.*`, captured by {@link ensureConfigStructure}.
259
- * This is the anchor `tsconfigPaths` entries are written against; it falls back to `process.cwd()`
260
- * when the config file could not be located.
825
+ * Directory holding the consumer's `insider.lint.*` module and its sibling linter configs, captured
826
+ * by {@link ensureConfigStructure}. This is the anchor `tsconfigPaths` entries are written against,
827
+ * and where {@link import('./ensureOxlintConfig').ensureOxlintConfig} looks for the oxlint config;
828
+ * it falls back to `process.cwd()` when the module could not be located.
261
829
  * @example
262
- * // consumer config at /repo/eslint.config.mjs, `eslint` launched from /repo/app
830
+ * // consumer module at /repo/insider.lint.ts, `eslint` launched from /repo/app
263
831
  * getConsumerConfigDir(); // '/repo'
264
832
  */
265
833
  function getConsumerConfigDir() {
@@ -270,12 +838,12 @@ function formatReport(configPath, violations) {
270
838
  const lines = violations.map((violation) => ` - [${violation.line}:${violation.column}] ${violation.reason}`);
271
839
  const footer = [
272
840
  "",
273
- "Allowed structure:",
274
- " - import statements",
275
- " - const/let/var declarations",
276
- " - exactly one `export default useInsider({ ... })`",
841
+ "Alongside the export, only import statements, const/let/var declarations and helper calls",
842
+ "imported from `@useinsider/eslint-config` are allowed.",
843
+ "",
844
+ EXPECTED_LAYOUT,
277
845
  "",
278
- "Emergency bypass: set INSIDER_SKIP_CONFIG_VALIDATION=true (do not commit)."
846
+ `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
279
847
  ].join("\n");
280
848
  return `${header}\n${lines.join("\n")}\n${footer}`;
281
849
  }
@@ -316,21 +884,57 @@ function parseConsumerSource(configPath, source) {
316
884
  return fail(`@useinsider/eslint-config: failed to parse consumer config at ${configPath}: ${message}`);
317
885
  }
318
886
  }
319
- function runValidation(configPath) {
320
- if (!configPath) fail(["@useinsider/eslint-config: unable to locate consumer `eslint.config.*` file for structure validation.", "If this is an unexpected environment, set INSIDER_SKIP_CONFIG_VALIDATION=true (do not commit)."].join("\n"));
321
- const violations = validateProgramStructure(parseConsumerSource(configPath, readConsumerSource(configPath)), configPath);
887
+ function resolveSibling(directory, filenames) {
888
+ const found = filenames.find((filename) => (0, node_fs.existsSync)((0, node_path.resolve)(directory, filename)));
889
+ return found ? (0, node_path.resolve)(directory, found) : null;
890
+ }
891
+ function validateFile(filePath, validator, options) {
892
+ const violations = validator(parseConsumerSource(filePath, readConsumerSource(filePath)), filePath, options);
322
893
  if (violations.length === 0) return;
323
- fail(formatReport(configPath, violations));
894
+ fail(formatReport(filePath, violations));
324
895
  }
325
- function ensureConfigStructure(overridePath) {
896
+ var MIGRATED_EXIT_NOTICE = [
897
+ "@useinsider/eslint-config: migration finished.",
898
+ "",
899
+ "Re-run your lint command. This process already loaded the old configuration, so continuing now",
900
+ "would lint against a config that no longer exists on disk."
901
+ ].join("\n");
902
+ var DECLINED_DETAIL = ["The guided migration was not completed, so nothing was written."];
903
+ function failWithGuidance(headline, diagnosis, detail) {
904
+ return fail(formatLayoutGuidance({
905
+ headline,
906
+ diagnosis,
907
+ detail
908
+ }));
909
+ }
910
+ function handleOutdatedLayout(headline, diagnosis, offer) {
911
+ if (!diagnosis.autoMigratable) return failWithGuidance(headline, diagnosis);
912
+ const outcome = offer(diagnosis);
913
+ if (!outcome.offered) return failWithGuidance(headline, diagnosis);
914
+ if (!outcome.accepted) return failWithGuidance(headline, diagnosis, DECLINED_DETAIL);
915
+ return fail(MIGRATED_EXIT_NOTICE);
916
+ }
917
+ function runValidation(anchorPath, deps = {}) {
918
+ const diagnose = deps.diagnose ?? ((directory) => diagnoseLayout(directory));
919
+ const offer = deps.offer ?? offerMigration;
920
+ if (!anchorPath) handleOutdatedLayout("unable to locate the consumer `insider.lint.*` module for structure validation.", diagnose(process.cwd()), offer);
921
+ const directory = (0, node_path.dirname)((0, node_path.resolve)(anchorPath));
922
+ const lintModulePath = resolveSibling(directory, LINT_MODULE_FILENAMES);
923
+ if (!lintModulePath) handleOutdatedLayout(`no \`insider.lint.*\` module found in ${directory}.`, diagnose(directory), offer);
924
+ validateFile(lintModulePath, validateLintModuleStructure, {});
925
+ const eslintConfigPath = resolveSibling(directory, ESLINT_CONFIG_FILENAMES);
926
+ if (!eslintConfigPath) failWithGuidance(`no \`eslint.config.*\` file found in ${directory}.`, diagnose(directory), [`\`${TARGET_CALLEE_NAME}()\` returns renderers, not a flat-config array, so ESLint still`, `needs a config file whose default export is \`${ESLINT_RENDERER_NAME}()\`.`]);
927
+ validateFile(eslintConfigPath, validateProgramStructure, { expectedCallee: ESLINT_RENDERER_NAME });
928
+ }
929
+ function ensureConfigStructure(overridePath, deps = {}) {
326
930
  const configPath = overridePath === void 0 ? getConsumerConfigPath() : overridePath;
327
931
  captureConsumerConfigDir(configPath);
328
- if (process.env[OPT_OUT_ENV] === "true") {
932
+ if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") {
329
933
  emitBypassWarning();
330
934
  return;
331
935
  }
332
936
  try {
333
- runValidation(configPath);
937
+ runValidation(configPath, deps);
334
938
  } catch (error) {
335
939
  if (error instanceof ConfigStructureExit) return;
336
940
  throw error;
@@ -366,52 +970,52 @@ function applyVueOverrides(block, input) {
366
970
  //#region src/builders/scopeBlocks.ts
367
971
  var scopeMap = {
368
972
  async javascript() {
369
- return Promise.resolve().then(() => require("./javascript-VBXqjVi8.cjs"));
973
+ return Promise.resolve().then(() => require("./javascript-DH8-N38w.cjs"));
370
974
  },
371
975
  async javascriptDom() {
372
- return Promise.resolve().then(() => require("./javascript-dom-5jRL_yKP.cjs"));
976
+ return Promise.resolve().then(() => require("./javascript-dom-CvtQ6_9W.cjs"));
373
977
  },
374
978
  async javascriptNode() {
375
- return Promise.resolve().then(() => require("./javascript-node-C1AU_4X2.cjs"));
979
+ return Promise.resolve().then(() => require("./javascript-node-BNsy3loY.cjs"));
376
980
  },
377
981
  async typescript() {
378
- return Promise.resolve().then(() => require("./typescript-C1icmOxY.cjs"));
982
+ return Promise.resolve().then(() => require("./typescript-D3VeLnpn.cjs"));
379
983
  },
380
984
  async typescriptDom() {
381
- return Promise.resolve().then(() => require("./typescript-dom-RUJn2WCZ.cjs"));
985
+ return Promise.resolve().then(() => require("./typescript-dom-iB_xBTAD.cjs"));
382
986
  },
383
987
  async typescriptNode() {
384
- return Promise.resolve().then(() => require("./typescript-node-CBEzcc56.cjs"));
988
+ return Promise.resolve().then(() => require("./typescript-node-Cq6qhj53.cjs"));
385
989
  },
386
990
  async vue2() {
387
- return Promise.resolve().then(() => require("./vue2-azKwRgaJ.cjs"));
991
+ return Promise.resolve().then(() => require("./vue2-Da1i_oVn.cjs"));
388
992
  },
389
993
  async vue3() {
390
- return Promise.resolve().then(() => require("./vue3-Df5UQ1Ys.cjs"));
994
+ return Promise.resolve().then(() => require("./vue3-621EM2Ii.cjs"));
391
995
  },
392
996
  async vue2Typescript() {
393
- return Promise.resolve().then(() => require("./vue2-typescript-CZSHzp7I.cjs"));
997
+ return Promise.resolve().then(() => require("./vue2-typescript-dXLRSGNP.cjs"));
394
998
  },
395
999
  async vue3Typescript() {
396
- return Promise.resolve().then(() => require("./vue3-typescript-C0O1JZlk.cjs"));
1000
+ return Promise.resolve().then(() => require("./vue3-typescript-DONe-IKz.cjs"));
397
1001
  },
398
1002
  async vue2OptionsApi() {
399
- return Promise.resolve().then(() => require("./vue2-options-api-ma3J-bvM.cjs"));
1003
+ return Promise.resolve().then(() => require("./vue2-options-api-BSdrUm8H.cjs"));
400
1004
  },
401
1005
  async vue3OptionsApi() {
402
- return Promise.resolve().then(() => require("./vue3-options-api-Bx7yYHza.cjs"));
1006
+ return Promise.resolve().then(() => require("./vue3-options-api-CC9_fAiq.cjs"));
403
1007
  },
404
1008
  async vue2TypescriptOptionsApi() {
405
- return Promise.resolve().then(() => require("./vue2-typescript-options-api-B8OzZlje.cjs"));
1009
+ return Promise.resolve().then(() => require("./vue2-typescript-options-api-LM7U5dWq.cjs"));
406
1010
  },
407
1011
  async vue3TypescriptOptionsApi() {
408
- return Promise.resolve().then(() => require("./vue3-typescript-options-api-cPal4O-K.cjs"));
1012
+ return Promise.resolve().then(() => require("./vue3-typescript-options-api-D4pqpDFs.cjs"));
409
1013
  },
410
1014
  async jest() {
411
- return Promise.resolve().then(() => require("./jest-CHzjzBtf.cjs"));
1015
+ return Promise.resolve().then(() => require("./jest-4Es9Ggaa.cjs"));
412
1016
  },
413
1017
  async vitest() {
414
- return Promise.resolve().then(() => require("./vitest-BomI8gtu.cjs"));
1018
+ return Promise.resolve().then(() => require("./vitest-BlGQswK2.cjs"));
415
1019
  },
416
1020
  async jsdocBasedTs() {
417
1021
  return Promise.resolve().then(() => require("./jsdoc-based-ts-Cfahz0nn.cjs"));
@@ -454,6 +1058,14 @@ function clonePreset(preset) {
454
1058
  function isScopeName(key) {
455
1059
  return Object.keys(scopeMap).includes(key);
456
1060
  }
1061
+ /**
1062
+ * Every scope name `useInsider()` accepts, for error messages that have to spell the options out.
1063
+ * @example
1064
+ * scopeNames().includes('typescriptNode'); // true
1065
+ */
1066
+ function scopeNames() {
1067
+ return Object.keys(scopeMap);
1068
+ }
457
1069
  function applyCommonEntryFields(block, entry) {
458
1070
  if (entry.ignores && entry.ignores.length > 0) block.ignores = [...entry.ignores];
459
1071
  if (entry.globals) {
@@ -469,7 +1081,7 @@ async function buildScopeBlocks(scope, entries) {
469
1081
  const preset = (await scopeMap[scope]()).default;
470
1082
  return entries.map((entry) => {
471
1083
  const block = clonePreset(preset);
472
- block.files = entry.files;
1084
+ block.files = [...entry.files];
473
1085
  if (tsScopes.has(scope)) {
474
1086
  const { tsconfigPaths } = entry;
475
1087
  if (tsconfigPaths) applyTypescriptOverrides(block, { tsconfigPaths });
@@ -503,7 +1115,7 @@ async function buildStrictBlocks(value) {
503
1115
  const { default: strictConfig } = await Promise.resolve().then(() => require("./strict-DnJFBY_B.cjs"));
504
1116
  const clone = { ...strictConfig };
505
1117
  if (clone.rules) clone.rules = { ...clone.rules };
506
- if (isFileScoped) clone.files = value;
1118
+ if (isFileScoped) clone.files = [...value];
507
1119
  else delete clone.files;
508
1120
  return [clone];
509
1121
  }
@@ -512,7 +1124,11 @@ async function buildStrictBlocks(value) {
512
1124
  var turnOff = false;
513
1125
  var scanned = false;
514
1126
  var silencedDependencies = /* @__PURE__ */ new Set();
515
- var fixedDependencies = ["eslint", "globals"];
1127
+ var fixedDependencies = [
1128
+ "eslint",
1129
+ "globals",
1130
+ "oxlint"
1131
+ ];
516
1132
  var require$1 = (0, node_module.createRequire)({}.url);
517
1133
  function getPackageJSONPath(currentDir) {
518
1134
  const fileLocation = (0, node_path.resolve)(currentDir, "package.json");
@@ -568,6 +1184,42 @@ function silenceDependencyWarning(input) {
568
1184
  input.forEach((dependency) => silencedDependencies.add(String(dependency)));
569
1185
  }
570
1186
  //#endregion
1187
+ //#region src/utils/ensureOxlintConfig.ts
1188
+ function formatMessage(directory) {
1189
+ return formatLayoutGuidance({
1190
+ headline: `no oxlint config found in ${directory}.`,
1191
+ diagnosis: diagnoseLayout(directory),
1192
+ detail: [
1193
+ "useInsiderESLint() turns off every rule oxlint owns, so running ESLint alone would silently",
1194
+ "drop those checks. Create the sibling oxlint config:",
1195
+ "",
1196
+ " // oxlint.config.ts",
1197
+ ` import { useInsiderOXC } from '${lintModuleSpecifier(directory)}';`,
1198
+ "",
1199
+ " export default await useInsiderOXC();",
1200
+ "",
1201
+ "and run both linters:",
1202
+ "",
1203
+ " \"lint\": \"oxlint --disable-nested-config && eslint .\"",
1204
+ "",
1205
+ `oxlint discovers only ${OXLINT_CONFIG_FILENAMES.join(" and ")} — any other extension is`,
1206
+ "ignored without a word."
1207
+ ]
1208
+ });
1209
+ }
1210
+ /**
1211
+ * Fails when the consumer enabled the ESLint-side oxlint suppression without a sibling oxlint
1212
+ * config to run the suppressed rules. Resolved against the directory
1213
+ * {@link getConsumerConfigDir} captured during `useInsider()`.
1214
+ * @example
1215
+ * ensureOxlintConfig(); // throws unless oxlint.config.{ts,mts} sits beside insider.lint.*
1216
+ */
1217
+ function ensureOxlintConfig(directory = getConsumerConfigDir()) {
1218
+ if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") return;
1219
+ if (OXLINT_CONFIG_FILENAMES.some((filename) => (0, node_fs.existsSync)((0, node_path.join)(directory, filename)))) return;
1220
+ throw new Error(formatMessage(directory));
1221
+ }
1222
+ //#endregion
571
1223
  //#region src/utils/silenceRules.ts
572
1224
  var globallySilencedRules = /* @__PURE__ */ new Set();
573
1225
  /**
@@ -587,14 +1239,25 @@ function silenceRule(config) {
587
1239
  const [, ...restConfig] = Array.isArray(config) ? config : [];
588
1240
  return ["warn", ...restConfig];
589
1241
  }
1242
+ /**
1243
+ * Reports a silenced rule name that no emitted block declares, which is how a typo in
1244
+ * `silenceRules` stays visible. The caller passes the union of every block's rule keys: a single
1245
+ * block is not evidence, because the emission is many narrow blocks and a correctly spelled rule is
1246
+ * absent from nearly all of them.
1247
+ * @example
1248
+ * warnUnknownSilencedRules(['no-console'], ['no-unknown-rule']); // warns once
1249
+ */
1250
+ function warnUnknownSilencedRules(knownRules, silencedRules) {
1251
+ const known = new Set(knownRules);
1252
+ new Set(silencedRules).forEach((rule) => {
1253
+ if (known.has(rule)) return;
1254
+ (0, node_console.warn)(`Unknown rule "${rule}". Please remove it from the silenced rules list.`);
1255
+ });
1256
+ }
590
1257
  function filterSilencedRules(rules, silencedRules) {
591
1258
  let targetSilencedRules = silencedRules || globallySilencedRules;
592
1259
  if (Array.isArray(targetSilencedRules)) targetSilencedRules = new Set(targetSilencedRules);
593
1260
  if (!rules || !targetSilencedRules.size) return rules;
594
- if (silencedRules) targetSilencedRules.forEach((rule) => {
595
- if (rules[rule]) return;
596
- (0, node_console.warn)(`Unknown rule "${rule}". Please remove it from the silenced rules list.`);
597
- });
598
1261
  return Object.entries(rules).reduce((newRules, [key, value]) => {
599
1262
  if (value) newRules[key] = targetSilencedRules.has(key) && !isDisabled(value) ? silenceRule(value) : value;
600
1263
  return newRules;
@@ -617,6 +1280,11 @@ function assertNoRemovedKeys(config) {
617
1280
  const details = used.map((key) => ` - \`${key}\`: ${removedKeys.get(key)}`).join("\n");
618
1281
  throw new Error(`@useinsider/eslint-config: removed option(s) passed to useInsider():\n${details}`);
619
1282
  }
1283
+ function assertNoUnknownKeys(config) {
1284
+ const unknown = Object.keys(config).filter((key) => !reservedKeys.has(key) && !removedKeys.has(key) && !isScopeName(key));
1285
+ if (unknown.length === 0) return;
1286
+ throw new Error(`@useinsider/eslint-config: unknown option(s) passed to useInsider(): ${unknown.join(", ")}.\n Scopes: ${scopeNames().join(", ")}\n Other options: ${[...reservedKeys].join(", ")}`);
1287
+ }
620
1288
  function isProcessableScopeKey(key) {
621
1289
  return !reservedKeys.has(key) && isScopeName(key);
622
1290
  }
@@ -630,6 +1298,7 @@ async function collectScopeBlocks(config) {
630
1298
  }, Promise.resolve([]));
631
1299
  }
632
1300
  function applySilenceRules(blocks, perCallSilence) {
1301
+ if (perCallSilence) warnUnknownSilencedRules(blocks.flatMap((block) => Object.keys(block.rules ?? {})), perCallSilence);
633
1302
  blocks.forEach((block) => {
634
1303
  if (!block.rules) return;
635
1304
  block.rules = perCallSilence ? filterSilencedRules(block.rules, perCallSilence) : filterSilencedRules(block.rules);
@@ -639,29 +1308,45 @@ function buildIgnoresBlocks(ignores) {
639
1308
  if (!ignores || ignores.length === 0) return [];
640
1309
  return [{ ignores: [...ignores] }];
641
1310
  }
642
- async function useInsider(config) {
643
- assertNoRemovedKeys(config);
644
- ensureConfigStructure();
645
- await ensureDependencies();
646
- const ignoresBlocks = buildIgnoresBlocks(config.ignores);
1311
+ async function buildBaseBlocks(config) {
647
1312
  const scopeBlocks = await collectScopeBlocks(config);
648
- const jqueryBlocks = buildJqueryBlocks(config.jquery);
649
- const configFilesBlocks = buildConfigFilesBlocks(config.configFiles);
650
1313
  const strictBlocks = await buildStrictBlocks(config.strict);
651
- const spellCheckerBlocks = buildSpellCheckerBlocks(config.spellChecker);
652
- const blocks = [
653
- ...ignoresBlocks,
1314
+ return [
1315
+ ...buildIgnoresBlocks(config.ignores),
1316
+ ...buildUnusedDirectiveBlocks(),
654
1317
  ...scopeBlocks,
655
- ...jqueryBlocks,
656
- ...configFilesBlocks,
1318
+ ...buildJqueryBlocks(config.jquery),
1319
+ ...buildConfigFilesBlocks(config.configFiles),
657
1320
  ...strictBlocks,
658
- ...spellCheckerBlocks
1321
+ ...buildLintTopologyBlocks(),
1322
+ ...buildSpellCheckerBlocks(config.spellChecker)
659
1323
  ];
1324
+ }
1325
+ async function renderEslint(config) {
1326
+ await ensureDependencies();
1327
+ ensureOxlintConfig();
1328
+ const baseBlocks = await buildBaseBlocks(config);
1329
+ const blocks = [...baseBlocks, ...buildOxlintSuppressionBlocks(baseBlocks)];
660
1330
  applySilenceRules(blocks, config.silenceRules);
661
1331
  return (0, eslint_config.defineConfig)(blocks);
662
1332
  }
1333
+ async function renderOxlint(config) {
1334
+ const baseBlocks = await buildBaseBlocks(config);
1335
+ applySilenceRules(baseBlocks, config.silenceRules);
1336
+ return buildOxlintConfig(baseBlocks, config.ignores);
1337
+ }
1338
+ function useInsider(config) {
1339
+ assertNoRemovedKeys(config);
1340
+ assertNoUnknownKeys(config);
1341
+ ensureConfigStructure();
1342
+ return {
1343
+ useInsiderESLint: () => renderEslint(config),
1344
+ useInsiderOXC: () => renderOxlint(config)
1345
+ };
1346
+ }
663
1347
  //#endregion
664
1348
  exports.ensureConfigStructure = ensureConfigStructure;
1349
+ exports.ensureOxlintConfig = ensureOxlintConfig;
665
1350
  exports.merge = require_merge.merge;
666
1351
  exports.mergeAll = require_mergeAllConfig.mergeAll;
667
1352
  exports.silenceDependencyWarning = silenceDependencyWarning;