feat(core): use inputs to determine package dependencies (#13966)

This commit is contained in:
Denis Frenademetz 2023-03-14 16:02:00 +01:00 committed by GitHub
parent 9acd7757ae
commit ebdb193f0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 701 additions and 85 deletions

View File

@ -1071,12 +1071,14 @@ Convert an Nx Generator into an Angular Devkit Schematic.
#### Parameters
| Name | Type |
| :---------------------- | :---------------------------------------------------------------- |
| :---------------------------- | :---------------------------------------------------------------- |
| `projectName` | `string` |
| `graph` | [`ProjectGraph`](../../devkit/documents/nrwl_devkit#projectgraph) |
| `options` | `Object` |
| `options.helperDependencies?` | `string`[] |
| `options.isProduction?` | `boolean` |
| `options.root?` | `string` |
| `options.target?` | `string` |
#### Returns

View File

@ -1071,12 +1071,14 @@ Convert an Nx Generator into an Angular Devkit Schematic.
#### Parameters
| Name | Type |
| :---------------------- | :---------------------------------------------------------------- |
| :---------------------------- | :---------------------------------------------------------------- |
| `projectName` | `string` |
| `graph` | [`ProjectGraph`](../../devkit/documents/nrwl_devkit#projectgraph) |
| `options` | `Object` |
| `options.helperDependencies?` | `string`[] |
| `options.isProduction?` | `boolean` |
| `options.root?` | `string` |
| `options.target?` | `string` |
#### Returns

View File

@ -6,7 +6,7 @@ import {
UpdatePackageJsonOption,
} from './update-package-json';
import { vol } from 'memfs';
import { ExecutorContext, ProjectGraph } from '@nrwl/devkit';
import { DependencyType, ExecutorContext, ProjectGraph } from '@nrwl/devkit';
import { DependentBuildableProjectNode } from '../buildable-libs-utils';
jest.mock('nx/src/utils/workspace-root', () => ({
@ -298,7 +298,24 @@ describe('updatePackageJson', () => {
outputs: ['{workspaceRoot}/dist/libs/lib1'],
},
},
files: [],
files: [
{
file: 'test.ts',
hash: '',
dependencies: [
{
type: DependencyType.static,
target: 'npm:external1',
source: '@org/lib1',
},
{
type: DependencyType.static,
target: 'npm:external2',
source: '@org/lib1',
},
],
},
],
},
},
},
@ -330,8 +347,16 @@ describe('updatePackageJson', () => {
},
dependencies: {
'@org/lib1': [
{ source: '@org/lib1', target: 'npm:external1', type: 'static' },
{ source: '@org/lib1', target: 'npm:external2', type: 'static' },
{
source: '@org/lib1',
target: 'npm:external1',
type: DependencyType.static,
},
{
source: '@org/lib1',
target: 'npm:external2',
type: DependencyType.static,
},
],
},
};

View File

@ -54,6 +54,7 @@ export function updatePackageJson(
if (options.updateBuildableProjectDepsInPackageJson) {
packageJson = createPackageJson(context.projectName, context.projectGraph, {
target: context.targetName,
root: context.root,
// By default we remove devDependencies since this is a production build.
isProduction: true,

View File

@ -84,6 +84,7 @@ export default async function buildExecutor(
context.projectName,
context.projectGraph,
{
target: context.targetName,
root: context.root,
isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build.
}

View File

@ -186,7 +186,7 @@ export class Hasher {
}
}
const DEFAULT_INPUTS = [
const DEFAULT_INPUTS: ReadonlyArray<InputDefinition> = [
{
projects: 'self',
fileset: '{projectRoot}/**/*',
@ -221,11 +221,7 @@ class TaskHasher {
if (!projectNode) {
return this.hashExternalDependency(task.target.project);
}
const namedInputs = {
default: [{ fileset: '{projectRoot}/**/*' }],
...this.nxJson.namedInputs,
...projectNode.data.namedInputs,
};
const namedInputs = getNamedInputs(this.nxJson, projectNode);
const targetData = projectNode.data.targets[task.target.target];
const targetDefaults = (this.nxJson.targetDefaults || {})[
task.target.target
@ -399,9 +395,7 @@ class TaskHasher {
projectName: string,
inputs: ExpandedSelfInput[]
): Promise<PartialHash[]> {
const filesets = inputs
.filter((r) => !!r['fileset'])
.map((r) => r['fileset']);
const filesets = extractPatternsFromFileSets(inputs);
const projectFilesets = [];
const workspaceFilesets = [];
@ -563,9 +557,52 @@ class TaskHasher {
}
}
export function getNamedInputs(
nxJson: NxJsonConfiguration,
project: ProjectGraphProjectNode
) {
return {
default: [{ fileset: '{projectRoot}/**/*' }],
...nxJson.namedInputs,
...project.data.namedInputs,
};
}
export function getTargetInputs(
nxJson: NxJsonConfiguration,
projectNode: ProjectGraphProjectNode,
target: string
) {
const namedInputs = getNamedInputs(nxJson, projectNode);
const targetData = projectNode.data.targets[target];
const targetDefaults = (nxJson.targetDefaults || {})[target];
const inputs = splitInputsIntoSelfAndDependencies(
targetData.inputs || targetDefaults?.inputs || DEFAULT_INPUTS,
namedInputs
);
const selfInputs = extractPatternsFromFileSets(inputs.selfInputs);
const dependencyInputs = extractPatternsFromFileSets(
inputs.depsInputs.map((s) => expandNamedInput(s.input, namedInputs)).flat()
);
return { selfInputs, dependencyInputs };
}
export function extractPatternsFromFileSets(
inputs: readonly ExpandedSelfInput[]
): string[] {
return inputs
.filter((c): c is { fileset: string } => !!c['fileset'])
.map((c) => c['fileset']);
}
export function splitInputsIntoSelfAndDependencies(
inputs: (InputDefinition | string)[],
namedInputs: { [inputName: string]: (InputDefinition | string)[] }
inputs: ReadonlyArray<InputDefinition | string>,
namedInputs: { [inputName: string]: ReadonlyArray<InputDefinition | string> }
): {
depsInputs: { input: string }[];
selfInputs: ExpandedSelfInput[];
@ -591,8 +628,8 @@ export function splitInputsIntoSelfAndDependencies(
}
function expandSelfInputs(
inputs: (InputDefinition | string)[],
namedInputs: { [inputName: string]: (InputDefinition | string)[] }
inputs: ReadonlyArray<InputDefinition | string>,
namedInputs: { [inputName: string]: ReadonlyArray<InputDefinition | string> }
): ExpandedSelfInput[] {
const expanded = [];
for (const d of inputs) {
@ -623,7 +660,7 @@ function expandSelfInputs(
export function expandNamedInput(
input: string,
namedInputs: { [inputName: string]: (InputDefinition | string)[] }
namedInputs: { [inputName: string]: ReadonlyArray<InputDefinition | string> }
): ExpandedSelfInput[] {
namedInputs ||= {};
if (!namedInputs[input]) throw new Error(`Input '${input}' is not defined`);

View File

@ -0,0 +1,502 @@
import * as fs from 'fs';
import * as configModule from '../config/configuration';
import { DependencyType } from '../config/project-graph';
import * as hashModule from '../hasher/hasher';
import { createPackageJson } from './create-package-json';
import * as fileutilsModule from './fileutils';
describe('createPackageJson', () => {
it('should add additional dependencies', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
jest.spyOn(fileutilsModule, 'readJsonFile').mockReturnValue({
dependencies: {
typescript: '4.8.4',
tslib: '2.4.0',
},
});
expect(
createPackageJson(
'lib1',
{
nodes: {
lib1: {
type: 'lib',
name: 'lib1',
data: { files: [], targets: {}, root: '' },
},
},
externalNodes: {
'npm:tslib': {
type: 'npm',
name: 'npm:tslib',
data: { version: '2.4.0', hash: '', packageName: 'tslib' },
},
},
dependencies: {},
},
{ helperDependencies: ['npm:tslib'] }
)
).toEqual({
dependencies: {
tslib: '2.4.0',
},
name: 'lib1',
version: '0.0.1',
});
});
it('should only add file dependencies if target is specified', () => {
jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
targetDefaults: {
build: {
inputs: ['default', 'production', '^production'],
},
},
});
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
jest.spyOn(fileutilsModule, 'readJsonFile').mockReturnValue({
dependencies: {
axios: '1.0.0',
tslib: '2.4.0',
jest: '29.0.0',
typescript: '4.8.4',
},
});
expect(
createPackageJson(
'lib1',
{
nodes: {
lib1: {
type: 'lib',
name: 'lib1',
data: {
root: 'libs/lib1',
targets: {
build: {},
},
files: [
{
file: 'libs/lib1/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:typescript',
source: 'lib1',
},
],
hash: '',
},
{
file: 'libs/lib1/src/main2.ts',
dependencies: [
{
type: DependencyType.static,
target: 'lib2',
source: 'lib1',
},
],
hash: '',
},
{
file: 'libs/lib1/src/main.spec.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:jest',
source: 'lib1',
},
],
hash: '',
},
],
},
},
lib2: {
type: 'lib',
name: 'lib2',
data: {
root: 'libs/lib2',
targets: {
build: {},
},
files: [
{
file: 'libs/lib2/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:axios',
source: 'lib2',
},
],
hash: '',
},
{
file: 'libs/lib2/src/main.spec.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:jest',
source: 'lib2',
},
],
hash: '',
},
],
},
},
},
externalNodes: {
'npm:tslib': {
type: 'npm',
name: 'npm:tslib',
data: { version: '2.4.0', hash: '', packageName: 'tslib' },
},
'npm:typescript': {
type: 'npm',
name: 'npm:typescript',
data: { version: '4.8.4', hash: '', packageName: 'typescript' },
},
'npm:jest': {
type: 'npm',
name: 'npm:jest',
data: { version: '29.0.0', hash: '', packageName: 'jest' },
},
'npm:axios': {
type: 'npm',
name: 'npm:jest',
data: { version: '1.0.0', hash: '', packageName: 'axios' },
},
},
dependencies: {},
},
{
target: 'build',
isProduction: true,
helperDependencies: ['npm:tslib'],
}
)
).toEqual({
dependencies: {
axios: '1.0.0',
tslib: '2.4.0',
typescript: '4.8.4',
},
name: 'lib1',
version: '0.0.1',
});
});
it('should only add all dependencies if target is not specified', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
jest.spyOn(fileutilsModule, 'readJsonFile').mockReturnValue({
dependencies: {
axios: '1.0.0',
tslib: '2.4.0',
jest: '29.0.0',
typescript: '4.8.4',
},
});
expect(
createPackageJson(
'lib1',
{
nodes: {
lib1: {
type: 'lib',
name: 'lib1',
data: {
root: 'libs/lib1',
targets: {
build: {},
},
files: [
{
file: 'libs/lib1/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:typescript',
source: 'lib1',
},
],
hash: '',
},
{
file: 'libs/lib1/src/main2.ts',
dependencies: [
{
type: DependencyType.static,
target: 'lib2',
source: 'lib1',
},
],
hash: '',
},
{
file: 'libs/lib1/src/main.spec.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:jest',
source: 'lib1',
},
],
hash: '',
},
],
},
},
lib2: {
type: 'lib',
name: 'lib2',
data: {
root: 'libs/lib2',
targets: {
build: {},
},
files: [
{
file: 'libs/lib2/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:axios',
source: 'lib2',
},
],
hash: '',
},
{
file: 'libs/lib2/src/main.spec.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:jest',
source: 'lib2',
},
],
hash: '',
},
],
},
},
},
externalNodes: {
'npm:tslib': {
type: 'npm',
name: 'npm:tslib',
data: { version: '2.4.0', hash: '', packageName: 'tslib' },
},
'npm:typescript': {
type: 'npm',
name: 'npm:typescript',
data: { version: '4.8.4', hash: '', packageName: 'typescript' },
},
'npm:jest': {
type: 'npm',
name: 'npm:jest',
data: { version: '29.0.0', hash: '', packageName: 'jest' },
},
'npm:axios': {
type: 'npm',
name: 'npm:axios',
data: { version: '1.0.0', hash: '', packageName: 'axios' },
},
},
dependencies: {},
},
{ isProduction: true, helperDependencies: ['npm:tslib'] }
)
).toEqual({
dependencies: {
axios: '1.0.0',
jest: '29.0.0',
tslib: '2.4.0',
typescript: '4.8.4',
},
name: 'lib1',
version: '0.0.1',
});
});
it('should cache filterUsingGlobPatterns', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
jest.spyOn(fileutilsModule, 'readJsonFile').mockReturnValue({
dependencies: {
axios: '1.0.0',
tslib: '2.4.0',
jest: '29.0.0',
typescript: '4.8.4',
},
});
const filterUsingGlobPatternsSpy = jest.spyOn(
hashModule,
'filterUsingGlobPatterns'
);
expect(
createPackageJson(
'lib1',
{
nodes: {
lib1: {
type: 'lib',
name: 'lib1',
data: {
root: 'libs/lib1',
targets: {
build: {},
},
files: [
{
file: 'libs/lib1/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'lib3',
source: 'lib1',
},
],
hash: '',
},
{
file: 'libs/lib1/src/main2.ts',
dependencies: [
{
type: DependencyType.static,
target: 'lib2',
source: 'lib1',
},
],
hash: '',
},
],
},
},
lib2: {
type: 'lib',
name: 'lib2',
data: {
root: 'libs/lib2',
targets: {
build: {},
},
files: [
{
file: 'libs/lib2/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'lib4',
source: 'lib2',
},
],
hash: '',
},
],
},
},
lib3: {
type: 'lib',
name: 'lib3',
data: {
root: 'libs/lib3',
targets: {
build: {},
},
files: [
{
file: 'libs/lib3/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'lib4',
source: 'lib3',
},
],
hash: '',
},
],
},
},
lib4: {
type: 'lib',
name: 'lib4',
data: {
root: 'libs/lib4',
targets: {
build: {},
},
files: [
{
file: 'libs/lib2/src/main.ts',
dependencies: [
{
type: DependencyType.static,
target: 'npm:axios',
source: 'lib2',
},
],
hash: '',
},
],
},
},
},
externalNodes: {
'npm:axios': {
type: 'npm',
name: 'npm:axios',
data: { version: '1.0.0', hash: '', packageName: 'axios' },
},
},
dependencies: {},
},
{ isProduction: true }
)
).toEqual({
dependencies: {
axios: '1.0.0',
},
name: 'lib1',
version: '0.0.1',
});
expect(filterUsingGlobPatternsSpy).toHaveBeenNthCalledWith(
1,
'libs/lib1',
expect.anything(),
expect.anything()
);
expect(filterUsingGlobPatternsSpy).toHaveBeenNthCalledWith(
2,
'libs/lib3',
expect.anything(),
expect.anything()
);
expect(filterUsingGlobPatternsSpy).toHaveBeenNthCalledWith(
3,
'libs/lib4',
expect.anything(),
expect.anything()
);
expect(filterUsingGlobPatternsSpy).toHaveBeenNthCalledWith(
4,
'libs/lib2',
expect.anything(),
expect.anything()
);
expect(filterUsingGlobPatternsSpy).toHaveBeenCalledTimes(4);
});
});

