-
-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nx-container): add inferred targets
- Loading branch information
Badeau, Jose
committed
Nov 29, 2024
1 parent
150024b
commit 6d53fa5
Showing
6 changed files
with
323 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
67
plugins/nx-container/src/plugins/__snapshots__/nodes.spec.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-", | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './nodes'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |