@nx/oxlint 23.2.0-rc.0 → 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.
- package/dist/src/plugins/plugin.js +90 -17
- package/package.json +5 -5
|
@@ -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';
|
|
@@ -27,6 +27,8 @@ const LINTABLE_EXTENSIONS = [
|
|
|
27
27
|
'astro',
|
|
28
28
|
];
|
|
29
29
|
const LINTABLE_FILES_GLOB = `**/*.{${LINTABLE_EXTENSIONS.join(',')}}`;
|
|
30
|
+
// Oxlint honours both, so both decide which files it lints.
|
|
31
|
+
const IGNORE_FILENAMES = ['.eslintignore', '.gitignore'];
|
|
30
32
|
// The one rule that looks across projects; every other rule sees one file.
|
|
31
33
|
const BOUNDARIES_PLUGIN_SPECIFIER = '@nx/oxlint/boundaries-plugin';
|
|
32
34
|
const PROJECT_CONFIG_FILENAMES = ['project.json', 'package.json'];
|
|
@@ -34,7 +36,7 @@ const OXLINT_CONFIG_GLOB = combineGlobPatterns([
|
|
|
34
36
|
...OXLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
|
|
35
37
|
...PROJECT_CONFIG_FILENAMES.map((f) => `**/${f}`),
|
|
36
38
|
]);
|
|
37
|
-
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) => {
|
|
38
40
|
const configDir = dirname(configFilePath);
|
|
39
41
|
// Collect each project root's contribution in parallel, but write them into
|
|
40
42
|
// `projects` afterwards in input order so insertion order (and therefore
|
|
@@ -55,7 +57,7 @@ const internalCreateNodes = async (configFilePath, options, context, projectRoot
|
|
|
55
57
|
projectsCache.set(hash, {});
|
|
56
58
|
return null;
|
|
57
59
|
}
|
|
58
|
-
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);
|
|
59
61
|
if (project) {
|
|
60
62
|
const entry = { [projectRoot]: project };
|
|
61
63
|
projectsCache.set(hash, entry);
|
|
@@ -80,6 +82,7 @@ export const createNodes = [
|
|
|
80
82
|
const cachePath = join(workspaceDataDirectory, `oxlint-${optionsHash}.hash`);
|
|
81
83
|
const targetsCache = new PluginCache(cachePath);
|
|
82
84
|
const { oxlintConfigFiles, projectRoots, projectRootsByOxlintRoots } = splitConfigFiles(configFiles, context.workspaceRoot);
|
|
85
|
+
const nestedRootsByParent = nestedRootsByParentRoot(projectRoots);
|
|
83
86
|
// The glob also matches `**/package.json`, so this callback runs in every
|
|
84
87
|
// workspace, Oxlint or not. Bail before the chain walks and the hashing.
|
|
85
88
|
if (oxlintConfigFiles.length === 0) {
|
|
@@ -118,7 +121,7 @@ export const createNodes = [
|
|
|
118
121
|
...allConfigs.flatMap((config) => localJsPluginFiles(config, jsPluginSpecifiersByConfig.get(config) ?? [])),
|
|
119
122
|
// Change which files Oxlint considers lintable, and therefore
|
|
120
123
|
// whether a target is inferred at all.
|
|
121
|
-
...
|
|
124
|
+
...ancestorIgnorePaths(root),
|
|
122
125
|
lockFilePattern,
|
|
123
126
|
...(tsconfigChainsByProjectRoot.get(root) ?? []),
|
|
124
127
|
]),
|
|
@@ -126,7 +129,7 @@ export const createNodes = [
|
|
|
126
129
|
}));
|
|
127
130
|
const hashByRoot = new Map(projectRoots.map((r, i) => [r, hashes[i]]));
|
|
128
131
|
try {
|
|
129
|
-
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);
|
|
130
133
|
}
|
|
131
134
|
finally {
|
|
132
135
|
targetsCache.writeToDisk();
|
|
@@ -518,16 +521,18 @@ async function collectLintableFilesByProjectRoot(projectRoots, context) {
|
|
|
518
521
|
return lintableFilesPerProjectRoot;
|
|
519
522
|
}
|
|
520
523
|
/**
|
|
521
|
-
*
|
|
524
|
+
* Ignore-file candidates in every ancestor directory of the project root,
|
|
522
525
|
* workspace root included. The project's own directory is excluded — files
|
|
523
|
-
* there are covered by the
|
|
524
|
-
* which is its own ancestor and so keeps its
|
|
526
|
+
* there are covered by the in-project ignore-file input — except at the
|
|
527
|
+
* workspace root, which is its own ancestor and so keeps its ignore files.
|
|
525
528
|
*/
|
|
526
|
-
function
|
|
529
|
+
function ancestorIgnorePaths(projectRoot) {
|
|
527
530
|
const result = [];
|
|
528
531
|
let dir = projectRoot === '.' ? '.' : dirname(projectRoot);
|
|
529
532
|
while (true) {
|
|
530
|
-
|
|
533
|
+
for (const filename of IGNORE_FILENAMES) {
|
|
534
|
+
result.push(dir === '.' ? filename : `${dir}/${filename}`);
|
|
535
|
+
}
|
|
531
536
|
if (dir === '.') {
|
|
532
537
|
break;
|
|
533
538
|
}
|
|
@@ -535,7 +540,69 @@ function ancestorEslintignorePaths(projectRoot) {
|
|
|
535
540
|
}
|
|
536
541
|
return result;
|
|
537
542
|
}
|
|
538
|
-
|
|
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.
|
|
539
606
|
function getRootForDirectory(directory, roots) {
|
|
540
607
|
let currentPath = normalize(directory);
|
|
541
608
|
while (currentPath !== dirname(currentPath)) {
|
|
@@ -546,7 +613,7 @@ function getRootForDirectory(directory, roots) {
|
|
|
546
613
|
}
|
|
547
614
|
return roots.has(currentPath) ? currentPath : null;
|
|
548
615
|
}
|
|
549
|
-
function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
|
|
616
|
+
function getProjectUsingOxlintConfig(configFilePath, projectRoot, nestedRootsByParent, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
|
|
550
617
|
let standaloneSrcPath;
|
|
551
618
|
if (projectRoot === '.' &&
|
|
552
619
|
existsSync(join(context.workspaceRoot, projectRoot, 'package.json'))) {
|
|
@@ -573,11 +640,17 @@ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, conte
|
|
|
573
640
|
];
|
|
574
641
|
const isRootProject = projectRoot === '.';
|
|
575
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}`))}`);
|
|
576
649
|
const jsPluginFiles = new Set(configInputs.flatMap((config) => localJsPluginFiles(config, jsPluginSpecifiersByConfig.get(config) ?? [])));
|
|
577
650
|
const lintableFiles = `{projectRoot}/${LINTABLE_FILES_GLOB}`;
|
|
578
651
|
const usesBoundariesBridge = configInputs.some((config) => (jsPluginSpecifiersByConfig.get(config) ?? []).includes(BOUNDARIES_PLUGIN_SPECIFIER));
|
|
579
652
|
const targetConfig = {
|
|
580
|
-
command: `oxlint ${lintPath}`,
|
|
653
|
+
command: `oxlint ${[lintPath, ...nestedIgnoreArgs].join(' ')}`,
|
|
581
654
|
options: { cwd: projectRoot },
|
|
582
655
|
cache: true,
|
|
583
656
|
inputs: [
|
|
@@ -596,13 +669,13 @@ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, conte
|
|
|
596
669
|
: []),
|
|
597
670
|
// Configs, ignore files and tsconfigs inside the project.
|
|
598
671
|
{
|
|
599
|
-
fileset: `{projectRoot}/**/{${[...OXLINT_CONFIG_FILENAMES,
|
|
672
|
+
fileset: `{projectRoot}/**/{${[...OXLINT_CONFIG_FILENAMES, ...IGNORE_FILENAMES, 'tsconfig*.json'].join(',')}}`,
|
|
600
673
|
},
|
|
601
674
|
...configInputs.map((config) => `{workspaceRoot}/${config}`),
|
|
602
675
|
...[...jsPluginFiles].map((file) => `{workspaceRoot}/${file}`),
|
|
603
|
-
// Oxlint layers
|
|
604
|
-
//
|
|
605
|
-
...
|
|
676
|
+
// Oxlint layers ignore files from every ancestor of a linted file.
|
|
677
|
+
// Declared even when absent, like the extends chain.
|
|
678
|
+
...ancestorIgnorePaths(projectRoot).map((file) => `{workspaceRoot}/${file}`),
|
|
606
679
|
...tsconfigChainOutsideProjectRoot.map((file) => `{workspaceRoot}/${file}`),
|
|
607
680
|
{ externalDependencies: ['oxlint'] },
|
|
608
681
|
],
|
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.
|
|
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.
|
|
73
|
-
"@nx/eslint-plugin": "23.2.0-rc.
|
|
74
|
-
"@nx/js": "23.2.0-rc.
|
|
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.
|
|
88
|
+
"nx": "23.2.0-rc.2"
|
|
89
89
|
}
|
|
90
90
|
}
|