Skip to content

Commit

Permalink
feat(plugin): add plugin utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
jogelin committed Dec 12, 2024
1 parent 2786b16 commit acf3883
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 5 deletions.
2 changes: 2 additions & 0 deletions packages/plugin/generators.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
17 changes: 14 additions & 3 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
{
"name": "@huge-nx/plugin",
"version": "0.0.1",
"version": "0.0.0",
"private": false,
"repository": {
"type": "git",
"url": "https://github.com/jogelin/huge-nx"
},
"dependencies": {
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"minimatch": "^9.0.4",
"@nx/devkit": "20.2.2",
"nx": "20.2.2"
},
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"private": true
"generators": "./generators.json",
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './utils/index';
27 changes: 27 additions & 0 deletions packages/plugin/src/utils/cache-config.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ProjectConfiguration, readJsonFile, writeJsonFile } from '@nx/devkit';
import { existsSync } from 'node:fs';
import { CreateNodesContext, CreateNodesContextV2, hashArray } from 'nx/src/devkit-exports';

import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals';
import { join } from 'path';

export type ConfigCache = Record<string, Partial<ProjectConfiguration>>;

export function readConfigCache(cachePath: string): ConfigCache {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}

export function writeConfigToCache(cachePath: string, results: ConfigCache) {
writeJsonFile(cachePath, results);
}

export async function calculateHashForCreateNodes(
projectRoot: string,
options: object,
context: CreateNodesContext | CreateNodesContextV2,
rootGlob = '**/*',
additionalGlobs: string[] = [],
exclude: string[] = []
): Promise<string> {
return hashArray([await hashWithWorkspaceContext(context.workspaceRoot, [join(projectRoot, rootGlob), ...additionalGlobs], exclude), hashObject(options)]);
}
41 changes: 41 additions & 0 deletions packages/plugin/src/utils/combine-create-nodes.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createNodesFromFiles, CreateNodesV2 } from '@nx/devkit';
import { minimatch } from 'minimatch';
import { join } from 'node:path';
import { hashObject } from 'nx/src/hasher/file-hasher';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import { readConfigCache, writeConfigToCache } from './cache-config.utils';
import { CreateNodesInternal } from './create-nodes-internal-builder.utils';

export function combineCreateNodes<T extends Record<string, unknown>>(pluginName: string, createNodesInternals: CreateNodesInternal<T>[]): CreateNodesV2<T> {
const projectFilePatterns = createNodesInternals.map(([globPattern]) => globPattern);

return [
combineGlobPatterns(projectFilePatterns),
async (files, opt, context) => {
const options = opt as T;
const optionsHash = hashObject(options);
const cachePath = join(workspaceDataDirectory, `${pluginName}-${optionsHash}.hash`);
const configCache = readConfigCache(cachePath);
try {
return await createNodesFromFiles(
(filePath, nestedOpt, context) => {
const options = nestedOpt as T;

const createNodesInternal = createNodesInternals.find(([globPattern]) => minimatch(filePath, globPattern, { dot: true }));

if (!createNodesInternal) throw new Error(`No createNodesInternal found for ${filePath}`);

const nestedCreateNodesInternal = createNodesInternal[1];
return nestedCreateNodesInternal(filePath, options, { ...context, pluginName }, configCache);
},
files,
options,
context
);
} finally {
writeConfigToCache(cachePath, configCache);
}
},
];
}
92 changes: 92 additions & 0 deletions packages/plugin/src/utils/create-nodes-internal-builder.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { dirname } from 'node:path';
import { CreateNodesContext, CreateNodesContextV2, CreateNodesResult } from 'nx/src/project-graph/plugins/public-api';
import { calculateHashForCreateNodes, ConfigCache } from './cache-config.utils';
import { isAttachedToProject } from './is-attached-to-project.util';
import { ProjectConfiguration } from '@nx/devkit';

export type GenerateConfig<T extends Record<string, unknown>> = (
projectRoot: string,
filePath: string,
options: T,
context: CreateNodesContextV2
) => Partial<ProjectConfiguration>;
export type WithProjectRoot<T> = (filePath: string, options: T, context: CreateNodesContextV2) => string;
export type SkipIf<T> = (projectRoot: string, filePath: string, options: T, context: CreateNodesContextV2) => boolean;
export type WithOptionsNormalizer<T> = (options: Partial<T>) => T;

export type CreateNodesInternal<T extends Record<string, unknown>> = readonly [projectFilePattern: string, createNodesInternal: CreateNodesInternalFunction<T>];

export type CreateNodesInternalFunction<T> = (
filePath: string,
options: T,
context: CreateNodesContext & { pluginName: string },
configCache: ConfigCache
) => Promise<CreateNodesResult>;

export function createNodesInternalBuilder<T extends Record<string, unknown>>(projectFilePattern: string, generateConfig: GenerateConfig<T>) {
let withOptionsNormalizer: WithOptionsNormalizer<T>;
let withProjectRoot: WithProjectRoot<T>;
const skipIf: SkipIf<T>[] = [];

const builder = {
withProjectRoot(fn: WithProjectRoot<T>) {
withProjectRoot = fn;
return builder;
},
withOptionsNormalizer(fn: WithOptionsNormalizer<T>) {
withOptionsNormalizer = fn;
return builder;
},
skipIf(fn: SkipIf<T>) {
skipIf.push(fn);
return builder;
},
build(): CreateNodesInternal<T> {
return [
projectFilePattern,
async (filePath, options, context, configCache) => {
// Normalize the options if a normalizer function is provided.
options ??= {} as T;
options = withOptionsNormalizer ? withOptionsNormalizer(options) : options;

// Get project root from the file path. By default, take the directory of the file.
const projectRoot = withProjectRoot ? withProjectRoot(filePath, options, context) : dirname(filePath);

// Skip if one of the skipIf functions return true. By default, it should be linked to a project.json.
const isNotAttachedToProject: SkipIf<T> = (projectRoot, filePath) => !filePath.includes('project.json') && !isAttachedToProject(projectRoot);
const shouldSkip = [isNotAttachedToProject, ...skipIf].some((fn) => fn(projectRoot, filePath, options, context));
if (shouldSkip) return {};

// Compute hash based on the parameters and the pattern
const nodeHash = await calculateHashForCreateNodes(projectRoot, options, context);
const hash = `${nodeHash}_${projectFilePattern}`;

// if config not yet in cache, generate it
if (!configCache[hash]) {
// logger.verbose(`Devkit ${context.pluginName}: Re-Compute Cache for ${filePath}`);

// add by default a tag for the
const pluginTag = `nx-plugin:${context.pluginName}`;
const config = generateConfig(projectRoot, filePath, options, context);

configCache[hash] = {
...config,
tags: [...(config?.tags ?? []), pluginTag],
};
}

return {
projects: {
[projectRoot]: {
root: projectRoot,
...configCache[hash],
},
},
};
},
];
},
};

return builder;
}
3 changes: 3 additions & 0 deletions packages/plugin/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './combine-create-nodes.utils';
export * from './create-nodes-internal-builder.utils';
export * from './is-matching-plugin.utils';
9 changes: 9 additions & 0 deletions packages/plugin/src/utils/is-attached-to-project.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { readdirSync } from 'node:fs';
import { join } from 'node:path';
import { workspaceRoot } from 'nx/src/utils/workspace-root';

export function isAttachedToProject(projectRoot: string) {
// Ensure we inject config on an existing project
const siblingFiles = readdirSync(join(workspaceRoot, projectRoot));
return siblingFiles.includes('project.json');
}
11 changes: 11 additions & 0 deletions packages/plugin/src/utils/is-matching-plugin.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CreateNodesV2 } from '@nx/devkit';
import { CreateNodesInternal } from './create-nodes-internal-builder.utils';
import { minimatch } from 'minimatch';

export function isMatchingPlugin<T extends Record<string, unknown>>(
createNodes: CreateNodesV2<T> | CreateNodesInternal<T>,
filePath: string
) {
const pluginPattern = createNodes[0];
return minimatch(filePath, pluginPattern);
}
4 changes: 3 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@huge-nx/conventions": ["packages/conventions/src/index.ts"],
"@huge-nx/devkit": ["packages/devkit/src/index.ts"],
"@huge-nx/e2e-utils": ["e2e/utils/index.ts"],
"@huge-nx/huge-nx-plugin": ["packages/plugin/src/index.ts"]
"@huge-nx/plugin": ["packages/plugin/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]
Expand Down

0 comments on commit acf3883

Please sign in to comment.