@nx/oxlint 23.2.0-rc.1 → 23.2.0-rc.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.
@@ -1,4 +1,4 @@
1
- import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, calculateHashesForCreateNodes, combineGlobPatterns, globWithWorkspaceContext, hashObject, PluginCache, TargetProjectLocator, workspaceDataDirectory, } from '@nx/devkit/internal';
1
+ import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, calculateHashesForCreateNodes, combineGlobPatterns, globWithWorkspaceContext, hashObject, PluginCache, TargetProjectLocator, workspaceDataDirectory, quoteShellArg, } from '@nx/devkit/internal';
2
2
  import { createNodesFromFiles, DependencyType, detectPackageManager, getPackageManagerCommand, readJsonFile, validateDependency, } from '@nx/devkit';
3
3
  import { getLockFileName, getRootTsConfigFileName } from '@nx/js';
4
4
  import { walkTsconfigExtendsChain, } from '@nx/js/internal';
@@ -36,7 +36,7 @@ const OXLINT_CONFIG_GLOB = combineGlobPatterns([
36
36
  ...OXLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
37
37
  ...PROJECT_CONFIG_FILENAMES.map((f) => `**/${f}`),
38
38
  ]);
39
- const internalCreateNodes = async (configFilePath, options, context, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot, projectsCache, hashByRoot, pmc, rootConfig) => {
39
+ const internalCreateNodes = async (configFilePath, options, context, projectRootsByOxlintRoots, nestedRootsByParent, getLintableFilesPerProjectRoot, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot, projectsCache, hashByRoot, pmc, rootConfig) => {
40
40
  const configDir = dirname(configFilePath);
41
41
  // Collect each project root's contribution in parallel, but write them into
42
42
  // `projects` afterwards in input order so insertion order (and therefore
@@ -57,7 +57,7 @@ const internalCreateNodes = async (configFilePath, options, context, projectRoot
57
57
  projectsCache.set(hash, {});
58
58
  return null;
59
59
  }
60
- const project = getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot.get(projectRoot) ?? [], rootConfig);
60
+ const project = getProjectUsingOxlintConfig(configFilePath, projectRoot, nestedRootsByParent, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot.get(projectRoot) ?? [], rootConfig);
61
61
  if (project) {
62
62
  const entry = { [projectRoot]: project };
63
63
  projectsCache.set(hash, entry);
@@ -82,6 +82,7 @@ export const createNodes = [
82
82
  const cachePath = join(workspaceDataDirectory, `oxlint-${optionsHash}.hash`);
83
83
  const targetsCache = new PluginCache(cachePath);
84
84
  const { oxlintConfigFiles, projectRoots, projectRootsByOxlintRoots } = splitConfigFiles(configFiles, context.workspaceRoot);
85
+ const nestedRootsByParent = nestedRootsByParentRoot(projectRoots);
85
86
  // The glob also matches `**/package.json`, so this callback runs in every
86
87
  // workspace, Oxlint or not. Bail before the chain walks and the hashing.
87
88
  if (oxlintConfigFiles.length === 0) {
@@ -128,7 +129,7 @@ export const createNodes = [
128
129
  }));
129
130
  const hashByRoot = new Map(projectRoots.map((r, i) => [r, hashes[i]]));
130
131
  try {
131
- return await createNodesFromFiles((configFile, fileOptions, fileContext) => internalCreateNodes(configFile, fileOptions, fileContext, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot, targetsCache, hashByRoot, pmc, rootConfig), oxlintConfigFiles, options, context);
132
+ return await createNodesFromFiles((configFile, fileOptions, fileContext) => internalCreateNodes(configFile, fileOptions, fileContext, projectRootsByOxlintRoots, nestedRootsByParent, getLintableFilesPerProjectRoot, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot, targetsCache, hashByRoot, pmc, rootConfig), oxlintConfigFiles, options, context);
132
133
  }
133
134
  finally {
134
135
  targetsCache.writeToDisk();
@@ -539,7 +540,69 @@ function ancestorIgnorePaths(projectRoot) {
539
540
  }
540
541
  return result;
541
542
  }
542
- // Only the keys are read, so the value type is left open for both callers.
543
+ /**
544
+ * Escape the gitignore metacharacters in a path so `--ignore-pattern` matches it
545
+ * literally. The value crosses two languages and `quoteShellArg` only covers the
546
+ * shell: to Oxlint's matcher `\`, `[`, `]`, `*` and `?` are pattern syntax, and a
547
+ * trailing space is stripped unless escaped — so `/a[b]` excludes `ab` while
548
+ * leaving `a[b]` walked, which is both a miss and a silent over-exclusion.
549
+ */
550
+ function escapeIgnorePattern(pattern) {
551
+ return pattern
552
+ .replace(/([\\[\]*?])/g, '\\$1')
553
+ .replace(/ +$/, (spaces) => spaces.replace(/ /g, '\\ '));
554
+ }
555
+ /**
556
+ * Direct child project roots for each project root, keyed by the parent. A root
557
+ * belongs to its NEAREST enclosing root, so a grandchild lands under the child
558
+ * rather than the grandparent — which is what keeps an outer project from
559
+ * emitting an exclusion that a shallower one already covers.
560
+ *
561
+ * Built once per run: resolving each root's parent by walking up is linear in
562
+ * the workspace, where asking every project which roots sit below it is not.
563
+ */
564
+ function nestedRootsByParentRoot(projectRoots) {
565
+ // getRootForDirectory reads only the keys.
566
+ const roots = new Map(projectRoots.map((root) => [root, true]));
567
+ const byParent = new Map();
568
+ for (const root of projectRoots) {
569
+ const parent = getRootForDirectory(dirname(root), roots);
570
+ // A workspace-root project is its own ancestor, so it must not claim itself.
571
+ if (parent === null || parent === root) {
572
+ continue;
573
+ }
574
+ const claimed = byParent.get(parent);
575
+ if (claimed) {
576
+ claimed.push(root);
577
+ }
578
+ else {
579
+ byParent.set(parent, [root]);
580
+ }
581
+ }
582
+ for (const claimed of byParent.values()) {
583
+ claimed.sort();
584
+ }
585
+ return byParent;
586
+ }
587
+ /**
588
+ * The project roots nested directly below `projectRoot`, relative to it and
589
+ * limited to `lintDir` when the lint walk starts there. An excluded root either
590
+ * has its own inferred target or owns nothing lintable, so pruning it drops no
591
+ * lint coverage.
592
+ *
593
+ * Callers must emit each one anchored (`/dir`) and never as `dir/**`. A gitignore
594
+ * pattern is anchored only when it contains a slash, so a bare single-segment
595
+ * root would also match a same-named directory the outer project owns; and
596
+ * `dir/**` matches only entries inside `dir`, leaving Oxlint free to descend and
597
+ * read that directory's ignore files.
598
+ */
599
+ function nestedProjectRoots(projectRoot, nestedRootsByParent, lintDir) {
600
+ const prefix = projectRoot === '.' ? '' : `${projectRoot}/`;
601
+ return (nestedRootsByParent.get(projectRoot) ?? [])
602
+ .map((root) => root.slice(prefix.length))
603
+ .filter((rel) => !lintDir || rel === lintDir || rel.startsWith(`${lintDir}/`));
604
+ }
605
+ // Only the keys are read, so the value type is left open for all callers.
543
606
  function getRootForDirectory(directory, roots) {
544
607
  let currentPath = normalize(directory);
545
608
  while (currentPath !== dirname(currentPath)) {
@@ -550,7 +613,7 @@ function getRootForDirectory(directory, roots) {
550
613
  }
551
614
  return roots.has(currentPath) ? currentPath : null;
552
615
  }
553
- function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
616
+ function getProjectUsingOxlintConfig(configFilePath, projectRoot, nestedRootsByParent, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
554
617
  let standaloneSrcPath;
555
618
  if (projectRoot === '.' &&
556
619
  existsSync(join(context.workspaceRoot, projectRoot, 'package.json'))) {
@@ -577,11 +640,17 @@ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, conte
577
640
  ];
578
641
  const isRootProject = projectRoot === '.';
579
642
  const lintPath = isRootProject && standaloneSrcPath ? `./${standaloneSrcPath}` : '.';
643
+ const nestedIgnoreArgs = nestedProjectRoots(projectRoot, nestedRootsByParent, isRootProject && standaloneSrcPath ? standaloneSrcPath : '')
644
+ // `quoteShellArg` documents that it cannot keep `%` literal through cmd.exe,
645
+ // so on Windows such a root is left unexcluded rather than excluded wrongly:
646
+ // it gets linted twice, which is what happened before this exclusion existed.
647
+ .filter((relativeRoot) => process.platform !== 'win32' || !relativeRoot.includes('%'))
648
+ .map((relativeRoot) => `--ignore-pattern ${quoteShellArg(escapeIgnorePattern(`/${relativeRoot}`))}`);
580
649
  const jsPluginFiles = new Set(configInputs.flatMap((config) => localJsPluginFiles(config, jsPluginSpecifiersByConfig.get(config) ?? [])));
581
650
  const lintableFiles = `{projectRoot}/${LINTABLE_FILES_GLOB}`;
582
651
  const usesBoundariesBridge = configInputs.some((config) => (jsPluginSpecifiersByConfig.get(config) ?? []).includes(BOUNDARIES_PLUGIN_SPECIFIER));
583
652
  const targetConfig = {
584
- command: `oxlint ${lintPath}`,
653
+ command: `oxlint ${[lintPath, ...nestedIgnoreArgs].join(' ')}`,
585
654
  options: { cwd: projectRoot },
586
655
  cache: true,
587
656
  inputs: [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nx/oxlint",
3
3
  "description": "Experimental. The Oxlint plugin for Nx contains generators and utilities used for linting JavaScript/TypeScript projects within an Nx workspace.",
4
- "version": "23.2.0-rc.1",
4
+ "version": "23.2.0-rc.2",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -69,9 +69,9 @@
69
69
  "generators": "./generators.json",
70
70
  "dependencies": {
71
71
  "tslib": "^2.3.0",
72
- "@nx/devkit": "23.2.0-rc.1",
73
- "@nx/eslint-plugin": "23.2.0-rc.1",
74
- "@nx/js": "23.2.0-rc.1"
72
+ "@nx/devkit": "23.2.0-rc.2",
73
+ "@nx/eslint-plugin": "23.2.0-rc.2",
74
+ "@nx/js": "23.2.0-rc.2"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "oxlint": "^1.43.0"
@@ -85,6 +85,6 @@
85
85
  "node": "^20.19.0 || >=22.12.0"
86
86
  },
87
87
  "devDependencies": {
88
- "nx": "23.2.0-rc.1"
88
+ "nx": "23.2.0-rc.2"
89
89
  }
90
90
  }