@nx/oxlint 0.0.1

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 ADDED
@@ -0,0 +1,67 @@
1
+ <p style="text-align: center;">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-dark.svg">
4
+ <img alt="Nx - Smart Monorepos · Fast Builds" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-light.svg" width="100%">
5
+ </picture>
6
+ </p>
7
+
8
+ <div style="text-align: center;">
9
+
10
+ [![License](https://img.shields.io/npm/l/@nx/workspace.svg?style=flat-square)]()
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)]()
13
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
14
+ [![Join us on the Official Nx Discord Server](https://img.shields.io/discord/1143497901675401286?label=discord)](https://go.nx.dev/community)
15
+ [![Nx Sandboxing](https://staging.nx.app/workspaces/62d013ea0852fe0a2df74438/sandbox-badge.svg)](https://nx.dev/docs/features/ci-features/sandboxing)
16
+
17
+ </div>
18
+
19
+
20
+ <hr>
21
+
22
+ # Nx: Smart Monorepos · Fast Builds
23
+
24
+ Get to green PRs in half the time. Nx optimizes your builds, scales your CI, and fixes failed PRs. Built for developers and AI agents.
25
+
26
+ This package is an [Oxlint plugin for Nx](https://nx.dev/nx-api/oxlint).
27
+
28
+ ## Getting Started
29
+
30
+ ### Creating an Nx Workspace
31
+
32
+ **Using `npx`**
33
+
34
+ ```bash
35
+ npx create-nx-workspace
36
+ ```
37
+
38
+ **Using `npm init`**
39
+
40
+ ```bash
41
+ npm init nx-workspace
42
+ ```
43
+
44
+ **Using `yarn create`**
45
+
46
+ ```bash
47
+ yarn create nx-workspace
48
+ ```
49
+
50
+ ### Adding Nx to an Existing Repository
51
+
52
+ Run:
53
+
54
+ ```bash
55
+ npx nx@latest init
56
+ ```
57
+
58
+ ## Documentation & Resources
59
+
60
+ - [Nx.Dev: Documentation, Guides, Tutorials](https://nx.dev)
61
+ - [Intro to Nx](https://nx.dev/getting-started/intro)
62
+ - [Official Nx YouTube Channel](https://www.youtube.com/@NxDevtools)
63
+ - [Blog Posts About Nx](https://nx.dev/blog)
64
+
65
+ <p style="text-align: center;"><a href="https://nx.dev/#learning-materials" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-courses-and-videos.svg"
66
+ width="100%" alt="Nx - Smart Monorepos · Fast Builds"></a></p>
67
+
package/dist/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2017-2026 Narwhal Technologies Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ export { initGenerator } from './src/generators/init/init.js';
2
+ export { configurationGenerator } from './src/generators/configuration/configuration.js';
@@ -0,0 +1,2 @@
1
+ export { initGenerator } from './src/generators/init/init.js';
2
+ export { configurationGenerator } from './src/generators/configuration/configuration.js';
@@ -0,0 +1 @@
1
+ export { createNodes, createNodesV2, type OxlintPluginOptions, } from './src/plugins/plugin.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // The graph plugin only — this is what `nx.json` names as `@nx/oxlint`.
2
+ // Generators live in `./generators`.
3
+ export { createNodes, createNodesV2, } from './src/plugins/plugin.js';
@@ -0,0 +1,7 @@
1
+ declare const nxOxlintBoundariesPlugin: {
2
+ meta: {
3
+ name: string;
4
+ };
5
+ rules: Record<string, unknown>;
6
+ };
7
+ export default nxOxlintBoundariesPlugin;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Experimental bridge exposing Nx's `enforce-module-boundaries` rule to Oxlint
3
+ * as a JS plugin.
4
+ *
5
+ * Oxlint's JS-plugin rule context deliberately mirrors ESLint's `RuleContext`
6
+ * (`options`, `report`, `filename`, `sourceCode`, …), so the ESLint rule runs
7
+ * unmodified — `ESLintUtils.RuleCreator` applies `defaultOptions` from
8
+ * `context.options` inside its own `create`.
9
+ *
10
+ * Oxlint's JS-plugin API is excluded from its semver policy and may change in
11
+ * any release.
12
+ */
13
+ // Imported from `/internal` rather than the `/nx` plugin entry: that entry
14
+ // spreads `workspaceRules`, whose module-level initializer compiles the
15
+ // consuming workspace's `tools/eslint-rules` directory just to hand back one
16
+ // rule.
17
+ import { enforceModuleBoundaries } from '@nx/eslint-plugin/internal';
18
+ const RULE_NAME = 'enforce-module-boundaries';
19
+ // Annotated rather than inferred: the inferred type names `Options` /
20
+ // `MessageIds` from a non-portable deep path in `@nx/eslint-plugin` (TS2883).
21
+ const nxOxlintBoundariesPlugin = {
22
+ meta: { name: '@nx' },
23
+ rules: {
24
+ [RULE_NAME]: {
25
+ ...enforceModuleBoundaries,
26
+ // Oxlint takes the rule's name from this object's key, not from `meta`.
27
+ // `meta.name` on the *plugin* above is not required either — without it
28
+ // Oxlint falls back to the package name and the rule registers as
29
+ // `@nx/oxlint/enforce-module-boundaries` instead of the id the docs tell
30
+ // users to configure. Carrying the name here anyway keeps the rule
31
+ // self-describing, since `ESLintUtils.RuleCreator` keeps `name` as a
32
+ // sibling of `meta` rather than inside it.
33
+ meta: { ...enforceModuleBoundaries.meta, name: RULE_NAME },
34
+ },
35
+ },
36
+ };
37
+ export default nxOxlintBoundariesPlugin;
@@ -0,0 +1,17 @@
1
+ import { GeneratorCallback, Tree } from '@nx/devkit';
2
+ export interface ConfigurationGeneratorSchema {
3
+ project: string;
4
+ skipFormat?: boolean;
5
+ skipPackageJson?: boolean;
6
+ keepExistingVersions?: boolean;
7
+ /** Oxlint plugins to enable for this project, e.g. `['react', 'jsx-a11y']`. */
8
+ plugins?: string[];
9
+ }
10
+ /**
11
+ * Registers `@nx/oxlint` and enables this project's Oxlint plugins.
12
+ *
13
+ * It writes no target: `@nx/oxlint` is inference-only, so the plugin registered
14
+ * by `init` is what gives the project its Oxlint task.
15
+ */
16
+ export declare function configurationGenerator(tree: Tree, options: ConfigurationGeneratorSchema): Promise<GeneratorCallback>;
17
+ export default configurationGenerator;
@@ -0,0 +1,28 @@
1
+ import { formatFiles, readProjectConfiguration, runTasksInSerial, } from '@nx/devkit';
2
+ import { initGenerator } from '../init/init.js';
3
+ import { addPluginsToOxlintConfig } from '../../utils/oxlint-config.js';
4
+ /**
5
+ * Registers `@nx/oxlint` and enables this project's Oxlint plugins.
6
+ *
7
+ * It writes no target: `@nx/oxlint` is inference-only, so the plugin registered
8
+ * by `init` is what gives the project its Oxlint task.
9
+ */
10
+ export async function configurationGenerator(tree, options) {
11
+ const tasks = [];
12
+ tasks.push(await initGenerator(tree, {
13
+ skipFormat: true,
14
+ skipPackageJson: options.skipPackageJson,
15
+ keepExistingVersions: options.keepExistingVersions,
16
+ }));
17
+ // Read before the `plugins` check: `plugins` defaults to `[]`, so gating this
18
+ // on it means a plain `--project=<typo>` run validates nothing and exits 0.
19
+ const projectConfig = readProjectConfiguration(tree, options.project);
20
+ if (options.plugins?.length) {
21
+ addPluginsToOxlintConfig(tree, projectConfig.root, options.plugins);
22
+ }
23
+ if (!options.skipFormat) {
24
+ await formatFiles(tree);
25
+ }
26
+ return runTasksInSerial(...tasks);
27
+ }
28
+ export default configurationGenerator;
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "NxOxlintConfiguration",
4
+ "title": "Configure Oxlint for a project",
5
+ "description": "Register `@nx/oxlint` and enable this project\u2019s Oxlint plugins.",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "The name of the project.",
11
+ "$default": {
12
+ "$source": "projectName"
13
+ },
14
+ "x-prompt": "Which project should use Oxlint?"
15
+ },
16
+ "skipFormat": {
17
+ "type": "boolean",
18
+ "description": "Skip formatting files.",
19
+ "default": false
20
+ },
21
+ "skipPackageJson": {
22
+ "type": "boolean",
23
+ "description": "Do not add dependencies to `package.json`.",
24
+ "default": false
25
+ },
26
+ "keepExistingVersions": {
27
+ "type": "boolean",
28
+ "description": "Keep existing dependency versions when possible.",
29
+ "default": false
30
+ },
31
+ "plugins": {
32
+ "type": "array",
33
+ "items": {
34
+ "type": "string"
35
+ },
36
+ "description": "Oxlint plugins to enable for this project, such as `react` or `jsx-a11y`.",
37
+ "default": []
38
+ }
39
+ },
40
+ "required": ["project"],
41
+ "additionalProperties": false
42
+ }
@@ -0,0 +1,14 @@
1
+ import { GeneratorCallback, Tree } from '@nx/devkit';
2
+ export interface InitGeneratorSchema {
3
+ skipPackageJson?: boolean;
4
+ keepExistingVersions?: boolean;
5
+ updatePackageScripts?: boolean;
6
+ skipFormat?: boolean;
7
+ }
8
+ /**
9
+ * `lint` first so greenfield workspaces get the natural name, falling through
10
+ * the rest when something else already owns it.
11
+ */
12
+ export declare const OXLINT_TARGET_NAMES: string[];
13
+ export declare function initGenerator(tree: Tree, options: InitGeneratorSchema): Promise<GeneratorCallback>;
14
+ export default initGenerator;
@@ -0,0 +1,104 @@
1
+ import { addDependenciesToPackageJson, createProjectGraphAsync, formatFiles, getDependencyVersionFromPackageJson, runTasksInSerial, updateJson, writeJson, } from '@nx/devkit';
2
+ import { addPlugin, assertSupportedPackageVersion } from '@nx/devkit/internal';
3
+ import { createNodes } from '../../plugins/plugin.js';
4
+ import { OXLINT_CONFIG_FILENAMES } from '../../utils/config-file.js';
5
+ import { minSupportedOxlintVersion, nxVersion, oxlintVersion, } from '../../utils/versions.js';
6
+ /**
7
+ * `lint` first so greenfield workspaces get the natural name, falling through
8
+ * the rest when something else already owns it.
9
+ */
10
+ export const OXLINT_TARGET_NAMES = [
11
+ 'lint',
12
+ 'oxlint',
13
+ 'oxlint:lint',
14
+ 'oxlint-lint',
15
+ ];
16
+ /**
17
+ * The candidate target names nothing in the workspace already uses.
18
+ *
19
+ * `addPlugin` can resolve this itself, but it does so by running our own
20
+ * `createNodes` against the real filesystem — where the root config does not
21
+ * exist yet on a first install, because the `Tree` has not been flushed. Our
22
+ * plugin returns no projects without a config, so `addPlugin` would see zero
23
+ * conflicts and take `lint` even in a workspace where ESLint owns it. Reading
24
+ * the existing graph answers the same question without that dependency.
25
+ */
26
+ function resolveTargetNames(graph) {
27
+ const taken = new Set();
28
+ for (const node of Object.values(graph.nodes ?? {})) {
29
+ for (const target of Object.keys(node.data?.targets ?? {})) {
30
+ taken.add(target);
31
+ }
32
+ }
33
+ const available = OXLINT_TARGET_NAMES.filter((name) => !taken.has(name));
34
+ if (!available.length) {
35
+ throw new Error(`Could not add the @nx/oxlint plugin: every candidate target name is already in use (${OXLINT_TARGET_NAMES.join(', ')}). Rename one of them, or register the plugin yourself in nx.json with ` +
36
+ `{ "plugin": "@nx/oxlint", "options": { "targetName": "<name>" } }.`);
37
+ }
38
+ // Hand `addPlugin` the whole remaining list rather than one name, so it keeps
39
+ // its own fallback chain if its per-project probe disagrees with this one.
40
+ return available;
41
+ }
42
+ export async function initGenerator(tree, options) {
43
+ assertSupportedPackageVersion(tree, 'oxlint', minSupportedOxlintVersion);
44
+ const tasks = [];
45
+ // Registered unconditionally, and deliberately not gated on
46
+ // `useInferencePlugins` / `NX_ADD_PLUGINS`: `@nx/oxlint` produces tasks only
47
+ // through inference, so opting out would leave a workspace with no Oxlint
48
+ // tasks at all rather than a different way of running them. Matches
49
+ // `@nx/dotnet`, the other inference-only plugin.
50
+ const graph = await createProjectGraphAsync();
51
+ await addPlugin(tree, graph, '@nx/oxlint', createNodes, { targetName: resolveTargetNames(graph) }, options.updatePackageScripts);
52
+ ensureRootConfig(tree);
53
+ updateVsCodeRecommendedExtensions(tree);
54
+ if (!options.skipPackageJson) {
55
+ const devDependencies = { oxlint: oxlintVersion };
56
+ // Only declare ourselves when absent. `addDependenciesToPackageJson`
57
+ // compares versions and treats any non-semver range (`link:`, `file:`,
58
+ // `workspace:`) as lower than whatever it is given, so re-declaring would
59
+ // replace a deliberate local reference with a plain version.
60
+ if (!getDependencyVersionFromPackageJson(tree, '@nx/oxlint')) {
61
+ devDependencies['@nx/oxlint'] = nxVersion;
62
+ }
63
+ tasks.push(addDependenciesToPackageJson(tree, {}, devDependencies, undefined, options.keepExistingVersions));
64
+ }
65
+ if (!options.skipFormat) {
66
+ await formatFiles(tree);
67
+ }
68
+ return runTasksInSerial(...tasks);
69
+ }
70
+ /**
71
+ * Appends the official Oxc extension to existing VS Code recommendations.
72
+ * Only touches the file when it is already there — setting up a linter is not
73
+ * a reason to start dictating a workspace's editor config.
74
+ */
75
+ function updateVsCodeRecommendedExtensions(tree) {
76
+ if (!tree.exists('.vscode/extensions.json')) {
77
+ return;
78
+ }
79
+ updateJson(tree, '.vscode/extensions.json', (json) => {
80
+ json.recommendations ??= [];
81
+ const extension = 'oxc.oxc-vscode';
82
+ if (!json.recommendations.includes(extension)) {
83
+ json.recommendations.push(extension);
84
+ }
85
+ return json;
86
+ });
87
+ }
88
+ function ensureRootConfig(tree) {
89
+ if (OXLINT_CONFIG_FILENAMES.some((file) => tree.exists(file))) {
90
+ return;
91
+ }
92
+ // Mirrors what `oxlint --init` scaffolds. The schema is resolved from
93
+ // `node_modules` rather than a URL so it matches the installed Oxlint
94
+ // version and works offline; this is only ever written at the workspace
95
+ // root, where `oxlint` is a direct dependency.
96
+ writeJson(tree, '.oxlintrc.json', {
97
+ $schema: './node_modules/oxlint/configuration_schema.json',
98
+ plugins: ['typescript', 'unicorn', 'oxc'],
99
+ categories: { correctness: 'error' },
100
+ rules: {},
101
+ env: { builtin: true },
102
+ });
103
+ }
104
+ export default initGenerator;
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "NxOxlintInit",
4
+ "title": "Initialize Oxlint",
5
+ "description": "Set up `@nx/oxlint` in the workspace.",
6
+ "type": "object",
7
+ "properties": {
8
+ "skipPackageJson": {
9
+ "type": "boolean",
10
+ "description": "Do not add dependencies to `package.json`.",
11
+ "default": false
12
+ },
13
+ "keepExistingVersions": {
14
+ "type": "boolean",
15
+ "description": "Keep existing dependency versions when possible.",
16
+ "default": false
17
+ },
18
+ "updatePackageScripts": {
19
+ "type": "boolean",
20
+ "description": "Update `package.json` scripts to use inferred targets.",
21
+ "default": true
22
+ },
23
+ "skipFormat": {
24
+ "type": "boolean",
25
+ "description": "Skip formatting files.",
26
+ "default": false
27
+ }
28
+ },
29
+ "additionalProperties": false
30
+ }
@@ -0,0 +1,6 @@
1
+ import { CreateNodes } from '@nx/devkit';
2
+ export interface OxlintPluginOptions {
3
+ targetName?: string;
4
+ }
5
+ export declare const createNodes: CreateNodes<OxlintPluginOptions>;
6
+ export declare const createNodesV2: CreateNodes<OxlintPluginOptions>;
@@ -0,0 +1,458 @@
1
+ import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, calculateHashesForCreateNodes, combineGlobPatterns, globWithWorkspaceContext, hashObject, PluginCache, workspaceDataDirectory, } from '@nx/devkit/internal';
2
+ import { createNodesFromFiles, detectPackageManager, getPackageManagerCommand, readJsonFile, } from '@nx/devkit';
3
+ import { getLockFileName, getRootTsConfigFileName } from '@nx/js';
4
+ import { walkTsconfigExtendsChain, } from '@nx/js/internal';
5
+ import { execFileSync } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import { dirname as nativeDirname, join as nativeJoin, relative as nativeRelative, sep as nativeSep, } from 'node:path';
9
+ import { basename, dirname, join, normalize, sep } from 'node:path/posix';
10
+ import { OXLINT_CONFIG_FILENAMES } from '../utils/config-file.js';
11
+ /**
12
+ * Source types Oxlint can parse. It does not lint JSON. Not configurable:
13
+ * Oxlint picks the files it lints itself, so this only decides whether a
14
+ * project has anything worth inferring a target for.
15
+ */
16
+ const LINTABLE_EXTENSIONS = [
17
+ 'js',
18
+ 'mjs',
19
+ 'cjs',
20
+ 'jsx',
21
+ 'ts',
22
+ 'mts',
23
+ 'cts',
24
+ 'tsx',
25
+ 'vue',
26
+ 'svelte',
27
+ 'astro',
28
+ ];
29
+ const LINTABLE_FILES_GLOB = `**/*.{${LINTABLE_EXTENSIONS.join(',')}}`;
30
+ const PROJECT_CONFIG_FILENAMES = ['project.json', 'package.json'];
31
+ const OXLINT_CONFIG_GLOB = combineGlobPatterns([
32
+ ...OXLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
33
+ ...PROJECT_CONFIG_FILENAMES.map((f) => `**/${f}`),
34
+ ]);
35
+ const internalCreateNodes = async (configFilePath, options, context, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, tsconfigChainsByProjectRoot, projectsCache, hashByRoot, pmc, rootConfig) => {
36
+ const configDir = dirname(configFilePath);
37
+ // Collect each project root's contribution in parallel, but write them into
38
+ // `projects` afterwards in input order so insertion order (and therefore
39
+ // downstream merge order) stays deterministic.
40
+ const orderedProjectRoots = projectRootsByOxlintRoots.get(configDir) ?? [];
41
+ const contributions = await Promise.all(orderedProjectRoots.map(async (projectRoot) => {
42
+ const hash = hashByRoot.get(projectRoot);
43
+ const cached = projectsCache.get(hash);
44
+ if (cached) {
45
+ return cached;
46
+ }
47
+ // Require something lintable, so docs-only projects get no target even
48
+ // when they own a config — a target there fails with "No files found to
49
+ // lint". This spans the whole workspace, so it stays behind the cache
50
+ // check and is memoized across configs.
51
+ const shouldInferTarget = ((await getLintableFilesPerProjectRoot()).get(projectRoot) ?? 0) > 0;
52
+ if (!shouldInferTarget) {
53
+ projectsCache.set(hash, {});
54
+ return null;
55
+ }
56
+ const project = getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, tsconfigChainsByProjectRoot.get(projectRoot) ?? [], rootConfig);
57
+ if (project) {
58
+ const entry = { [projectRoot]: project };
59
+ projectsCache.set(hash, entry);
60
+ return entry;
61
+ }
62
+ projectsCache.set(hash, {});
63
+ return null;
64
+ }));
65
+ const projects = {};
66
+ for (const contribution of contributions) {
67
+ if (contribution) {
68
+ Object.assign(projects, contribution);
69
+ }
70
+ }
71
+ return { projects };
72
+ };
73
+ export const createNodes = [
74
+ OXLINT_CONFIG_GLOB,
75
+ async (configFiles, options, context) => {
76
+ options = normalizeOptions(options);
77
+ const optionsHash = hashObject(options);
78
+ const cachePath = join(workspaceDataDirectory, `oxlint-${optionsHash}.hash`);
79
+ const targetsCache = new PluginCache(cachePath);
80
+ const { oxlintConfigFiles, projectRoots, projectRootsByOxlintRoots } = splitConfigFiles(configFiles, context.workspaceRoot);
81
+ // The glob also matches `**/package.json`, so this callback runs in every
82
+ // workspace, Oxlint or not. Bail before the chain walks and the hashing.
83
+ if (oxlintConfigFiles.length === 0) {
84
+ return [];
85
+ }
86
+ // The most expensive thing here, and only needed on a cache miss. Lazy so a
87
+ // warm run skips it; memoized so concurrent per-config calls share one glob.
88
+ let lintableFilesPerProjectRoot;
89
+ const getLintableFilesPerProjectRoot = () => (lintableFilesPerProjectRoot ??= collectLintableFilesByProjectRoot(projectRoots, context));
90
+ // Workspace-global and invariant for the whole run, so it is resolved once
91
+ // here rather than re-stat'ed for every project.
92
+ const rootConfig = OXLINT_CONFIG_FILENAMES.find((file) => existsSync(join(context.workspaceRoot, file)));
93
+ const configChainsByConfig = collectConfigChains(oxlintConfigFiles, context.workspaceRoot);
94
+ const tsconfigChainsByProjectRoot = collectTsconfigChainsByProjectRoot(projectRoots, context.workspaceRoot);
95
+ // Stats the workspace root, so detect once for both uses below — and after
96
+ // the bail, so a workspace with no Oxlint config never pays for it.
97
+ const packageManager = detectPackageManager(context.workspaceRoot);
98
+ const pmc = getPackageManagerCommand(packageManager);
99
+ const lockFilePattern = getLockFileName(packageManager);
100
+ const hashes = await calculateHashesForCreateNodes(projectRoots, options, context, projectRoots.map((root) => {
101
+ // Own directory and every ancestor. Configs below the root are already
102
+ // covered by the hasher's `{projectRoot}/**/*` glob.
103
+ const governingConfigs = oxlintConfigFiles.filter((oxlintConfig) => {
104
+ const configDir = dirname(oxlintConfig);
105
+ return configDir === root || isSubDir(configDir, root);
106
+ });
107
+ // Deduped: every chain ends at the same root config, and each duplicate
108
+ // is another whole-workspace glob for the hasher.
109
+ return [
110
+ ...new Set([
111
+ ...governingConfigs,
112
+ ...governingConfigs.flatMap((config) => configChainsByConfig.get(config) ?? []),
113
+ lockFilePattern,
114
+ ...(tsconfigChainsByProjectRoot.get(root) ?? []),
115
+ ]),
116
+ ];
117
+ }));
118
+ const hashByRoot = new Map(projectRoots.map((r, i) => [r, hashes[i]]));
119
+ try {
120
+ return await createNodesFromFiles((configFile, fileOptions, fileContext) => internalCreateNodes(configFile, fileOptions, fileContext, projectRootsByOxlintRoots, getLintableFilesPerProjectRoot, configChainsByConfig, tsconfigChainsByProjectRoot, targetsCache, hashByRoot, pmc, rootConfig), oxlintConfigFiles, options, context);
121
+ }
122
+ finally {
123
+ targetsCache.writeToDisk();
124
+ }
125
+ },
126
+ ];
127
+ export const createNodesV2 = createNodes;
128
+ function splitConfigFiles(configFiles, workspaceRoot) {
129
+ const oxlintConfigFiles = [];
130
+ const packageJsonFiles = [];
131
+ const projectJsonRoots = new Set();
132
+ for (const configFile of configFiles) {
133
+ const fileName = basename(configFile);
134
+ if (fileName === 'package.json') {
135
+ packageJsonFiles.push(configFile);
136
+ }
137
+ else if (fileName === 'project.json') {
138
+ projectJsonRoots.add(dirname(configFile));
139
+ }
140
+ else {
141
+ oxlintConfigFiles.push(configFile);
142
+ }
143
+ }
144
+ // The caller bails on this too, so returning early keeps a workspace with no
145
+ // Oxlint config from reading the root package.json, pnpm-workspace.yaml and
146
+ // lerna.json and minimatching every package.json for a discarded result.
147
+ if (oxlintConfigFiles.length === 0) {
148
+ return {
149
+ oxlintConfigFiles,
150
+ projectRoots: [],
151
+ projectRootsByOxlintRoots: new Map(),
152
+ };
153
+ }
154
+ // A package.json outside the workspaces globs is not a project: nested marker
155
+ // files have no `name`, and promoting one fails the whole graph with
156
+ // ProjectsWithNoNameError. Unconditional on purpose — core admits a root only
157
+ // when it carries an `nx` key, so empty globs mean core creates no root either.
158
+ const patterns = buildPackageJsonPatterns(workspaceRoot, (f) => readJsonFile(join(workspaceRoot, f)));
159
+ const isInPackageManagerWorkspaces = buildPackageJsonWorkspacesMatcher(patterns);
160
+ const projectRoots = new Set(projectJsonRoots);
161
+ for (const packageJsonFile of packageJsonFiles) {
162
+ // Next to a project.json it is a project regardless of the workspaces globs.
163
+ if (isInPackageManagerWorkspaces(packageJsonFile) ||
164
+ projectJsonRoots.has(dirname(packageJsonFile))) {
165
+ projectRoots.add(dirname(packageJsonFile));
166
+ }
167
+ }
168
+ const projectRootsByOxlintRoots = new Map();
169
+ for (const configFile of oxlintConfigFiles) {
170
+ projectRootsByOxlintRoots.set(dirname(configFile), []);
171
+ }
172
+ for (const projectRoot of projectRoots) {
173
+ const oxlintRoot = getRootForDirectory(projectRoot, projectRootsByOxlintRoots);
174
+ if (oxlintRoot) {
175
+ projectRootsByOxlintRoots.get(oxlintRoot).push(projectRoot);
176
+ }
177
+ }
178
+ return {
179
+ oxlintConfigFiles,
180
+ projectRoots: Array.from(projectRoots),
181
+ projectRootsByOxlintRoots,
182
+ };
183
+ }
184
+ /**
185
+ * Resolves each config's `extends` chain to workspace-relative target inputs.
186
+ * Oxlint resolves entries relative to the referencing config and only tracks the
187
+ * chain for its LSP, so Nx walks it here or caching goes stale. TypeScript
188
+ * configs are not statically readable, so only the file itself is tracked.
189
+ */
190
+ function collectConfigChains(oxlintConfigFiles, workspaceRoot) {
191
+ const result = new Map();
192
+ // Shared across configs so the common root config is read once. `null` records
193
+ // a failed read, so a bad file is not retried per referrer.
194
+ const jsonCache = new Map();
195
+ const existsCache = new Map();
196
+ const configExists = (relativeConfigPath) => {
197
+ let exists = existsCache.get(relativeConfigPath);
198
+ if (exists === undefined) {
199
+ exists = existsSync(join(workspaceRoot, relativeConfigPath));
200
+ existsCache.set(relativeConfigPath, exists);
201
+ }
202
+ return exists;
203
+ };
204
+ const readConfig = (relativeConfigPath) => {
205
+ if (jsonCache.has(relativeConfigPath)) {
206
+ return jsonCache.get(relativeConfigPath);
207
+ }
208
+ let json = null;
209
+ if (configExists(relativeConfigPath)) {
210
+ try {
211
+ json = readJsonFile(join(workspaceRoot, relativeConfigPath), {
212
+ expectComments: true,
213
+ allowTrailingComma: true,
214
+ });
215
+ }
216
+ catch {
217
+ // A malformed config drops its chain from the task inputs rather than
218
+ // failing graph construction, matching `readCachedJson` in @nx/js.
219
+ json = null;
220
+ }
221
+ }
222
+ jsonCache.set(relativeConfigPath, json);
223
+ return json;
224
+ };
225
+ for (const configFile of oxlintConfigFiles) {
226
+ const extended = [];
227
+ const seen = new Set();
228
+ const walk = (relativeConfigPath) => {
229
+ if (seen.has(relativeConfigPath)) {
230
+ return;
231
+ }
232
+ seen.add(relativeConfigPath);
233
+ if (!/\.jsonc?$/.test(relativeConfigPath)) {
234
+ return;
235
+ }
236
+ const json = readConfig(relativeConfigPath);
237
+ if (!Array.isArray(json?.extends)) {
238
+ return;
239
+ }
240
+ for (const entry of json.extends) {
241
+ if (typeof entry !== 'string') {
242
+ continue;
243
+ }
244
+ const resolved = normalize(join(dirname(relativeConfigPath), entry));
245
+ if (resolved.startsWith('..')) {
246
+ continue; // escapes the workspace, cannot be a `{workspaceRoot}` input
247
+ }
248
+ // Declared even when absent: an input matching no file costs nothing and
249
+ // keeps `inputs` stable if the file later appears.
250
+ extended.push(resolved);
251
+ if (configExists(resolved)) {
252
+ walk(resolved);
253
+ }
254
+ }
255
+ };
256
+ walk(configFile);
257
+ result.set(configFile, extended);
258
+ }
259
+ return result;
260
+ }
261
+ /**
262
+ * Per project root, the `extends`-reachable tsconfig files that live OUTSIDE the
263
+ * project root. Type-aware Oxlint reads the tsconfig, so those have to invalidate
264
+ * the cache. Mirrors `@nx/eslint`'s equivalent, including its skip cases.
265
+ */
266
+ function collectTsconfigChainsByProjectRoot(projectRoots, workspaceRoot) {
267
+ const jsonCache = new Map();
268
+ const result = new Map();
269
+ // The root tsconfig is handled by the native selective hasher, which only
270
+ // hashes the path aliases relevant to each project. Declaring it explicitly
271
+ // would bypass that and make every project dirty on any change.
272
+ const rootTsConfigName = getRootTsConfigFileName();
273
+ for (const projectRoot of projectRoots) {
274
+ if (projectRoot === '.')
275
+ continue;
276
+ const tsconfigPath = join(projectRoot, 'tsconfig.json');
277
+ if (!existsSync(join(workspaceRoot, tsconfigPath)))
278
+ continue;
279
+ const outside = [];
280
+ const projectPrefix = `${projectRoot}/`;
281
+ walkTsconfigExtendsChain(join(workspaceRoot, tsconfigPath), (absolutePath) => {
282
+ const wsRelative = nativeRelative(workspaceRoot, absolutePath)
283
+ .split(nativeSep)
284
+ .join('/');
285
+ if (wsRelative.startsWith('../') || wsRelative === '..') {
286
+ return 'continue'; // escapes workspace
287
+ }
288
+ if (wsRelative.startsWith('node_modules/') ||
289
+ wsRelative.includes('/node_modules/')) {
290
+ return 'continue'; // external package, lockfile invalidates
291
+ }
292
+ if (wsRelative === projectRoot ||
293
+ wsRelative.startsWith(projectPrefix)) {
294
+ return 'continue'; // inside project root, covered by `default`
295
+ }
296
+ if (wsRelative === rootTsConfigName) {
297
+ return 'continue'; // handled by native selective hasher
298
+ }
299
+ outside.push(wsRelative);
300
+ return 'continue';
301
+ }, { jsonCache });
302
+ result.set(projectRoot, outside);
303
+ }
304
+ return result;
305
+ }
306
+ const localRequire = createRequire(import.meta.url);
307
+ /**
308
+ * Asks Oxlint itself which files it would lint. A glob cannot answer this:
309
+ * Oxlint honours `ignorePatterns`, `.eslintignore` and `.gitignore`, including
310
+ * from nested project configs, so a project whose files are all ignored is
311
+ * empty to Oxlint but not to a glob.
312
+ *
313
+ * Returns null when Oxlint cannot answer — it is not installed, or one
314
+ * malformed config anywhere aborts the whole run. Callers fall back to globbing
315
+ * rather than failing graph construction over another project's broken config.
316
+ */
317
+ function enumerateLintableFilesWithOxlint(workspaceRoot) {
318
+ let bin;
319
+ try {
320
+ bin = nativeJoin(nativeDirname(localRequire.resolve('oxlint/package.json', { paths: [workspaceRoot] })), 'bin', 'oxlint');
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ if (!existsSync(bin)) {
326
+ return null;
327
+ }
328
+ try {
329
+ // Spawned through `process.execPath` rather than the `.bin` shim so this
330
+ // does not depend on a shell or on PATH.
331
+ // `--ignore-pattern` rather than trusting the workspace's `.gitignore`:
332
+ // `globWithWorkspaceContext` always skips `node_modules`, and a workspace
333
+ // that does not gitignore it would otherwise enumerate every dependency.
334
+ const stdout = execFileSync(process.execPath, [bin, '--debug=files', '--ignore-pattern', '**/node_modules/**', '.'], {
335
+ cwd: workspaceRoot,
336
+ encoding: 'utf-8',
337
+ stdio: ['ignore', 'pipe', 'ignore'],
338
+ maxBuffer: 64 * 1024 * 1024,
339
+ windowsHide: true,
340
+ // Graph construction must not hang on a wedged subprocess; a timeout
341
+ // throws, which is the same fall-back-to-globbing path as any other
342
+ // failure here.
343
+ timeout: 30_000,
344
+ });
345
+ return stdout
346
+ .split('\n')
347
+ .filter(Boolean)
348
+ .map((file) => file.split(nativeSep).join(sep));
349
+ }
350
+ catch {
351
+ return null;
352
+ }
353
+ }
354
+ /**
355
+ * Counts rather than collects: the only caller asks whether a project has any
356
+ * lintable file, and the enumeration spans the whole workspace.
357
+ */
358
+ async function collectLintableFilesByProjectRoot(projectRoots, context) {
359
+ const lintableFilesPerProjectRoot = new Map();
360
+ const lintableFiles = enumerateLintableFilesWithOxlint(context.workspaceRoot) ??
361
+ (await globWithWorkspaceContext(context.workspaceRoot, [
362
+ LINTABLE_FILES_GLOB,
363
+ ]));
364
+ for (const projectRoot of projectRoots) {
365
+ lintableFilesPerProjectRoot.set(projectRoot, 0);
366
+ }
367
+ for (const file of lintableFiles) {
368
+ const projectRoot = getRootForDirectory(dirname(file), lintableFilesPerProjectRoot);
369
+ if (projectRoot) {
370
+ lintableFilesPerProjectRoot.set(projectRoot, lintableFilesPerProjectRoot.get(projectRoot) + 1);
371
+ }
372
+ }
373
+ return lintableFilesPerProjectRoot;
374
+ }
375
+ // Only the keys are read, so the value type is left open for both callers.
376
+ function getRootForDirectory(directory, roots) {
377
+ let currentPath = normalize(directory);
378
+ while (currentPath !== dirname(currentPath)) {
379
+ if (roots.has(currentPath)) {
380
+ return currentPath;
381
+ }
382
+ currentPath = dirname(currentPath);
383
+ }
384
+ return roots.has(currentPath) ? currentPath : null;
385
+ }
386
+ function getProjectUsingOxlintConfig(configFilePath, projectRoot, options, context, pmc, configChainsByConfig, tsconfigChainOutsideProjectRoot, rootConfig) {
387
+ let standaloneSrcPath;
388
+ if (projectRoot === '.' &&
389
+ existsSync(join(context.workspaceRoot, projectRoot, 'package.json'))) {
390
+ if (existsSync(join(context.workspaceRoot, projectRoot, 'src'))) {
391
+ standaloneSrcPath = 'src';
392
+ }
393
+ else if (existsSync(join(context.workspaceRoot, projectRoot, 'lib'))) {
394
+ standaloneSrcPath = 'lib';
395
+ }
396
+ }
397
+ if (projectRoot === '.' && !standaloneSrcPath) {
398
+ return null;
399
+ }
400
+ const oxlintConfigs = [configFilePath];
401
+ if (rootConfig && !oxlintConfigs.includes(rootConfig)) {
402
+ oxlintConfigs.unshift(rootConfig);
403
+ }
404
+ // Deduped: a project config that extends the root names it a second time.
405
+ const configInputs = [
406
+ ...new Set(oxlintConfigs.flatMap((config) => [
407
+ config,
408
+ ...(configChainsByConfig.get(config) ?? []),
409
+ ])),
410
+ ];
411
+ const isRootProject = projectRoot === '.';
412
+ const lintPath = isRootProject && standaloneSrcPath ? `./${standaloneSrcPath}` : '.';
413
+ const targetConfig = {
414
+ command: `oxlint ${lintPath}`,
415
+ options: { cwd: projectRoot },
416
+ cache: true,
417
+ inputs: [
418
+ 'default',
419
+ '^default',
420
+ ...configInputs.map((config) => `{workspaceRoot}/${config}`),
421
+ ...tsconfigChainOutsideProjectRoot.map((file) => `{workspaceRoot}/${file}`),
422
+ { externalDependencies: ['oxlint'] },
423
+ ],
424
+ metadata: {
425
+ technologies: ['oxlint'],
426
+ description: 'Runs Oxlint on project',
427
+ help: {
428
+ command: `${pmc.exec} oxlint --help`,
429
+ example: {
430
+ options: {
431
+ 'max-warnings': 0,
432
+ },
433
+ },
434
+ },
435
+ },
436
+ };
437
+ return {
438
+ targets: {
439
+ [options.targetName]: targetConfig,
440
+ },
441
+ };
442
+ }
443
+ function normalizeOptions(options) {
444
+ return {
445
+ targetName: options?.targetName ?? 'lint',
446
+ };
447
+ }
448
+ function isSubDir(parent, child) {
449
+ if (parent === '.') {
450
+ return true;
451
+ }
452
+ parent = normalize(parent);
453
+ child = normalize(child);
454
+ if (!parent.endsWith(sep)) {
455
+ parent += sep;
456
+ }
457
+ return child.startsWith(parent);
458
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Config filenames Oxlint discovers on its own, in the order it declares them.
3
+ * Two configs in the same directory is a hard error in Oxlint, so order here
4
+ * only affects which one we report — not which one Oxlint uses.
5
+ * See `apps/oxlint/src/lib.rs` in oxc-project/oxc.
6
+ */
7
+ export declare const OXLINT_CONFIG_FILENAMES: string[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Config filenames Oxlint discovers on its own, in the order it declares them.
3
+ * Two configs in the same directory is a hard error in Oxlint, so order here
4
+ * only affects which one we report — not which one Oxlint uses.
5
+ * See `apps/oxlint/src/lib.rs` in oxc-project/oxc.
6
+ */
7
+ export const OXLINT_CONFIG_FILENAMES = [
8
+ '.oxlintrc.json',
9
+ '.oxlintrc.jsonc',
10
+ 'oxlint.config.ts',
11
+ 'oxlint.config.mts',
12
+ ];
@@ -0,0 +1,9 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ /**
3
+ * Enables Oxlint plugins for a single project, in that project's own config.
4
+ *
5
+ * A nested config *replaces* the one above it rather than merging into it, so
6
+ * the generated config extends its nearest ancestor explicitly. Without that
7
+ * `extends`, that config's `categories` and `rules` silently stop applying.
8
+ */
9
+ export declare function addPluginsToOxlintConfig(tree: Tree, projectRoot: string, plugins: string[]): void;
@@ -0,0 +1,106 @@
1
+ import { joinPathFragments, logger, offsetFromRoot, updateJson, writeJson, } from '@nx/devkit';
2
+ import { OXLINT_CONFIG_FILENAMES } from './config-file.js';
3
+ /**
4
+ * The root config a root project rewrites in place — though the `.json` check
5
+ * below still refuses `.jsonc`. Not where generated `extends` targets come from:
6
+ * those come from the ancestor walk, narrowed by the `.jsonc?` test in
7
+ * `addPluginsToOxlintConfig` rather than by this list.
8
+ */
9
+ const EXTENDABLE_CONFIG_FILENAMES = OXLINT_CONFIG_FILENAMES.filter((file) => /\.jsonc?$/.test(file));
10
+ function findRootOxlintConfig(tree) {
11
+ return EXTENDABLE_CONFIG_FILENAMES.find((file) => tree.exists(file)) ?? null;
12
+ }
13
+ /**
14
+ * Nearest config above `projectRoot` — the one Oxlint resolves to, and so the
15
+ * only correct `extends` target. Probes every filename, not just the extendable
16
+ * ones: walking past a TypeScript ancestor to reach the root would point the
17
+ * `extends` at a config that is not the one being replaced.
18
+ */
19
+ function findNearestOxlintConfig(tree, projectRoot) {
20
+ let dir = projectRoot;
21
+ while (dir !== '.') {
22
+ dir = dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '.';
23
+ const found = OXLINT_CONFIG_FILENAMES.map((file) => joinPathFragments(dir, file)).find((path) => tree.exists(path));
24
+ if (found) {
25
+ return found;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ /**
31
+ * Enables Oxlint plugins for a single project, in that project's own config.
32
+ *
33
+ * A nested config *replaces* the one above it rather than merging into it, so
34
+ * the generated config extends its nearest ancestor explicitly. Without that
35
+ * `extends`, that config's `categories` and `rules` silently stop applying.
36
+ */
37
+ export function addPluginsToOxlintConfig(tree, projectRoot, plugins) {
38
+ if (!plugins.length) {
39
+ return;
40
+ }
41
+ // Probe every filename Oxlint honours, not just the editable ones: two configs
42
+ // in one directory is a hard error in Oxlint, not an override, so a config we
43
+ // cannot edit still has to stop us writing `.oxlintrc.json` beside it.
44
+ const existingProjectConfig = projectRoot === '.'
45
+ ? undefined
46
+ : OXLINT_CONFIG_FILENAMES.map((file) => joinPathFragments(projectRoot, file)).find((path) => tree.exists(path));
47
+ // The governing config's format only constrains us when a project config has
48
+ // to be created, since that is what needs an `extends` pointing at it. A
49
+ // project that already has its own editable config can be updated whatever
50
+ // sits above it. A root project is its own config, so it always needs one.
51
+ const nearestConfigPath = projectRoot === '.'
52
+ ? findRootOxlintConfig(tree)
53
+ : findNearestOxlintConfig(tree, projectRoot);
54
+ // A TypeScript ancestor governs the project but cannot be extended, so it is
55
+ // no more usable as a target than having no config at all.
56
+ const governingConfigPath = nearestConfigPath && /\.jsonc?$/.test(nearestConfigPath)
57
+ ? nearestConfigPath
58
+ : null;
59
+ if (!governingConfigPath && !existingProjectConfig) {
60
+ // A TypeScript config cannot be rewritten statically. Say so — otherwise
61
+ // the generator reports success and the plugins silently never run. Name the
62
+ // file too: it can be an ancestor several levels up, so "your Oxlint config"
63
+ // sends the user looking in the project directory, where there is nothing.
64
+ logger.warn(`Could not enable the Oxlint plugin(s) ${plugins.join(', ')} for "${projectRoot}": only JSON Oxlint configs can be updated automatically. ` +
65
+ `Add them to the "plugins" array of ${nearestConfigPath ? `"${nearestConfigPath}"` : 'your Oxlint config'} manually.`);
66
+ return;
67
+ }
68
+ const projectConfigPath = projectRoot === '.'
69
+ ? governingConfigPath
70
+ : (existingProjectConfig ??
71
+ joinPathFragments(projectRoot, '.oxlintrc.json'));
72
+ // Only plain JSON survives a rewrite: without this, `updateJson` strips a
73
+ // `.jsonc`'s comments and throws outright on a TypeScript config.
74
+ if (!projectConfigPath.endsWith('.json')) {
75
+ const reason = projectConfigPath.endsWith('.jsonc')
76
+ ? `rewriting ${projectConfigPath} would strip its comments`
77
+ : `${projectConfigPath} is not JSON and cannot be updated automatically`;
78
+ logger.warn(`Could not enable the Oxlint plugin(s) ${plugins.join(', ')} for "${projectRoot}": ${reason}. ` +
79
+ `Add them to its "plugins" array manually.`);
80
+ return;
81
+ }
82
+ if (tree.exists(projectConfigPath)) {
83
+ updateJson(tree, projectConfigPath, (json) => {
84
+ json.plugins = union(json.plugins ?? [], plugins);
85
+ // Not added automatically: a config without `extends` may be isolating
86
+ // deliberately. Silent when an `extends` exists (it may reach the governing
87
+ // config through a preset) or the nearest ancestor is TypeScript.
88
+ if (governingConfigPath && !json.extends?.length && projectRoot !== '.') {
89
+ logger.warn(`"${projectRoot}" has an Oxlint config with no "extends", so ${governingConfigPath}'s ` +
90
+ `categories and rules do not apply to it. The plugin(s) ${plugins.join(', ')} still run, but under Oxlint's defaults — so this project can pass lint on ` +
91
+ `violations ${governingConfigPath} would fail it for. Add "extends": ["${joinPathFragments(offsetFromRoot(projectRoot), governingConfigPath)}"] if that is not intended.`);
92
+ }
93
+ return json;
94
+ });
95
+ return;
96
+ }
97
+ writeJson(tree, projectConfigPath, {
98
+ extends: [
99
+ joinPathFragments(offsetFromRoot(projectRoot), governingConfigPath),
100
+ ],
101
+ plugins: [...plugins],
102
+ });
103
+ }
104
+ function union(a, b) {
105
+ return Array.from(new Set([...a, ...b]));
106
+ }
@@ -0,0 +1,14 @@
1
+ export declare const nxVersion: string;
2
+ /**
3
+ * The inference plugin enumerates lintable files by spawning
4
+ * `oxlint --debug=files`, which needs two things that only line up from 1.70.0:
5
+ * the `--debug` flag itself, and a `./package.json` entry in Oxlint's `exports`
6
+ * so `require.resolve('oxlint/package.json')` can locate the binary. Below that
7
+ * both fail and inference silently loses ignore-awareness.
8
+ *
9
+ * Earlier floors this supersedes: `oxlint.config.ts` support (oxc #17563) in
10
+ * 1.43.0, which `OXLINT_CONFIG_FILENAMES` still depends on, the JS plugin API
11
+ * the boundaries bridge needs in 1.16.0, and `--type-aware` in 1.11.0.
12
+ */
13
+ export declare const minSupportedOxlintVersion = "1.70.0";
14
+ export declare const oxlintVersion = "^1.75.0";
@@ -0,0 +1,16 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export const nxVersion = require('@nx/oxlint/package.json').version;
4
+ /**
5
+ * The inference plugin enumerates lintable files by spawning
6
+ * `oxlint --debug=files`, which needs two things that only line up from 1.70.0:
7
+ * the `--debug` flag itself, and a `./package.json` entry in Oxlint's `exports`
8
+ * so `require.resolve('oxlint/package.json')` can locate the binary. Below that
9
+ * both fail and inference silently loses ignore-awareness.
10
+ *
11
+ * Earlier floors this supersedes: `oxlint.config.ts` support (oxc #17563) in
12
+ * 1.43.0, which `OXLINT_CONFIG_FILENAMES` still depends on, the JS plugin API
13
+ * the boundaries bridge needs in 1.16.0, and `--type-aware` in 1.11.0.
14
+ */
15
+ export const minSupportedOxlintVersion = '1.70.0';
16
+ export const oxlintVersion = '^1.75.0';
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "Nx Oxlint",
3
+ "version": "0.1",
4
+ "generators": {
5
+ "init": {
6
+ "factory": "./dist/src/generators/init/init#initGenerator",
7
+ "schema": "./dist/src/generators/init/schema.json",
8
+ "description": "Initialize the `@nx/oxlint` plugin.",
9
+ "aliases": ["ng-add"],
10
+ "hidden": true
11
+ },
12
+ "configuration": {
13
+ "factory": "./dist/src/generators/configuration/configuration#configurationGenerator",
14
+ "schema": "./dist/src/generators/configuration/schema.json",
15
+ "description": "Configure Oxlint for a project."
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "generators": {},
3
+ "packageJsonUpdates": {}
4
+ }
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@nx/oxlint",
3
+ "description": "Experimental. The Oxlint plugin for Nx contains generators and utilities used for linting JavaScript/TypeScript projects within an Nx workspace.",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "!dist/tsconfig.tsbuildinfo",
9
+ "migrations.json",
10
+ "generators.json"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/nrwl/nx.git",
18
+ "directory": "packages/oxlint"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/nrwl/nx/issues"
22
+ },
23
+ "keywords": [
24
+ "Monorepo",
25
+ "Web",
26
+ "Lint",
27
+ "Oxlint",
28
+ "CLI"
29
+ ],
30
+ "author": "Victor Savkin",
31
+ "license": "MIT",
32
+ "homepage": "https://nx.dev",
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "typesVersions": {
36
+ "*": {
37
+ "generators": [
38
+ "dist/generators.d.ts"
39
+ ],
40
+ "boundaries-plugin": [
41
+ "dist/src/boundaries-plugin/index.d.ts"
42
+ ]
43
+ }
44
+ },
45
+ "exports": {
46
+ ".": {
47
+ "@nx/nx-source": "./index.ts",
48
+ "types": "./dist/index.d.ts",
49
+ "default": "./dist/index.js"
50
+ },
51
+ "./package.json": "./package.json",
52
+ "./migrations.json": "./migrations.json",
53
+ "./generators.json": "./generators.json",
54
+ "./generators": {
55
+ "@nx/nx-source": "./generators.ts",
56
+ "types": "./dist/generators.d.ts",
57
+ "default": "./dist/generators.js"
58
+ },
59
+ "./boundaries-plugin": {
60
+ "@nx/nx-source": "./src/boundaries-plugin/index.ts",
61
+ "types": "./dist/src/boundaries-plugin/index.d.ts",
62
+ "default": "./dist/src/boundaries-plugin/index.js"
63
+ }
64
+ },
65
+ "nx-migrations": {
66
+ "migrations": "./migrations.json",
67
+ "supportsOptionalMigrations": true
68
+ },
69
+ "generators": "./generators.json",
70
+ "dependencies": {
71
+ "@nx/devkit": "workspace:*",
72
+ "@nx/eslint-plugin": "workspace:*",
73
+ "@nx/js": "workspace:*",
74
+ "tslib": "catalog:typescript"
75
+ },
76
+ "peerDependencies": {
77
+ "oxlint": "^1.43.0"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "oxlint": {
81
+ "optional": true
82
+ }
83
+ },
84
+ "engines": {
85
+ "node": "^20.19.0 || >=22.12.0"
86
+ },
87
+ "devDependencies": {
88
+ "nx": "workspace:*"
89
+ }
90
+ }