feat(angular): add plugin for inferring nodes from angular.json files (#27804)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
<!-- Fixes NXC-887 -->

Fixes #

---------

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
Leosvel Pérez Espinosa 2024-09-17 17:00:15 +02:00 committed by GitHub
parent 2be7424fc4
commit e0b3e73d7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 2256 additions and 44 deletions

View File

@ -0,0 +1,172 @@
import {
checkFilesExist,
cleanupProject,
getPackageManagerCommand,
getSelectedPackageManager,
isVerbose,
isVerboseE2ERun,
logInfo,
newProject,
runCLI,
runCommand,
tmpProjPath,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { angularCliVersion } from '@nx/workspace/src/utils/versions';
import { ensureDirSync } from 'fs-extra';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
describe('Angular Crystal Plugin', () => {
let proj: string;
beforeAll(() => {
proj = newProject({
packages: ['@nx/angular'],
unsetProjectNameAndRootFormat: false,
});
if (getSelectedPackageManager() === 'pnpm') {
updateFile(
'pnpm-workspace.yaml',
`packages:
- 'projects/*'
`
);
} else {
updateJson('package.json', (json) => {
json.workspaces = ['projects/*'];
return json;
});
}
});
afterAll(() => cleanupProject());
it('should infer tasks from multiple angular.json files', () => {
const ngOrg1App1 = uniq('ng-org1-app1');
const ngOrg1Lib1 = uniq('ng-org1-lib1');
const org1Root = join(tmpProjPath(), 'projects', ngOrg1App1);
const ngOrg2App1 = uniq('ng-org2-app1');
const ngOrg2Lib1 = uniq('ng-org2-lib1');
const org2Root = join(tmpProjPath(), 'projects', ngOrg2App1);
const pmc = getPackageManagerCommand();
// first angular inner repo (e.g. imported with nx import)
runNgNew(ngOrg1App1, 'projects');
// exclude scripts from nx, to prevent them to override the inferred tasks
updateJson(`projects/${ngOrg1App1}/package.json`, (json) => {
json.nx = { includedScripts: [] };
return json;
});
runCommand(pmc.run(`ng g @schematics/angular:library ${ngOrg1Lib1}`, ''), {
cwd: org1Root,
});
// second angular inner repo
runNgNew(ngOrg2App1, 'projects');
// exclude scripts from nx
updateJson(`projects/${ngOrg2App1}/package.json`, (json) => {
json.nx = { includedScripts: [] };
return json;
});
runCommand(pmc.run(`ng g @schematics/angular:library ${ngOrg2Lib1}`, ''), {
cwd: org2Root,
});
// add Angular Crystal plugin
updateJson('nx.json', (json) => {
json.plugins ??= [];
json.plugins.push('@nx/angular/plugin');
return json;
});
// check org1 tasks
// build
runCLI(`build ${ngOrg1App1} --output-hashing none`);
checkFilesExist(
`projects/${ngOrg1App1}/dist/${ngOrg1App1}/browser/main.js`
);
expect(runCLI(`build ${ngOrg1App1} --output-hashing none`)).toContain(
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
);
runCLI(`build ${ngOrg1Lib1}`);
checkFilesExist(
`projects/${ngOrg1App1}/dist/${ngOrg1Lib1}/fesm2022/${ngOrg1Lib1}.mjs`
);
expect(runCLI(`build ${ngOrg1Lib1}`)).toContain(
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
);
// test
expect(
runCLI(
`run-many -t test -p ${ngOrg1App1},${ngOrg1Lib1} --no-watch --browsers=ChromeHeadless`
)
).toContain('Successfully ran target test for 2 projects');
expect(
runCLI(
`run-many -t test -p ${ngOrg1App1},${ngOrg1Lib1} --no-watch --browsers=ChromeHeadless`
)
).toContain(
'Nx read the output from the cache instead of running the command for 2 out of 2 tasks'
);
// check org2 tasks
// build
runCLI(`build ${ngOrg2App1} --output-hashing none`);
checkFilesExist(
`projects/${ngOrg2App1}/dist/${ngOrg2App1}/browser/main.js`
);
expect(runCLI(`build ${ngOrg2App1} --output-hashing none`)).toContain(
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
);
runCLI(`build ${ngOrg2Lib1}`);
checkFilesExist(
`projects/${ngOrg2App1}/dist/${ngOrg2Lib1}/fesm2022/${ngOrg2Lib1}.mjs`
);
expect(runCLI(`build ${ngOrg2Lib1}`)).toContain(
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
);
// test
expect(
runCLI(
`run-many -t test -p ${ngOrg2App1},${ngOrg2Lib1} --no-watch --browsers=ChromeHeadless`
)
).toContain('Successfully ran target test for 2 projects');
expect(
runCLI(
`run-many -t test -p ${ngOrg2App1},${ngOrg2Lib1} --no-watch --browsers=ChromeHeadless`
)
).toContain(
'Nx read the output from the cache instead of running the command for 2 out of 2 tasks'
);
});
});
function runNgNew(projectName: string, cwd: string): void {
const packageManager = getSelectedPackageManager();
const pmc = getPackageManagerCommand({ packageManager });
const command = `${pmc.runUninstalledPackage} @angular/cli@${angularCliVersion} new ${projectName} --package-manager=${packageManager}`;
cwd = join(tmpProjPath(), cwd);
ensureDirSync(cwd);
execSync(command, {
cwd,
stdio: isVerbose() ? 'inherit' : 'pipe',
env: process.env,
encoding: 'utf-8',
});
if (isVerboseE2ERun()) {
logInfo(
`NX`,
`E2E created an Angular CLI project at ${join(cwd, projectName)}`
);
}
}

View File

@ -4,6 +4,7 @@ import {
getSelectedPackageManager, getSelectedPackageManager,
newProject, newProject,
runCLI, runCLI,
runCommand,
updateJson, updateJson,
updateFile, updateFile,
e2eCwd, e2eCwd,
@ -38,7 +39,11 @@ describe('Nx Import', () => {
try { try {
rmdirSync(join(tempImportE2ERoot)); rmdirSync(join(tempImportE2ERoot));
} catch {} } catch {}
runCommand(`git add .`);
runCommand(`git commit -am "Update" --allow-empty`);
}); });
afterAll(() => cleanupProject()); afterAll(() => cleanupProject());
it('should be able to import a vite app', () => { it('should be able to import a vite app', () => {
@ -111,7 +116,7 @@ describe('Nx Import', () => {
}); });
mkdirSync(join(repoPath, 'packages/a'), { recursive: true }); mkdirSync(join(repoPath, 'packages/a'), { recursive: true });
writeFileSync(join(repoPath, 'packages/a/README.md'), `# A`); writeFileSync(join(repoPath, 'packages/a/README.md'), `# A`);
execSync(`git add packages/a`, { execSync(`git add .`, {
cwd: repoPath, cwd: repoPath,
}); });
execSync(`git commit -m "add package a"`, { execSync(`git commit -m "add package a"`, {
@ -119,7 +124,7 @@ describe('Nx Import', () => {
}); });
mkdirSync(join(repoPath, 'packages/b'), { recursive: true }); mkdirSync(join(repoPath, 'packages/b'), { recursive: true });
writeFileSync(join(repoPath, 'packages/b/README.md'), `# B`); writeFileSync(join(repoPath, 'packages/b/README.md'), `# B`);
execSync(`git add packages/b`, { execSync(`git add .`, {
cwd: repoPath, cwd: repoPath,
}); });
execSync(`git commit -m "add package b"`, { execSync(`git commit -m "add package b"`, {

View File

@ -22,6 +22,7 @@
"./executors.json": "./executors.json", "./executors.json": "./executors.json",
"./generators": "./generators.js", "./generators": "./generators.js",
"./executors": "./executors.js", "./executors": "./executors.js",
"./plugin": "./plugin.js",
"./tailwind": "./tailwind.js", "./tailwind": "./tailwind.js",
"./module-federation": "./module-federation/index.js", "./module-federation": "./module-federation/index.js",
"./src/utils": "./src/utils/index.js", "./src/utils": "./src/utils/index.js",

View File

@ -0,0 +1 @@
export { createNodesV2 } from './src/plugins/plugin';

View File

@ -1,13 +1,16 @@
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
createProjectGraphAsync,
ensurePackage, ensurePackage,
formatFiles, formatFiles,
type GeneratorCallback,
logger, logger,
readNxJson, readNxJson,
type GeneratorCallback,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { getInstalledPackageVersion, versions } from '../utils/version-utils'; import { getInstalledPackageVersion, versions } from '../utils/version-utils';
import { createNodesV2 } from '../../plugins/plugin';
import { Schema } from './schema'; import { Schema } from './schema';
export async function angularInitGenerator( export async function angularInitGenerator(
@ -17,6 +20,25 @@ export async function angularInitGenerator(
ignoreAngularCacheDirectory(tree); ignoreAngularCacheDirectory(tree);
const installTask = installAngularDevkitCoreIfMissing(tree, options); const installTask = installAngularDevkitCoreIfMissing(tree, options);
// For Angular inference plugin, we only want it during import since our
// generators do not use `angular.json`, and `nx init` should split
// `angular.json` into multiple `project.json` files -- as this is preferred
// by most folks we've talked to.
options.addPlugin ??= process.env.NX_RUNNING_NX_IMPORT === 'true';
if (options.addPlugin) {
await addPlugin(
tree,
await createProjectGraphAsync(),
'@nx/angular/plugin',
createNodesV2,
{
targetNamePrefix: ['', 'angular:', 'angular-'],
},
options.updatePackageScripts
);
}
if (!options.skipFormat) { if (!options.skipFormat) {
await formatFiles(tree); await formatFiles(tree);
} }

View File

@ -3,4 +3,7 @@ export interface Schema {
skipInstall?: boolean; skipInstall?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;
keepExistingVersions?: boolean; keepExistingVersions?: boolean;
/* internal */
addPlugin?: boolean;
updatePackageScripts?: boolean;
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,763 @@
import {
type CreateNodesContextV2,
createNodesFromFiles,
type CreateNodesResult,
type CreateNodesV2,
detectPackageManager,
getPackageManagerCommand,
type ProjectConfiguration,
readJsonFile,
type Target,
type TargetConfiguration,
writeJsonFile,
} from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { getLockFileName } from '@nx/js';
import { existsSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import * as posix from 'node:path/posix';
import { hashObject } from 'nx/src/devkit-internals';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
export interface AngularPluginOptions {
targetNamePrefix?: string;
}
type AngularProjects = Record<
string,
Pick<ProjectConfiguration, 'projectType' | 'sourceRoot' | 'targets'>
>;
type AngularTargetConfiguration = {
builder: string;
options?: Record<string, any>;
configurations?: Record<string, any>;
defaultConfiguration?: string;
};
export type AngularProjectConfiguration = {
projectType: 'application' | 'library';
root: string;
sourceRoot?: string;
architect?: Record<string, AngularTargetConfiguration>;
targets?: Record<string, AngularTargetConfiguration>;
};
type AngularJson = { projects?: Record<string, AngularProjectConfiguration> };
const knownExecutors = {
appShell: new Set(['@angular-devkit/build-angular:app-shell']),
build: new Set([
'@angular-devkit/build-angular:application',
'@angular/build:application',
'@angular-devkit/build-angular:browser-esbuild',
'@angular-devkit/build-angular:browser',
'@angular-devkit/build-angular:ng-packagr',
]),
devServer: new Set(['@angular-devkit/build-angular:dev-server']),
extractI18n: new Set(['@angular-devkit/build-angular:extract-i18n']),
prerender: new Set([
'@angular-devkit/build-angular:prerender',
'@nguniversal/builders:prerender',
]),
server: new Set(['@angular-devkit/build-angular:server']),
serveSsr: new Set([
'@angular-devkit/build-angular:ssr-dev-server',
'@nguniversal/builders:ssr-dev-server',
]),
test: new Set(['@angular-devkit/build-angular:karma']),
};
const pmc = getPackageManagerCommand();
function readProjectsCache(cachePath: string): Record<string, AngularProjects> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}
function writeProjectsToCache(
cachePath: string,
results: Record<string, AngularProjects>
) {
writeJsonFile(cachePath, results);
}
export const createNodesV2: CreateNodesV2<AngularPluginOptions> = [
'**/angular.json',
async (configFiles, options, context) => {
const optionsHash = hashObject(options);
const cachePath = join(
workspaceDataDirectory,
`angular-${optionsHash}.hash`
);
const projectsCache = readProjectsCache(cachePath);
try {
return await createNodesFromFiles(
(configFile, options, context) =>
createNodesInternal(configFile, options, context, projectsCache),
configFiles,
options,
context
);
} finally {
writeProjectsToCache(cachePath, projectsCache);
}
},
];
async function createNodesInternal(
configFilePath: string,
options: {} | undefined,
context: CreateNodesContextV2,
projectsCache: Record<string, AngularProjects>
): Promise<CreateNodesResult> {
const angularWorkspaceRoot = dirname(configFilePath);
// Do not create a project if package.json isn't there
const siblingFiles = readdirSync(
join(context.workspaceRoot, angularWorkspaceRoot)
);
if (!siblingFiles.includes('package.json')) {
return {};
}
const hash = await calculateHashForCreateNodes(
angularWorkspaceRoot,
options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
projectsCache[hash] ??= await buildAngularProjects(
configFilePath,
options,
angularWorkspaceRoot,
context
);
return { projects: projectsCache[hash] };
}
async function buildAngularProjects(
configFilePath: string,
options: AngularPluginOptions,
angularWorkspaceRoot: string,
context: CreateNodesContextV2
): Promise<AngularProjects> {
const projects: Record<string, AngularProjects[string] & { root: string }> =
{};
const absoluteConfigFilePath = join(context.workspaceRoot, configFilePath);
const angularJson = readJsonFile<AngularJson>(absoluteConfigFilePath);
const appShellTargets: Target[] = [];
const prerenderTargets: Target[] = [];
for (const [projectName, project] of Object.entries(
angularJson.projects ?? {}
)) {
const targets: Record<string, TargetConfiguration> = {};
const projectTargets = getAngularJsonProjectTargets(project);
if (!projectTargets) {
continue;
}
const namedInputs = getNamedInputs(project.root, context);
for (const [angularTargetName, angularTarget] of Object.entries(
projectTargets
)) {
const nxTargetName = options?.targetNamePrefix
? `${options.targetNamePrefix}${angularTargetName}`
: angularTargetName;
const externalDependencies = ['@angular/cli'];
targets[nxTargetName] = {
command:
// For targets that are also Angular CLI commands, infer the simplified form.
// Otherwise, use `ng run` to support non-command targets so that they will run.
angularTargetName === 'build' ||
angularTargetName === 'deploy' ||
angularTargetName === 'extract-i18n' ||
angularTargetName === 'e2e' ||
angularTargetName === 'lint' ||
angularTargetName === 'serve' ||
angularTargetName === 'test'
? `ng ${angularTargetName}`
: `ng run ${projectName}:${angularTargetName}`,
options: { cwd: angularWorkspaceRoot },
metadata: {
technologies: ['angular'],
description: `Run the "${angularTargetName}" target for "${projectName}".`,
help: {
command: `${pmc.exec} ng run ${projectName}:${angularTargetName} --help`,
example: {},
},
},
};
if (knownExecutors.appShell.has(angularTarget.builder)) {
appShellTargets.push({ target: nxTargetName, project: projectName });
} else if (knownExecutors.build.has(angularTarget.builder)) {
await updateBuildTarget(
nxTargetName,
targets[nxTargetName],
angularTarget,
context,
angularWorkspaceRoot,
project.root,
namedInputs
);
} else if (knownExecutors.devServer.has(angularTarget.builder)) {
targets[nxTargetName].metadata.help.example.options = { port: 4201 };
} else if (knownExecutors.extractI18n.has(angularTarget.builder)) {
targets[nxTargetName].metadata.help.example.options = {
format: 'json',
};
} else if (knownExecutors.test.has(angularTarget.builder)) {
updateTestTarget(
targets[nxTargetName],
angularTarget,
context,
angularWorkspaceRoot,
project.root,
namedInputs,
externalDependencies
);
} else if (knownExecutors.server.has(angularTarget.builder)) {
updateServerTarget(
targets[nxTargetName],
angularTarget,
context,
angularWorkspaceRoot,
project.root,
namedInputs
);
} else if (knownExecutors.serveSsr.has(angularTarget.builder)) {
targets[nxTargetName].metadata.help.example.options = { port: 4201 };
} else if (knownExecutors.prerender.has(angularTarget.builder)) {
prerenderTargets.push({ target: nxTargetName, project: projectName });
}
if (targets[nxTargetName].inputs?.length) {
targets[nxTargetName].inputs.push({ externalDependencies });
}
if (angularTarget.configurations) {
for (const configurationName of Object.keys(
angularTarget.configurations
)) {
targets[nxTargetName].configurations = {
...targets[nxTargetName].configurations,
[configurationName]: {
command: `ng run ${projectName}:${angularTargetName}:${configurationName}`,
},
};
}
}
if (angularTarget.defaultConfiguration) {
targets[nxTargetName].defaultConfiguration =
angularTarget.defaultConfiguration;
}
}
projects[projectName] = {
projectType: project.projectType,
root: posix.join(angularWorkspaceRoot, project.root),
sourceRoot: project.sourceRoot
? posix.join(angularWorkspaceRoot, project.sourceRoot)
: undefined,
targets,
};
}
for (const { project, target } of appShellTargets) {
updateAppShellTarget(
project,
target,
projects,
angularJson,
angularWorkspaceRoot,
context
);
}
for (const { project, target } of prerenderTargets) {
updatePrerenderTarget(project, target, projects, angularJson);
}
return Object.entries(projects).reduce((acc, [projectName, project]) => {
acc[project.root] = {
projectType: project.projectType,
sourceRoot: project.sourceRoot,
targets: project.targets,
};
return acc;
}, {} as AngularProjects);
}
function updateAppShellTarget(
projectName: string,
targetName: string,
projects: AngularProjects,
angularJson: AngularJson,
angularWorkspaceRoot: string,
context: CreateNodesContextV2
): void {
// it must exist since we collected it when processing it
const target = projects[projectName].targets[targetName];
target.metadata.help.example.options = { route: '/some/route' };
const { inputs, outputs } = getBrowserAndServerTargetInputsAndOutputs(
projectName,
targetName,
projects,
angularJson
);
const outputIndexPath = getAngularJsonProjectTargets(
angularJson.projects[projectName]
)[targetName].options?.outputIndexPath;
if (outputIndexPath) {
const fullOutputIndexPath = join(
context.workspaceRoot,
angularWorkspaceRoot,
outputIndexPath
);
outputs.push(
getOutput(
fullOutputIndexPath,
context.workspaceRoot,
angularWorkspaceRoot,
angularJson.projects[projectName].root
)
);
}
if (!outputs.length) {
// no outputs were identified for the build or server target, so we don't
// set any Nx cache options
return;
}
target.cache = true;
target.inputs = inputs;
target.outputs = outputs;
}
async function updateBuildTarget(
targetName: string,
target: TargetConfiguration,
angularTarget: AngularTargetConfiguration,
context: CreateNodesContextV2,
angularWorkspaceRoot: string,
projectRoot: string,
namedInputs: ReturnType<typeof getNamedInputs>
): Promise<void> {
target.dependsOn = [`^${targetName}`];
if (angularTarget.options?.outputPath) {
const fullOutputPath = join(
context.workspaceRoot,
angularWorkspaceRoot,
angularTarget.options.outputPath
);
target.outputs = [
getOutput(
fullOutputPath,
context.workspaceRoot,
angularWorkspaceRoot,
projectRoot
),
];
} else if (
angularTarget.builder === '@angular-devkit/build-angular:ng-packagr'
) {
const outputs = await getNgPackagrOutputs(
angularTarget,
angularWorkspaceRoot,
projectRoot,
context
);
if (outputs.length) {
target.outputs = outputs;
}
}
if (target.outputs?.length) {
// make it cacheable if we were able to identify outputs
target.cache = true;
target.inputs =
'production' in namedInputs
? ['production', '^production']
: ['default', '^default'];
}
if (angularTarget.builder === '@angular-devkit/build-angular:ng-packagr') {
target.metadata.help.example.options = { watch: true };
} else {
target.metadata.help.example.options = { localize: true };
}
}
function updateTestTarget(
target: TargetConfiguration,
angularTarget: AngularTargetConfiguration,
context: CreateNodesContextV2,
angularWorkspaceRoot: string,
projectRoot: string,
namedInputs: ReturnType<typeof getNamedInputs>,
externalDependencies: string[]
): void {
target.cache = true;
target.inputs =
'production' in namedInputs
? ['default', '^production']
: ['default', '^default'];
target.outputs = getKarmaTargetOutputs(
angularTarget,
angularWorkspaceRoot,
projectRoot,
context
);
externalDependencies.push('karma');
target.metadata.help.example.options = { codeCoverage: true };
}
function updateServerTarget(
target: TargetConfiguration,
angularTarget: AngularTargetConfiguration,
context: CreateNodesContextV2,
angularWorkspaceRoot: string,
projectRoot: string,
namedInputs: ReturnType<typeof getNamedInputs>
): void {
target.metadata.help.example.options = { localize: true };
if (!angularTarget.options?.outputPath) {
// only make it cacheable if we were able to identify outputs
return;
}
target.cache = true;
target.inputs =
'production' in namedInputs
? ['production', '^production']
: ['default', '^default'];
const fullOutputPath = join(
context.workspaceRoot,
angularWorkspaceRoot,
angularTarget.options.outputPath
);
target.outputs = [
getOutput(
fullOutputPath,
context.workspaceRoot,
angularWorkspaceRoot,
projectRoot
),
];
}
function updatePrerenderTarget(
projectName: string,
targetName: string,
projects: AngularProjects,
angularJson: AngularJson
): void {
// it must exist since we collected it when processing it
const target = projects[projectName].targets[targetName];
target.metadata.help.example.options =
getAngularJsonProjectTargets(angularJson.projects[projectName])[targetName]
.builder === '@angular-devkit/build-angular:prerender'
? { discoverRoutes: false }
: { guessRoutes: false };
const { inputs, outputs } = getBrowserAndServerTargetInputsAndOutputs(
projectName,
targetName,
projects,
angularJson
);
if (!outputs.length) {
// no outputs were identified for the build or server target, so we don't
// set any Nx cache options
return;
}
target.cache = true;
target.inputs = inputs;
target.outputs = outputs;
}
async function getNgPackagrOutputs(
target: AngularTargetConfiguration,
angularWorkspaceRoot: string,
projectRoot: string,
context: CreateNodesContextV2
): Promise<string[]> {
let ngPackageJsonPath = join(
context.workspaceRoot,
angularWorkspaceRoot,
target.options.project
);
const readConfig = async (configPath: string) => {
if (!existsSync(configPath)) {
return undefined;
}
try {
if (configPath.endsWith('.js')) {
const result = await import(configPath);
return result['default'] ?? result;
}
return readJsonFile(configPath);
} catch {}
return undefined;
};
let ngPackageJson: { dest?: string };
let basePath: string;
if (statSync(ngPackageJsonPath).isDirectory()) {
basePath = ngPackageJsonPath;
ngPackageJson = await readConfig(
join(ngPackageJsonPath, 'ng-package.json')
);
if (!ngPackageJson) {
ngPackageJson = await readConfig(
join(ngPackageJsonPath, 'ng-package.js')
);
}
} else {
basePath = dirname(ngPackageJsonPath);
ngPackageJson = await readConfig(ngPackageJsonPath);
}
if (!ngPackageJson) {
return [];
}
const destination = ngPackageJson.dest
? join(basePath, ngPackageJson.dest)
: join(basePath, 'dist');
return [
getOutput(
destination,
context.workspaceRoot,
angularWorkspaceRoot,
projectRoot
),
];
}
function getKarmaTargetOutputs(
target: AngularTargetConfiguration,
angularWorkspaceRoot: string,
projectRoot: string,
context: CreateNodesContextV2
): string[] {
const defaultOutput = posix.join(
'{workspaceRoot}',
angularWorkspaceRoot,
'coverage/{projectName}'
);
if (!target.options?.karmaConfig) {
return [defaultOutput];
}
try {
const { parseConfig } = require('karma/lib/config');
const karmaConfigPath = join(
context.workspaceRoot,
angularWorkspaceRoot,
projectRoot,
target.options.karmaConfig
);
const config = parseConfig(karmaConfigPath);
if (config.coverageReporter.dir) {
return [
getOutput(
config.coverageReporter.dir,
context.workspaceRoot,
angularWorkspaceRoot,
projectRoot
),
];
}
} catch {
// we silently ignore any error here and fall back to the default output
}
return [defaultOutput];
}
function getBrowserAndServerTargetInputsAndOutputs(
projectName: string,
targetName: string,
projects: AngularProjects,
angularJson: AngularJson
) {
const { browserTarget, serverTarget } = extractBrowserAndServerTargets(
angularJson,
projectName,
targetName
);
if (!browserTarget || !serverTarget) {
// if any of these are missing, the target is invalid so we return empty values
return { inputs: [], outputs: [] };
}
const browserTargetInputs =
projects[browserTarget.project]?.targets?.[browserTarget.target]?.inputs ??
[];
const serverTargetInputs =
projects[serverTarget.project]?.targets?.[serverTarget.target]?.inputs ??
[];
const browserTargetOutputs =
projects[browserTarget.project]?.targets?.[browserTarget.target]?.outputs ??
[];
const serverTargetOutputs =
projects[serverTarget.project]?.targets?.[serverTarget.target]?.outputs ??
[];
return {
inputs: mergeInputs(...browserTargetInputs, ...serverTargetInputs),
outputs: Array.from(
new Set([...browserTargetOutputs, ...serverTargetOutputs])
),
};
}
function extractBrowserAndServerTargets(
angularJson: AngularJson,
projectName: string,
targetName: string
): {
browserTarget: Target;
serverTarget: Target;
} {
let browserTarget: Target | undefined;
let serverTarget: Target | undefined;
try {
const targets = getAngularJsonProjectTargets(
angularJson.projects[projectName]
);
const target = targets[targetName];
let browserTargetSpecifier = target.options?.browserTarget;
if (!browserTargetSpecifier) {
const configuration = Object.values(target.configurations ?? {}).find(
(config) => !!config.browserTarget
);
browserTargetSpecifier = configuration?.browserTarget;
}
if (browserTargetSpecifier) {
browserTarget = targetFromTargetString(
browserTargetSpecifier,
projectName,
targetName
);
}
let serverTargetSpecifier = target.options?.serverTarget;
if (!serverTargetSpecifier) {
serverTargetSpecifier = Object.values(target.configurations ?? {}).find(
(config) => !!config.serverTarget
)?.serverTarget;
}
if (serverTargetSpecifier) {
serverTarget = targetFromTargetString(
serverTargetSpecifier,
projectName,
targetName
);
}
} catch {}
return { browserTarget: browserTarget, serverTarget };
}
function mergeInputs(
...inputs: TargetConfiguration['inputs']
): TargetConfiguration['inputs'] {
const stringInputs = new Set<string>();
const externalDependencies = new Set<string>();
for (const input of inputs) {
if (typeof input === 'string') {
stringInputs.add(input);
} else if ('externalDependencies' in input) {
// we only infer external dependencies, so we don't need to handle the other input definitions
for (const externalDependency of input.externalDependencies) {
externalDependencies.add(externalDependency);
}
}
}
return [
...stringInputs,
...(externalDependencies.size
? [{ externalDependencies: Array.from(externalDependencies) }]
: []),
];
}
// angular support abbreviated target specifiers, this is adapter from:
// https://github.com/angular/angular-cli/blob/7d9ce246a33c60ec96eb4bf99520f5475716a910/packages/angular_devkit/architect/src/api.ts#L336
function targetFromTargetString(
specifier: string,
abbreviatedProjectName?: string,
abbreviatedTargetName?: string
) {
const tuple = specifier.split(':', 3);
if (tuple.length < 2) {
// invalid target, ignore
return undefined;
}
// we only care about project and target
return {
project: tuple[0] || abbreviatedProjectName || '',
target: tuple[1] || abbreviatedTargetName || '',
};
}
function getOutput(
path: string,
workspaceRoot: string,
angularWorkspaceRoot: string,
projectRoot: string
): string {
const relativePath = relative(
join(workspaceRoot, angularWorkspaceRoot, projectRoot),
path
);
if (relativePath.startsWith('..')) {
return posix.join(
'{workspaceRoot}',
join(angularWorkspaceRoot, projectRoot, relativePath)
);
} else {
return posix.join('{projectRoot}', relativePath);
}
}
function getAngularJsonProjectTargets(
project: AngularProjectConfiguration
): Record<string, AngularTargetConfiguration> {
return project.architect ?? project.targets;
}

View File

@ -104,21 +104,64 @@ async function _addPluginInternal<PluginOptions>(
let pluginOptions: PluginOptions; let pluginOptions: PluginOptions;
let projConfigs: ConfigurationResult; let projConfigs: ConfigurationResult;
const combinations = generateCombinations(options);
optionsLoop: for (const _pluginOptions of combinations) {
pluginOptions = _pluginOptions as PluginOptions;
nxJson.plugins ??= []; if (Object.keys(options).length > 0) {
if ( const combinations = generateCombinations(options);
nxJson.plugins.some((p) => optionsLoop: for (const _pluginOptions of combinations) {
typeof p === 'string' pluginOptions = _pluginOptions as PluginOptions;
? p === pluginName
: p.plugin === pluginName && !p.include nxJson.plugins ??= [];
) if (
) { nxJson.plugins.some((p) =>
// Plugin has already been added typeof p === 'string'
return; ? p === pluginName
: p.plugin === pluginName && !p.include
)
) {
// Plugin has already been added
return;
}
global.NX_GRAPH_CREATION = true;
try {
projConfigs = await retrieveProjectConfigurations(
[pluginFactory(pluginOptions)],
tree.root,
nxJson
);
} catch (e) {
// Errors are okay for this because we're only running 1 plugin
if (e instanceof ProjectConfigurationsError) {
projConfigs = e.partialProjectConfigurationsResult;
} else {
throw e;
}
}
global.NX_GRAPH_CREATION = false;
for (const projConfig of Object.values(projConfigs.projects)) {
const node = graphNodes.find(
(node) => node.data.root === projConfig.root
);
if (!node) {
continue;
}
for (const targetName in projConfig.targets) {
if (node.data.targets[targetName]) {
// Conflicting Target Name, check the next one
pluginOptions = null;
continue optionsLoop;
}
}
}
break;
} }
} else {
// If the plugin does not take in options, we add the plugin with empty options.
nxJson.plugins ??= [];
pluginOptions = {} as unknown as PluginOptions;
global.NX_GRAPH_CREATION = true; global.NX_GRAPH_CREATION = true;
try { try {
projConfigs = await retrieveProjectConfigurations( projConfigs = await retrieveProjectConfigurations(
@ -135,26 +178,6 @@ async function _addPluginInternal<PluginOptions>(
} }
} }
global.NX_GRAPH_CREATION = false; global.NX_GRAPH_CREATION = false;
for (const projConfig of Object.values(projConfigs.projects)) {
const node = graphNodes.find(
(node) => node.data.root === projConfig.root
);
if (!node) {
continue;
}
for (const targetName in projConfig.targets) {
if (node.data.targets[targetName]) {
// Conflicting Target Name, check the next one
pluginOptions = null;
continue optionsLoop;
}
}
}
break;
} }
if (!pluginOptions) { if (!pluginOptions) {

View File

@ -1,12 +1,16 @@
import { join } from 'path'; import { join } from 'path';
import { CreateNodesContext, hashArray } from 'nx/src/devkit-exports'; import {
CreateNodesContext,
CreateNodesContextV2,
hashArray,
} from 'nx/src/devkit-exports';
import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals'; import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals';
export async function calculateHashForCreateNodes( export async function calculateHashForCreateNodes(
projectRoot: string, projectRoot: string,
options: object, options: object,
context: CreateNodesContext, context: CreateNodesContext | CreateNodesContextV2,
additionalGlobs: string[] = [] additionalGlobs: string[] = []
): Promise<string> { ): Promise<string> {
return hashArray([ return hashArray([

View File

@ -6,6 +6,7 @@ import type { InputDefinition } from 'nx/src/config/workspace-json-project-json'
import { import {
CreateNodesContext, CreateNodesContext,
CreateNodesContextV2,
ProjectConfiguration, ProjectConfiguration,
readJsonFile, readJsonFile,
} from 'nx/src/devkit-exports'; } from 'nx/src/devkit-exports';
@ -15,7 +16,7 @@ import {
*/ */
export function getNamedInputs( export function getNamedInputs(
directory: string, directory: string,
context: CreateNodesContext context: CreateNodesContext | CreateNodesContextV2
): { [inputName: string]: (string | InputDefinition)[] } { ): { [inputName: string]: (string | InputDefinition)[] } {
const projectJsonPath = join(directory, 'project.json'); const projectJsonPath = join(directory, 'project.json');
const projectJson: ProjectConfiguration = existsSync(projectJsonPath) const projectJson: ProjectConfiguration = existsSync(projectJsonPath)

View File

@ -59,7 +59,15 @@ export interface ImportOptions {
} }
export async function importHandler(options: ImportOptions) { export async function importHandler(options: ImportOptions) {
process.env.NX_RUNNING_NX_IMPORT = 'true';
let { sourceRepository, ref, source, destination } = options; let { sourceRepository, ref, source, destination } = options;
const destinationGitClient = new GitRepository(process.cwd());
if (await destinationGitClient.hasUncommittedChanges()) {
throw new Error(
`You have uncommitted changes in the destination repository. Commit or revert the changes and try again.`
);
}
output.log({ output.log({
title: title:
@ -186,7 +194,6 @@ export async function importHandler(options: ImportOptions) {
const absDestination = join(process.cwd(), destination); const absDestination = join(process.cwd(), destination);
const destinationGitClient = new GitRepository(process.cwd());
await assertDestinationEmpty(destinationGitClient, absDestination); await assertDestinationEmpty(destinationGitClient, absDestination);
const tempImportBranch = getTempImportBranch(ref); const tempImportBranch = getTempImportBranch(ref);
@ -259,7 +266,8 @@ export async function importHandler(options: ImportOptions) {
const { plugins, updatePackageScripts } = await detectPlugins( const { plugins, updatePackageScripts } = await detectPlugins(
nxJson, nxJson,
options.interactive options.interactive,
true
); );
if (packageManager !== sourcePackageManager) { if (packageManager !== sourcePackageManager) {

View File

@ -157,7 +157,7 @@ export async function initHandler(options: InitArgs): Promise<void> {
}); });
} }
const npmPackageToPluginMap: Record<string, string> = { const npmPackageToPluginMap: Record<string, `@nx/${string}`> = {
// Generic JS tools // Generic JS tools
eslint: '@nx/eslint', eslint: '@nx/eslint',
storybook: '@nx/storybook', storybook: '@nx/storybook',
@ -181,7 +181,8 @@ const npmPackageToPluginMap: Record<string, string> = {
export async function detectPlugins( export async function detectPlugins(
nxJson: NxJsonConfiguration, nxJson: NxJsonConfiguration,
interactive: boolean interactive: boolean,
includeAngularCli?: boolean
): Promise<{ ): Promise<{
plugins: string[]; plugins: string[];
updatePackageScripts: boolean; updatePackageScripts: boolean;
@ -214,7 +215,13 @@ export async function detectPlugins(
...packageJson.devDependencies, ...packageJson.devDependencies,
}; };
for (const [dep, plugin] of Object.entries(npmPackageToPluginMap)) { const _npmPackageToPluginMap = {
...npmPackageToPluginMap,
};
if (includeAngularCli) {
_npmPackageToPluginMap['@angular/cli'] = '@nx/angular';
}
for (const [dep, plugin] of Object.entries(_npmPackageToPluginMap)) {
if (deps[dep]) { if (deps[dep]) {
detectedPlugins.add(plugin); detectedPlugins.add(plugin);
} }

View File

@ -45,6 +45,11 @@ export class GitRepository {
.trim(); .trim();
} }
async hasUncommittedChanges() {
const data = await this.execAsync(`git status --porcelain`);
return data.trim() !== '';
}
async addFetchRemote(remoteName: string, branch: string) { async addFetchRemote(remoteName: string, branch: string) {
return await this.execAsync( return await this.execAsync(
`git config --add remote.${remoteName}.fetch "+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}"` `git config --add remote.${remoteName}.fetch "+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}"`