Files
Craigory Coppola ee0bc60a33 fix(misc): move plugin internal imports to devkit/internal (#36430)
## Current Behavior

First-party plugins reach into `nx` internals through deep subpaths
(`nx/src/executors/run-commands/run-commands.impl`,
`nx/src/generators/tree`, `nx/src/plugins/js/lock-file/lock-file`, …),
so there is no single boundary between the plugins and nx's internal
module layout.

## Expected Behavior

Those imports are routed through the `@nx/devkit/internal` barrel, and a
lint rule enforces the boundary. `nx/release` stays exempt as a public,
stable entry point for release-extension plugins.

### Note on `packages/nest/test-setup.ts`

This one is a latent-bug fix, not lint appeasement. The previous setup
installed the project-graph stub with
`jest.spyOn(require('nx/src/project-graph/project-graph'),
'createProjectGraphAsync')` at module scope. `jest.restoreAllMocks()`
undoes anything installed via `jest.spyOn`, and four nest suites —
`init`, `library`, `application` and `run-nest-schematic` — call it from
an `afterAll` hook. Since `test-setup.ts` is wired in through
`setupFilesAfterEnv` and runs once per test file, that `afterAll` tore
the stub down for the rest of the file, so any later `describe` block
silently fell through to the **real** `createProjectGraphAsync` instead
of the stub. Switching to a `jest.mock(...)` module factory fixes it:
module-registry substitution is not affected by `restoreAllMocks()`, so
the stub now survives the whole file as intended.

## Related Issue(s)

<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes NXC-4748

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-08-05 13:00:44 -04:00

291 lines
7.9 KiB
TypeScript

import {
getNamedInputs,
calculateHashesForCreateNodes,
loadConfigFile,
PluginCache,
workspaceDataDirectory,
hashObject,
} from '@nx/devkit/internal';
import {
AggregateCreateNodesError,
CreateNodesContext,
createNodesFromFiles,
CreateNodesResult,
CreateNodesResultArray,
CreateNodes,
detectPackageManager,
joinPathFragments,
NxJsonConfiguration,
readJsonFile,
TargetConfiguration,
} from '@nx/devkit';
import { dirname, join } from 'path';
import { getLockFileName } from '@nx/js';
import { readdirSync } from 'fs';
export interface ReactNativePluginOptions {
startTargetName?: string;
podInstallTargetName?: string;
runIosTargetName?: string;
runAndroidTargetName?: string;
buildIosTargetName?: string;
buildAndroidTargetName?: string;
bundleTargetName?: string;
syncDepsTargetName?: string;
upgradeTargetName?: string;
}
type ReactNativeTargets = Record<
string,
TargetConfiguration<ReactNativePluginOptions>
>;
export const createNodes: CreateNodes<ReactNativePluginOptions> = [
'**/app.{json,config.js,config.ts}',
async (configFiles, options, context) => {
const optionsHash = hashObject(options);
const cachePath = join(
workspaceDataDirectory,
`react-native-${optionsHash}.hash`
);
const targetsCache = new PluginCache<ReactNativeTargets>(cachePath);
const lockFileName = getLockFileName(
detectPackageManager(context.workspaceRoot)
);
const normalizedOptions = normalizeOptions(options);
try {
const { entries, preErrors } = await filterReactNativeConfigs(
configFiles,
context
);
const projectHashes = await calculateHashesForCreateNodes(
entries.map((e) => e.projectRoot),
normalizedOptions,
context,
entries.map(() => [lockFileName])
);
let results: CreateNodesResultArray = [];
let nodeErrors: Array<[string | null, Error]> = [];
try {
results = await createNodesFromFiles(
(configFile, _, ctx, idx) =>
createNodesInternal(
configFile,
normalizedOptions,
ctx,
targetsCache,
projectHashes[idx]
),
entries.map((e) => e.configFile),
options,
context
);
} catch (e) {
if (e instanceof AggregateCreateNodesError) {
results = e.partialResults ?? [];
nodeErrors = e.errors;
} else {
throw e;
}
}
const allErrors = [...preErrors, ...nodeErrors];
if (allErrors.length > 0) {
throw new AggregateCreateNodesError(allErrors, results);
}
return results;
} finally {
targetsCache.writeToDisk();
}
},
];
export const createNodesV2 = createNodes;
async function createNodesInternal(
configFile: string,
options: ReactNativePluginOptions,
context: CreateNodesContext,
targetsCache: PluginCache<ReactNativeTargets>,
hash: string
): Promise<CreateNodesResult> {
const projectRoot = dirname(configFile);
if (!targetsCache.has(hash)) {
targetsCache.set(
hash,
buildReactNativeTargets(projectRoot, options, context)
);
}
return {
projects: {
[projectRoot]: {
targets: targetsCache.get(hash),
},
},
};
}
function buildReactNativeTargets(
projectRoot: string,
options: ReactNativePluginOptions,
context: CreateNodesContext
) {
const namedInputs = getNamedInputs(projectRoot, context);
const targets: Record<string, TargetConfiguration> = {
[options.startTargetName]: {
command: `react-native start`,
continuous: true,
options: { cwd: projectRoot },
},
[options.podInstallTargetName]: {
command: `pod install`,
options: { cwd: joinPathFragments(projectRoot, 'ios') },
dependsOn: [`${options.syncDepsTargetName}`],
},
[options.runIosTargetName]: {
command: `react-native run-ios`,
continuous: true,
options: { cwd: projectRoot },
},
[options.runAndroidTargetName]: {
command: `react-native run-android`,
continuous: true,
options: { cwd: projectRoot },
},
[options.buildIosTargetName]: {
command: `react-native build-ios`,
options: { cwd: projectRoot },
cache: true,
dependsOn: [`^${options.buildIosTargetName}`],
inputs: getInputs(namedInputs),
outputs: [getOutputs(projectRoot, 'ios/build/Build/Products')],
},
[options.buildAndroidTargetName]: {
command: `react-native build-android`,
options: { cwd: projectRoot },
cache: true,
dependsOn: [`^${options.buildAndroidTargetName}`],
inputs: getInputs(namedInputs),
outputs: [getOutputs(projectRoot, 'android/app/build/outputs')],
},
[options.bundleTargetName]: {
command: `react-native bundle`,
options: { cwd: projectRoot },
dependsOn: [`^${options.bundleTargetName}`],
inputs: getInputs(namedInputs),
},
[options.syncDepsTargetName]: {
executor: '@nx/react-native:sync-deps',
continuous: false,
},
[options.upgradeTargetName]: {
command: `react-native upgrade`,
options: { cwd: projectRoot },
},
};
return targets;
}
function getAppConfig(
configFilePath: string,
context: CreateNodesContext
): Promise<any> {
const resolvedPath = join(context.workspaceRoot, configFilePath);
return loadConfigFile(resolvedPath);
}
async function filterReactNativeConfigs(
configFiles: readonly string[],
context: CreateNodesContext
): Promise<{
entries: Array<{ configFile: string; projectRoot: string }>;
preErrors: Array<[string, Error]>;
}> {
const preErrors: Array<[string, Error]> = [];
const candidates = await Promise.all(
configFiles.map(
async (
configFile
): Promise<{ configFile: string; projectRoot: string } | null> => {
try {
const projectRoot = dirname(configFile);
const siblingFiles = readdirSync(
join(context.workspaceRoot, projectRoot)
);
if (
!siblingFiles.includes('package.json') ||
!siblingFiles.includes('metro.config.js')
) {
return null;
}
// Skip Expo projects; the @nx/expo plugin handles them.
const packageJson = readJsonFile(
join(context.workspaceRoot, projectRoot, 'package.json')
);
const appConfig = await getAppConfig(configFile, context);
if (
appConfig.expo ||
packageJson.dependencies?.['expo'] ||
packageJson.devDependencies?.['expo']
) {
return null;
}
return { configFile, projectRoot };
} catch (e) {
preErrors.push([configFile, e as Error]);
return null;
}
}
)
);
return {
entries: candidates.filter((c): c is NonNullable<typeof c> => c !== null),
preErrors,
};
}
function getInputs(
namedInputs: NxJsonConfiguration['namedInputs']
): TargetConfiguration['inputs'] {
return [
...('production' in namedInputs
? ['default', '^production']
: ['default', '^default']),
{
externalDependencies: ['react-native'],
},
];
}
function getOutputs(projectRoot: string, dir: string) {
if (projectRoot === '.') {
return `{projectRoot}/${dir}`;
} else {
return `{workspaceRoot}/${projectRoot}/${dir}`;
}
}
function normalizeOptions(
options: ReactNativePluginOptions
): ReactNativePluginOptions {
options ??= {};
options.startTargetName ??= 'start';
options.podInstallTargetName ??= 'pod-install';
options.runIosTargetName ??= 'run-ios';
options.runAndroidTargetName ??= 'run-android';
options.buildIosTargetName ??= 'build-ios';
options.buildAndroidTargetName ??= 'build-android';
options.bundleTargetName ??= 'bundle';
options.syncDepsTargetName ??= 'sync-deps';
options.upgradeTargetName ??= 'upgrade';
return options;
}