@nx/oxlint 23.2.0-beta.10 → 23.2.0-beta.12

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/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  <div style="text-align: center;">
9
9
 
10
- [![License](https://img.shields.io/npm/l/@nx/workspace.svg?style=flat-square)]()
10
+ [![License](https://img.shields.io/npm/l/@nx/workspace.svg?style=flat-square)](<>)
11
11
  [![NPM Version](https://badge.fury.io/js/nx.svg)](https://www.npmjs.com/package/nx)
12
- [![Semantic Release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)]()
12
+ [![Semantic Release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](<>)
13
13
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
14
14
  [![Join us on the Official Nx Discord Server](https://img.shields.io/discord/1143497901675401286?label=discord)](https://go.nx.dev/community)
15
15
  [![Nx Sandboxing](https://staging.nx.app/workspaces/62d013ea0852fe0a2df74438/sandbox-badge.svg)](https://nx.dev/docs/features/ci-features/sandboxing)
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { createNodes, createNodesV2, type OxlintPluginOptions, } from './src/plugins/plugin.js';
1
+ export { createDependencies, createNodes, createNodesV2, type OxlintPluginOptions, } from './src/plugins/plugin.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // The graph plugin only — this is what `nx.json` names as `@nx/oxlint`.
2
2
  // Generators live in `./generators`.
3
- export { createNodes, createNodesV2, } from './src/plugins/plugin.js';
3
+ export { createDependencies, createNodes, createNodesV2, } from './src/plugins/plugin.js';
@@ -1,6 +1,14 @@
1
- import { CreateNodes } from '@nx/devkit';
1
+ import { CreateDependencies, CreateNodes } from '@nx/devkit';
2
2
  export interface OxlintPluginOptions {
3
3
  targetName?: string;
4
4
  }
5
5
  export declare const createNodes: CreateNodes<OxlintPluginOptions>;
6
6
  export declare const createNodesV2: CreateNodes<OxlintPluginOptions>;
7
+ /**
8
+ * A config's `jsPlugins` produce the lint results, so every project linted
9
+ * under that config depends on them. Resolving the specifiers here, against the
10
+ * finished node set, is what makes a workspace plugin a project edge and an npm
11
+ * plugin an external edge; `^default` then hashes both. Implicit, because the
12
+ * config may not be a file of the depending project.
13
+ */
14
+ export declare const createDependencies: CreateDependencies<OxlintPluginOptions>;
@@ -1,5 +1,5 @@
1
- import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, calculateHashesForCreateNodes, combineGlobPatterns, globWithWorkspaceContext, hashObject, PluginCache, workspaceDataDirectory, } from '@nx/devkit/internal';
2
- import { createNodesFromFiles, detectPackageManager, getPackageManagerCommand, readJsonFile, } from '@nx/devkit';
1
+ import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, calculateHashesForCreateNodes, combineGlobPatterns, globWithWorkspaceContext, hashObject, PluginCache, TargetProjectLocator, workspaceDataDirectory, } from '@nx/devkit/internal';
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';
5
5
  import { execFileSync } from 'node:child_process';
@@ -32,7 +32,7 @@ const OXLINT_CONFIG_GLOB = combineGlobPatterns([
32
32
  ...OXLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
33
33
  ...PROJECT_CONFIG_FILENAMES.map((f) => `**/${f}`),
34
34
  ]);
35
- const internalCreateNodes = async (configFilePath, options, context, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, tsconfigChainsByProjectRoot, projectsCache, hashByRoot, pmc, rootConfig) => {
35
+ const internalCreateNodes = async (configFilePath, options, context, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot, projectsCache, hashByRoot, pmc, rootConfig) => {
36
36
  const configDir = dirname(configFilePath);
37
37
  // Collect each project root's contribution in parallel, but write them into
38
38
  // `projects` afterwards in input order so insertion order (and therefore
@@ -53,7 +53,7 @@ const internalCreateNodes = async (configFilePath, options, context, projectRoot
53
53
  projectsCache.set(hash, {});
54
54
  return null;
55
55
  }
56
- const project = getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, tsconfigChainsByProjectRoot.get(projectRoot) ?? [], rootConfig);
56
+ const project = getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot.get(projectRoot) ?? [], rootConfig);
57
57
  if (project) {
58
58
  const entry = { [projectRoot]: project };
59
59
  projectsCache.set(hash, entry);
@@ -90,7 +90,7 @@ export const createNodes = [
90
90
  // Workspace-global and invariant for the whole run, so it is resolved once
91
91
  // here rather than re-stat'ed for every project.
92
92
  const rootConfig = OXLINT_CONFIG_FILENAMES.find((file) => existsSync(join(context.workspaceRoot, file)));
93
- const configChainsByConfig = collectConfigChains(oxlintConfigFiles, context.workspaceRoot);
93
+ const { chains: configChainsByConfig, jsPluginSpecifiersByConfig } = collectConfigChains(oxlintConfigFiles, context.workspaceRoot);
94
94
  const tsconfigChainsByProjectRoot = collectTsconfigChainsByProjectRoot(projectRoots, context.workspaceRoot);
95
95
  // Stats the workspace root, so detect once for both uses below — and after
96
96
  // the bail, so a workspace with no Oxlint config never pays for it.
@@ -106,10 +106,17 @@ export const createNodes = [
106
106
  });
107
107
  // Deduped: every chain ends at the same root config, and each duplicate
108
108
  // is another whole-workspace glob for the hasher.
109
+ const allConfigs = [
110
+ ...governingConfigs,
111
+ ...governingConfigs.flatMap((config) => configChainsByConfig.get(config) ?? []),
112
+ ];
109
113
  return [
110
114
  ...new Set([
111
- ...governingConfigs,
112
- ...governingConfigs.flatMap((config) => configChainsByConfig.get(config) ?? []),
115
+ ...allConfigs,
116
+ ...allConfigs.flatMap((config) => localJsPluginFiles(config, jsPluginSpecifiersByConfig.get(config) ?? [])),
117
+ // Change which files Oxlint considers lintable, and therefore
118
+ // whether a target is inferred at all.
119
+ ...ancestorEslintignorePaths(root),
113
120
  lockFilePattern,
114
121
  ...(tsconfigChainsByProjectRoot.get(root) ?? []),
115
122
  ]),
@@ -117,7 +124,7 @@ export const createNodes = [
117
124
  }));
118
125
  const hashByRoot = new Map(projectRoots.map((r, i) => [r, hashes[i]]));
119
126
  try {
120
- return await createNodesFromFiles((configFile, fileOptions, fileContext) => internalCreateNodes(configFile, fileOptions, fileContext, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, tsconfigChainsByProjectRoot, targetsCache, hashByRoot, pmc, rootConfig), oxlintConfigFiles, options, context);
127
+ return await createNodesFromFiles((configFile, fileOptions, fileContext) => internalCreateNodes(configFile, fileOptions, fileContext, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainsByProjectRoot, targetsCache, hashByRoot, pmc, rootConfig), oxlintConfigFiles, options, context);
121
128
  }
122
129
  finally {
123
130
  targetsCache.writeToDisk();
@@ -125,6 +132,98 @@ export const createNodes = [
125
132
  },
126
133
  ];
127
134
  export const createNodesV2 = createNodes;
135
+ /**
136
+ * A config's `jsPlugins` produce the lint results, so every project linted
137
+ * under that config depends on them. Resolving the specifiers here, against the
138
+ * finished node set, is what makes a workspace plugin a project edge and an npm
139
+ * plugin an external edge; `^default` then hashes both. Implicit, because the
140
+ * config may not be a file of the depending project.
141
+ */
142
+ export const createDependencies = (_options, context) => {
143
+ // Walked rather than flattened: this runs on every graph build, and a copy
144
+ // of the whole file map is the wrong price for finding a few config files.
145
+ const oxlintConfigFiles = [];
146
+ const collect = (files) => {
147
+ for (const { file } of files) {
148
+ if (OXLINT_CONFIG_FILENAMES.includes(basename(file))) {
149
+ oxlintConfigFiles.push(file);
150
+ }
151
+ }
152
+ };
153
+ for (const files of Object.values(context.fileMap.projectFileMap)) {
154
+ collect(files);
155
+ }
156
+ collect(context.fileMap.nonProjectFiles);
157
+ if (oxlintConfigFiles.length === 0) {
158
+ return [];
159
+ }
160
+ const { chains, jsPluginSpecifiersByConfig } = collectConfigChains(oxlintConfigFiles, context.workspaceRoot);
161
+ const configsWithPlugins = [...jsPluginSpecifiersByConfig].filter(([, specifiers]) => specifiers.length > 0);
162
+ if (configsWithPlugins.length === 0) {
163
+ return [];
164
+ }
165
+ const nodes = {};
166
+ for (const [name, data] of Object.entries(context.projects)) {
167
+ nodes[name] = { name, type: null, data };
168
+ }
169
+ // Own resolution cache: the locator's shared default one is never
170
+ // invalidated, so a plugin resolved before `install` would stay unresolved
171
+ // for the life of the plugin worker.
172
+ const locator = new TargetProjectLocator(nodes, context.externalNodes, new Map());
173
+ // Resolution is relative to the config, so it is the same for every project
174
+ // that config governs.
175
+ const targetsByConfig = new Map();
176
+ const resolveTargets = (config) => {
177
+ let targets = targetsByConfig.get(config);
178
+ if (!targets) {
179
+ targets = [];
180
+ for (const specifier of jsPluginSpecifiersByConfig.get(config) ?? []) {
181
+ const target = locator.findProjectFromImport(specifier, config);
182
+ if (target) {
183
+ targets.push(target);
184
+ }
185
+ }
186
+ targetsByConfig.set(config, targets);
187
+ }
188
+ return targets;
189
+ };
190
+ const dependencies = [];
191
+ for (const [name, project] of Object.entries(context.projects)) {
192
+ if (!Object.values(project.targets ?? {}).some((target) => target.metadata?.technologies?.includes('oxlint'))) {
193
+ continue;
194
+ }
195
+ // Own directory and every ancestor, plus their `extends` chains — the
196
+ // same set `createNodes` hashes. A plugin project that depends on a
197
+ // project it lints closes a cycle here; that is a real circularity, and
198
+ // the docs say so.
199
+ const governingConfigs = oxlintConfigFiles.filter((config) => {
200
+ const configDir = dirname(config);
201
+ return configDir === project.root || isSubDir(configDir, project.root);
202
+ });
203
+ const configs = new Set([
204
+ ...governingConfigs,
205
+ ...governingConfigs.flatMap((config) => chains.get(config) ?? []),
206
+ ]);
207
+ const targets = new Set();
208
+ for (const config of configs) {
209
+ for (const target of resolveTargets(config)) {
210
+ if (target !== name) {
211
+ targets.add(target);
212
+ }
213
+ }
214
+ }
215
+ for (const target of targets) {
216
+ const dependency = {
217
+ source: name,
218
+ target,
219
+ type: DependencyType.implicit,
220
+ };
221
+ validateDependency(dependency, context);
222
+ dependencies.push(dependency);
223
+ }
224
+ }
225
+ return dependencies;
226
+ };
128
227
  function splitConfigFiles(configFiles, workspaceRoot) {
129
228
  const oxlintConfigFiles = [];
130
229
  const packageJsonFiles = [];
@@ -186,9 +285,13 @@ function splitConfigFiles(configFiles, workspaceRoot) {
186
285
  * Oxlint resolves entries relative to the referencing config and only tracks the
187
286
  * chain for its LSP, so Nx walks it here or caching goes stale. TypeScript
188
287
  * configs are not statically readable, so only the file itself is tracked.
288
+ *
289
+ * Also collects each config's raw `jsPlugins` specifiers. `createDependencies`
290
+ * resolves them to graph nodes; `createNodes` only needs the local files.
189
291
  */
190
292
  function collectConfigChains(oxlintConfigFiles, workspaceRoot) {
191
293
  const result = new Map();
294
+ const jsPluginSpecifiersByConfig = new Map();
192
295
  // Shared across configs so the common root config is read once. `null` records
193
296
  // a failed read, so a bad file is not retried per referrer.
194
297
  const jsonCache = new Map();
@@ -201,6 +304,25 @@ function collectConfigChains(oxlintConfigFiles, workspaceRoot) {
201
304
  }
202
305
  return exists;
203
306
  };
307
+ const recordJsPluginSpecifiers = (relativeConfigPath, jsPlugins) => {
308
+ if (jsPluginSpecifiersByConfig.has(relativeConfigPath)) {
309
+ return;
310
+ }
311
+ const specifiers = [];
312
+ if (Array.isArray(jsPlugins)) {
313
+ for (const entry of jsPlugins) {
314
+ const specifier = typeof entry === 'string'
315
+ ? entry
316
+ : typeof entry?.specifier === 'string'
317
+ ? entry.specifier
318
+ : undefined;
319
+ if (specifier) {
320
+ specifiers.push(specifier);
321
+ }
322
+ }
323
+ }
324
+ jsPluginSpecifiersByConfig.set(relativeConfigPath, specifiers);
325
+ };
204
326
  const readConfig = (relativeConfigPath) => {
205
327
  if (jsonCache.has(relativeConfigPath)) {
206
328
  return jsonCache.get(relativeConfigPath);
@@ -216,6 +338,8 @@ function collectConfigChains(oxlintConfigFiles, workspaceRoot) {
216
338
  catch {
217
339
  // A malformed config drops its chain from the task inputs rather than
218
340
  // failing graph construction, matching `readCachedJson` in @nx/js.
341
+ // `oxlint.config.ts`/`.mts` land here too: their chains and jsPlugins
342
+ // are out of scope, so they get neither file inputs nor graph edges.
219
343
  json = null;
220
344
  }
221
345
  }
@@ -234,6 +358,7 @@ function collectConfigChains(oxlintConfigFiles, workspaceRoot) {
234
358
  return;
235
359
  }
236
360
  const json = readConfig(relativeConfigPath);
361
+ recordJsPluginSpecifiers(relativeConfigPath, json?.jsPlugins);
237
362
  if (!Array.isArray(json?.extends)) {
238
363
  return;
239
364
  }
@@ -256,7 +381,25 @@ function collectConfigChains(oxlintConfigFiles, workspaceRoot) {
256
381
  walk(configFile);
257
382
  result.set(configFile, extended);
258
383
  }
259
- return result;
384
+ return { chains: result, jsPluginSpecifiersByConfig };
385
+ }
386
+ /**
387
+ * Workspace-relative paths of a config's relative-path `jsPlugins`. Declared as
388
+ * file inputs because such a file may sit outside every project (`tools/x.js`
389
+ * at the root), where no graph edge can reach it.
390
+ */
391
+ function localJsPluginFiles(relativeConfigPath, specifiers) {
392
+ const files = [];
393
+ for (const specifier of specifiers) {
394
+ if (!specifier.startsWith('.')) {
395
+ continue;
396
+ }
397
+ const resolved = normalize(join(dirname(relativeConfigPath), specifier));
398
+ if (resolved !== '..' && !resolved.startsWith('../')) {
399
+ files.push(resolved);
400
+ }
401
+ }
402
+ return files;
260
403
  }
261
404
  /**
262
405
  * Per project root, the `extends`-reachable tsconfig files that live OUTSIDE the
@@ -372,6 +515,24 @@ async function collectLintableFilesByProjectRoot(projectRoots, context) {
372
515
  }
373
516
  return lintableFilesPerProjectRoot;
374
517
  }
518
+ /**
519
+ * `.eslintignore` candidates in every ancestor directory of the project root,
520
+ * workspace root included. The project's own directory is excluded — files
521
+ * there are covered by the `default` input — except at the workspace root,
522
+ * which is its own ancestor and so keeps its `.eslintignore`.
523
+ */
524
+ function ancestorEslintignorePaths(projectRoot) {
525
+ const result = [];
526
+ let dir = projectRoot === '.' ? '.' : dirname(projectRoot);
527
+ while (true) {
528
+ result.push(dir === '.' ? '.eslintignore' : `${dir}/.eslintignore`);
529
+ if (dir === '.') {
530
+ break;
531
+ }
532
+ dir = dirname(dir);
533
+ }
534
+ return result;
535
+ }
375
536
  // Only the keys are read, so the value type is left open for both callers.
376
537
  function getRootForDirectory(directory, roots) {
377
538
  let currentPath = normalize(directory);
@@ -383,7 +544,7 @@ function getRootForDirectory(directory, roots) {
383
544
  }
384
545
  return roots.has(currentPath) ? currentPath : null;
385
546
  }
386
- function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
547
+ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, jsPluginSpecifiersByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
387
548
  let standaloneSrcPath;
388
549
  if (projectRoot === '.' &&
389
550
  existsSync(join(context.workspaceRoot, projectRoot, 'package.json'))) {
@@ -410,6 +571,7 @@ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, conte
410
571
  ];
411
572
  const isRootProject = projectRoot === '.';
412
573
  const lintPath = isRootProject && standaloneSrcPath ? `./${standaloneSrcPath}` : '.';
574
+ const jsPluginFiles = new Set(configInputs.flatMap((config) => localJsPluginFiles(config, jsPluginSpecifiersByConfig.get(config) ?? [])));
413
575
  const targetConfig = {
414
576
  command: `oxlint ${lintPath}`,
415
577
  options: { cwd: projectRoot },
@@ -418,6 +580,11 @@ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, conte
418
580
  'default',
419
581
  '^default',
420
582
  ...configInputs.map((config) => `{workspaceRoot}/${config}`),
583
+ ...[...jsPluginFiles].map((file) => `{workspaceRoot}/${file}`),
584
+ // Oxlint layers .eslintignore files from every ancestor of a linted
585
+ // file; the project's own is covered by `default`. Declared even when
586
+ // absent, like the extends chain.
587
+ ...ancestorEslintignorePaths(projectRoot).map((file) => `{workspaceRoot}/${file}`),
421
588
  ...tsconfigChainOutsideProjectRoot.map((file) => `{workspaceRoot}/${file}`),
422
589
  { externalDependencies: ['oxlint'] },
423
590
  ],
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-beta.10",
4
+ "version": "23.2.0-beta.12",
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-beta.10",
73
- "@nx/eslint-plugin": "23.2.0-beta.10",
74
- "@nx/js": "23.2.0-beta.10"
72
+ "@nx/devkit": "23.2.0-beta.12",
73
+ "@nx/eslint-plugin": "23.2.0-beta.12",
74
+ "@nx/js": "23.2.0-beta.12"
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-beta.10"
88
+ "nx": "23.2.0-beta.12"
89
89
  }
90
90
  }