View File

@ -1,9 +1,17 @@
import { readJsonFile } from './fileutils';
import { sortObjectByKeys } from './object-sort';
import { ProjectGraph } from '../config/project-graph';
import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
import { PackageJson } from './package-json';
import { existsSync } from 'fs';
import { workspaceRoot } from './workspace-root';
import { filterUsingGlobPatterns, getTargetInputs } from '../hasher/hasher';
import { readNxJson } from '../config/configuration';
interface NpmDeps {
readonly dependencies: Record<string, string>;
readonly peerDependencies: Record<string, string>;
readonly peerDependenciesMeta: Record<string, { optional: boolean }>;
}
/**
* Creates a package.json in the output directory for support to install dependencies within containers.
@ -15,11 +23,42 @@ export function createPackageJson(
projectName: string,
graph: ProjectGraph,
options: {
target?: string;
root?: string;
isProduction?: boolean;
helperDependencies?: string[];
} = {}
): PackageJson {
const npmDeps = findAllNpmDeps(projectName, graph);
const projectNode = graph.nodes[projectName];
const { selfInputs, dependencyInputs } = options.target
? getTargetInputs(readNxJson(), projectNode, options.target)
: { selfInputs: [], dependencyInputs: [] };
const npmDeps: NpmDeps = {
dependencies: {},
peerDependencies: {},
peerDependenciesMeta: {},
};
const seen = new Set<string>();
options.helperDependencies?.forEach((dep) => {
seen.add(dep);
npmDeps.dependencies[graph.externalNodes[dep].data.packageName] =
graph.externalNodes[dep].data.version;
recursivelyCollectPeerDependencies(dep, graph, npmDeps, seen);
});
findAllNpmDeps(
projectNode,
graph,
npmDeps,
seen,
dependencyInputs,
selfInputs
);
// default package.json if one does not exist
let packageJson: PackageJson = {
name: projectName,
@ -104,64 +143,73 @@ export function createPackageJson(
}
function findAllNpmDeps(
projectName: string,
projectNode: ProjectGraphProjectNode,
graph: ProjectGraph,
list: {
dependencies: Record<string, string>;
peerDependencies: Record<string, string>;
peerDependenciesMeta: Record<string, { optional: boolean }>;
} = { dependencies: {}, peerDependencies: {}, peerDependenciesMeta: {} },
seen = new Set<string>()
) {
const node = graph.externalNodes[projectName];
npmDeps: NpmDeps,
seen: Set<string>,
dependencyPatterns: string[],
rootPatterns?: string[]
): void {
if (seen.has(projectNode.name)) return;
if (seen.has(projectName)) {
seen.add(projectNode.name);
const projectFiles = filterUsingGlobPatterns(
projectNode.data.root,
projectNode.data.files,
rootPatterns ?? dependencyPatterns
);
const projectDependencies = new Set<string>();
projectFiles.forEach((fileData) =>
fileData.dependencies?.forEach((dep) => projectDependencies.add(dep.target))
);
for (const dep of projectDependencies) {
const node = graph.externalNodes[dep];
if (seen.has(dep)) {
// if it's in peerDependencies, move it to regular dependencies
// since this is a direct dependency of the project
if (node && list.peerDependencies[node.data.packageName]) {
list.dependencies[node.data.packageName] = node.data.version;
delete list.peerDependencies[node.data.packageName];
if (node && npmDeps.peerDependencies[node.data.packageName]) {
npmDeps.dependencies[node.data.packageName] = node.data.version;
delete npmDeps.peerDependencies[node.data.packageName];
}
return list;
}
seen.add(projectName);
if (node) {
list.dependencies[node.data.packageName] = node.data.version;
recursivelyCollectPeerDependencies(node.name, graph, list, seen);
} else {
// we are not interested in the dependencies of external projects
graph.dependencies[projectName]?.forEach((dep) => {
if (dep.type === 'static' || dep.type === 'dynamic') {
findAllNpmDeps(dep.target, graph, list, seen);
if (node) {
seen.add(dep);
npmDeps.dependencies[node.data.packageName] = node.data.version;
recursivelyCollectPeerDependencies(node.name, graph, npmDeps, seen);
} else {
findAllNpmDeps(
graph.nodes[dep],
graph,
npmDeps,
seen,
dependencyPatterns
);
}
}
});
}
return list;
}
function recursivelyCollectPeerDependencies(
projectName: string,
graph: ProjectGraph,
list: {
dependencies: Record<string, string>;
peerDependencies: Record<string, string>;
peerDependenciesMeta: Record<string, { optional: boolean }>;
},
seen = new Set<string>()
npmDeps: NpmDeps,
seen: Set<string>
) {
const npmPackage = graph.externalNodes[projectName];
if (!npmPackage) {
return list;
return npmDeps;
}
const packageName = npmPackage.data.packageName;
try {
const packageJson = require(`${packageName}/package.json`);
if (!packageJson.peerDependencies) {
return list;
return npmDeps;
}
Object.keys(packageJson.peerDependencies)
@ -171,21 +219,21 @@ function recursivelyCollectPeerDependencies(
.forEach((node) => {
if (!seen.has(node.name)) {
seen.add(node.name);
list.peerDependencies[node.data.packageName] = node.data.version;
npmDeps.peerDependencies[node.data.packageName] = node.data.version;
if (
packageJson.peerDependenciesMeta &&
packageJson.peerDependenciesMeta[node.data.packageName] &&
packageJson.peerDependenciesMeta[node.data.packageName].optional
) {
list.peerDependenciesMeta[node.data.packageName] = {
npmDeps.peerDependenciesMeta[node.data.packageName] = {
optional: true,
};
}
recursivelyCollectPeerDependencies(node.name, graph, list, seen);
recursivelyCollectPeerDependencies(node.name, graph, npmDeps, seen);
}
});
return list;
return npmDeps;
} catch (e) {
return list;
return npmDeps;
}
}

View File

@ -54,17 +54,15 @@ export class GeneratePackageJsonPlugin implements WebpackPluginInstance {
});
}
if (helperDependencies.length > 0) {
this.projectGraph.dependencies[this.context.projectName] =
this.projectGraph.dependencies[this.context.projectName].concat(
helperDependencies
);
}
const packageJson = createPackageJson(
this.context.projectName,
this.projectGraph,
{ root: this.context.root, isProduction: true }
{
target: this.context.targetName,
root: this.context.root,
isProduction: true,
helperDependencies: helperDependencies.map((dep) => dep.target),
}
);
packageJson.main = packageJson.main ?? this.options.outputFileName;