diff --git a/packages/nx-heroku/src/index.ts b/packages/nx-heroku/src/index.ts index e69de29..df16006 100644 --- a/packages/nx-heroku/src/index.ts +++ b/packages/nx-heroku/src/index.ts @@ -0,0 +1 @@ +export * from './plugins/plugin'; diff --git a/packages/nx-heroku/src/plugins/plugin.ts b/packages/nx-heroku/src/plugins/plugin.ts new file mode 100644 index 0000000..1b906f5 --- /dev/null +++ b/packages/nx-heroku/src/plugins/plugin.ts @@ -0,0 +1,153 @@ +// inspired by https://github.com/nrwl/nx/blob/master/packages/eslint/src/plugins/plugin.ts +import { + CreateDependencies, + CreateNodes, + CreateNodesContext, + joinPathFragments, + NxJsonConfiguration, + readJsonFile, + 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 { minimatch } from 'minimatch'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/src/plugins/package-json-workspaces'; +import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; +import { combineGlobPatterns } from 'nx/src/utils/globs'; + +export interface HerokuPluginOptions { + buildTarget: string; + deployTargetName: string; + promoteTargetName: string; +} + +const cachePath = join(projectGraphCacheDirectory, 'heroku.hash'); +const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; + +const calculatedTargets: Record< + string, + Record +> = {}; + +function readTargetsCache(): Record< + string, + Record +> { + return readJsonFile(cachePath); +} + +function writeTargetsToCache( + targets: Record> +) { + writeJsonFile(cachePath, targets); +} + +export const createDependencies: CreateDependencies = () => { + writeTargetsToCache(calculatedTargets); + return []; +}; + +export const createNodes: CreateNodes> = [ + combineGlobPatterns(['**/Procfile']), + (configFilePath, options, context) => { + const projectRoot = dirname(configFilePath); + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } else if ( + !siblingFiles.includes('project.json') && + siblingFiles.includes('package.json') + ) { + const packageManagerWorkspacesGlob = combineGlobPatterns( + getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot) + ); + const path = joinPathFragments(projectRoot, 'package.json'); + const isPackageJsonProject = minimatch( + path, + packageManagerWorkspacesGlob + ); + if (!isPackageJsonProject) { + return {}; + } + } + + const opts = normalizeOptions(options ?? {}); + const hash = calculateHashForCreateNodes(projectRoot, opts, context); + const targets = + targetsCache[hash] ?? + buildHerokuTargets(configFilePath, projectRoot, opts, context); + + calculatedTargets[hash] = targets; + return { + projects: { + [projectRoot]: { + root: projectRoot, + targets, + }, + }, + }; + }, +]; + +function getInputs( + namedInputs: NxJsonConfiguration['namedInputs'] +): TargetConfiguration['inputs'] { + return [ + ...(namedInputs && 'production' in namedInputs + ? ['default', '^production'] + : ['default', '^default']), + ]; +} + +function buildHerokuTargets( + configFilePath: string, + projectRoot: string, + options: HerokuPluginOptions, + context: CreateNodesContext +) { + const namedInputs = getNamedInputs(projectRoot, context); + + const targets: Record = {}; + const baseConfig: TargetConfiguration = { + cache: false, + inputs: getInputs(namedInputs), + }; + const procfile = readFileSync( + join(context.workspaceRoot, configFilePath), + 'utf-8' + ).trim(); + + const deployConfig: TargetConfiguration = { + ...baseConfig, + executor: '@aloes/nx-heroku:deploy', + options: { + procfile, + }, + }; + const promoteConfig: TargetConfiguration = { + ...baseConfig, + executor: '@aloes/nx-heroku:promote', + options: {}, + }; + + targets[options.deployTargetName] = deployConfig; + targets[options.promoteTargetName] = promoteConfig; + return targets; +} + +function normalizeOptions( + options: Partial +): HerokuPluginOptions { + return { + deployTargetName: options.deployTargetName ?? 'deploy', + promoteTargetName: options.promoteTargetName ?? 'promote', + buildTarget: options.buildTarget ?? 'build', + }; +}