@nx/oxlint 23.2.0-rc.1 → 23.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/src/plugins/plugin.js +76 -7
- 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';
|
|
@@ -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
|
-
|
|
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
|
|
4
|
+
"version": "23.2.0",
|
|
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
|
|
73
|
-
"@nx/eslint-plugin": "23.2.0
|
|
74
|
-
"@nx/js": "23.2.0
|
|
72
|
+
"@nx/devkit": "23.2.0",
|
|
73
|
+
"@nx/eslint-plugin": "23.2.0",
|
|
74
|
+
"@nx/js": "23.2.0"
|
|
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
|
|
88
|
+
"nx": "23.2.0"
|
|
89
89
|
}
|
|
90
90
|
}
|