Skip to content

Commit

Permalink
feat(nx-container): add inferred targets
Browse files Browse the repository at this point in the history
  • Loading branch information
Badeau, Jose committed Nov 29, 2024
1 parent 150024b commit 6d53fa5
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 21 deletions.
52 changes: 31 additions & 21 deletions plugins/nx-container/src/generators/configuration/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
formatFiles,
generateFiles,
ProjectConfiguration,
readNxJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
Expand All @@ -21,30 +22,32 @@ function addFiles(tree: Tree, project: ProjectConfiguration, template) {
export async function configurationGenerator(tree: Tree, options: ConfigurationGeneratorSchema) {
const project = readProjectConfiguration(tree, options.project);

updateProjectConfiguration(tree, options.project, {
...project,
targets: {
...project.targets,
container: {
executor: `@nx-tools/nx-container:build`,
dependsOn: ['build'],
options: {
engine: options.engine ?? DEFAULT_ENGINE,
metadata: {
images: [project.name],
load: true,
tags: [
'type=schedule',
'type=ref,event=branch',
'type=ref,event=tag',
'type=ref,event=pr',
'type=sha,prefix=sha-',
],
if (!hasContainerPlugin(tree)) {
updateProjectConfiguration(tree, options.project, {
...project,
targets: {
...project.targets,
container: {
executor: `@nx-tools/nx-container:build`,
dependsOn: ['build'],
options: {
engine: options.engine ?? DEFAULT_ENGINE,
metadata: {
images: [project.name],
load: true,
tags: [
'type=schedule',
'type=ref,event=branch',
'type=ref,event=tag',
'type=ref,event=pr',
'type=sha,prefix=sha-',
],
},
},
},
},
},
});
});
}

addFiles(tree, project, options.template ?? DEFAULT_TEMPLATE);

Expand All @@ -53,4 +56,11 @@ export async function configurationGenerator(tree: Tree, options: ConfigurationG
}
}

export function hasContainerPlugin(tree: Tree): boolean {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string' ? p === '@nx-tools/nx-container' : p.plugin === '@nx-tools/nx-container'
);
}

export default configurationGenerator;
2 changes: 2 additions & 0 deletions plugins/nx-container/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { default as run } from './executors/build/executor';

export * from './plugins';
67 changes: 67 additions & 0 deletions plugins/nx-container/src/plugins/__snapshots__/nodes.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@nx/container/plugin non-root project with Dockerfile should create nodes for non-root project with Dockerfile 1`] = `
{
"projects": {
"apps/your-docker-app": {
"targets": {
"build": {
"dependsOn": [
"build",
],
"executor": "@nx-tools/nx-container:build",
"options": {
"engine": "docker",
"metadata": {
"images": [
"my-docker-app",
],
"load": true,
"tags": [
"type=schedule",
"type=ref,event=branch",
"type=ref,event=tag",
"type=ref,event=pr",
"type=sha,prefix=sha-",
],
},
},
},
},
},
},
}
`;

exports[`@nx/container/plugin root project should create nodes with correct targets 1`] = `
{
"projects": {
".": {
"targets": {
"build": {
"dependsOn": [
"build",
],
"executor": "@nx-tools/nx-container:build",
"options": {
"engine": "docker",
"metadata": {
"images": [
"my-docker-app",
],
"load": true,
"tags": [
"type=schedule",
"type=ref,event=branch",
"type=ref,event=tag",
"type=ref,event=pr",
"type=sha,prefix=sha-",
],
},
},
},
},
},
},
}
`;
1 change: 1 addition & 0 deletions plugins/nx-container/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './nodes';
110 changes: 110 additions & 0 deletions plugins/nx-container/src/plugins/nodes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { type CreateNodesContext, readNxJson, Tree } from '@nx/devkit';
import { createNodes } from './nodes';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { readdirSync, existsSync, readFileSync } from 'fs';

jest.mock('fs', () => ({
...jest.requireActual('fs'),
readdirSync: jest.fn(),
existsSync: jest.fn(),
readFileSync: jest.fn(),
}));

jest.mock('@nx/devkit/src/utils/calculate-hash-for-create-nodes', () => {
return {
calculateHashForCreateNodes: jest.fn().mockResolvedValue('mock-hash'),
};
});

describe('@nx/container/plugin', () => {
let tree: Tree;
const createNodesFunction = createNodes[1];
let context: CreateNodesContext;

beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
context = {
nxJsonConfiguration: readNxJson(tree),
workspaceRoot: '/',
configFiles: [],
};

(existsSync as jest.Mock).mockImplementation((path: string) => {
return tree.exists(path);
});

(readdirSync as jest.Mock).mockImplementation((path: string) => {
return tree.children(path.replace(/\*/g, ''));
});

(readFileSync as jest.Mock).mockImplementation((path: string) => {
return tree.read(path);
});
});

describe('root project', () => {
beforeEach(() => {
tree.write('Dockerfile', '');
tree.write('package.json', JSON.stringify({ name: 'my-docker-app' }));
tree.write('project.json', JSON.stringify({ name: 'my-docker-app' }));
});

it('should create nodes with correct targets', async () => {
const nodes = await createNodesFunction(
'Dockerfile',
{
buildTargetName: 'build',
defaultEngine: 'docker',
},
context
);

expect(nodes).toMatchSnapshot();
expect(calculateHashForCreateNodes).toHaveBeenCalled();
});
});

describe('non-root project with Dockerfile', () => {
beforeEach(() => {
tree.write('apps/your-docker-app/Dockerfile', '');
tree.write('apps/your-docker-app/package.json', JSON.stringify({ name: 'my-docker-app' }));
tree.write('apps/your-docker-app/project.json', JSON.stringify({ name: 'my-docker-app' }));
});

it('should create nodes for non-root project with Dockerfile', async () => {
const nodes = await createNodesFunction(
'apps/your-docker-app/Dockerfile',
{
buildTargetName: 'build',
defaultEngine: 'docker',
},
context
);

expect(nodes).toMatchSnapshot();
expect(calculateHashForCreateNodes).toHaveBeenCalled();
});
});

describe('non-root project without project.json', () => {
beforeEach(() => {
tree.write('apps/no-project/Dockerfile', '');
tree.write('apps/no-project/package.json', JSON.stringify({ name: 'no-project-app' }));
});

it('should not create nodes if project.json is missing', async () => {
const nodes = await createNodesFunction(
'apps/no-project/Dockerfile',
{
buildTargetName: 'build',
defaultEngine: 'docker',
},
context
);

expect(nodes).toEqual({});
expect(calculateHashForCreateNodes).not.toHaveBeenCalled();
});
});
});
112 changes: 112 additions & 0 deletions plugins/nx-container/src/plugins/nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
CreateDependencies,
CreateNodes,
detectPackageManager,
parseJson,
readJsonFile,
TargetConfiguration,
writeJsonFile,
} from '@nx/devkit';
import { dirname, join } from 'path';
import { getLockFileName } from '@nx/js';
import { existsSync, readdirSync, readFileSync } from 'fs';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { DEFAULT_ENGINE } from '../generators/configuration/constants';

export interface ContainerPluginOptions {
buildTargetName?: string;
defaultEngine?: string;
}

const cachePath = join(workspaceDataDirectory, 'container.hash');
const targetsCache = readTargetsCache();

function readTargetsCache(): Record<string, Record<string, TargetConfiguration<ContainerPluginOptions>>> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}

function writeTargetsToCache() {
writeJsonFile(cachePath, targetsCache);
}

export const createDependencies: CreateDependencies = () => {
writeTargetsToCache();
return [];
};

export const createNodes: CreateNodes<ContainerPluginOptions> = [
'**/Dockerfile',
async (configFilePath, options, context) => {
options = normalizeOptions(options);
const projectRoot = dirname(configFilePath);

// Do not create a project if project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (!siblingFiles.includes('project.json')) {
return {};
}

const hash = await calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);

const projectName = buildProjectName(projectRoot, context.workspaceRoot);

targetsCache[hash] ??= buildTargets(projectRoot, options, projectName);

return {
projects: {
[projectRoot]: {
targets: targetsCache[hash],
},
},
};
},
];

function buildTargets(projectRoot: string, options: ContainerPluginOptions, projectName: string) {
const targets: Record<string, TargetConfiguration> = {
[options.buildTargetName]: {
executor: '@nx-tools/nx-container:build',
dependsOn: ['build'],
options: {
engine: options.defaultEngine,
metadata: {
images: [projectName],
load: true,
tags: [
'type=schedule',
'type=ref,event=branch',
'type=ref,event=tag',
'type=ref,event=pr',
'type=sha,prefix=sha-',
],
},
},
},
};

return targets;
}

function buildProjectName(projectRoot: string, workspaceRoot: string): string | undefined {
const packageJsonPath = join(workspaceRoot, projectRoot, 'package.json');
const projectJsonPath = join(workspaceRoot, projectRoot, 'project.json');
let name: string;
if (existsSync(projectJsonPath)) {
const projectJson = parseJson(readFileSync(projectJsonPath, 'utf-8'));
name = projectJson.name;
} else if (existsSync(packageJsonPath)) {
const packageJson = parseJson(readFileSync(packageJsonPath, 'utf-8'));
name = packageJson.name;
}
return name;
}

function normalizeOptions(options: ContainerPluginOptions): ContainerPluginOptions {
options ??= {};
options.buildTargetName ??= 'container';
options.defaultEngine ??= DEFAULT_ENGINE;
return options;
}

0 comments on commit 6d53fa5

Please sign in to comment.