diff --git a/nx-stylelint/.eslintrc.json b/nx-stylelint/.eslintrc.json index 66fbb884..4a709010 100644 --- a/nx-stylelint/.eslintrc.json +++ b/nx-stylelint/.eslintrc.json @@ -10,7 +10,7 @@ "@nx/dependency-checks": [ "error", { - "ignoredDependencies": ["stylelint-config-standard", "stylelint-config-standard-scss"] + "ignoredDependencies": ["stylelint-config-standard", "stylelint-config-standard-scss", "nx"] } ] } diff --git a/nx-stylelint/plugin.ts b/nx-stylelint/plugin.ts index 3a901a34..92599dfa 100644 --- a/nx-stylelint/plugin.ts +++ b/nx-stylelint/plugin.ts @@ -1 +1 @@ -export { createNodes, createDependencies, type StylelintPluginOptions } from './src/plugins/plugin'; +export { createNodes, createNodesV2, type StylelintPluginOptions } from './src/plugins/plugin'; diff --git a/nx-stylelint/src/plugins/plugin.spec.ts b/nx-stylelint/src/plugins/plugin.spec.ts index 981feeec..5eaa9301 100644 --- a/nx-stylelint/src/plugins/plugin.spec.ts +++ b/nx-stylelint/src/plugins/plugin.spec.ts @@ -1,6 +1,6 @@ import { CreateNodesContext } from '@nx/devkit'; import { vol } from 'memfs'; -import { createNodes } from './plugin'; +import { createNodesV2 } from './plugin'; jest.mock('node:fs', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -16,7 +16,7 @@ jest.mock('fs/promises', () => { }); describe('nx-stylelint/plugin', () => { - const createNodesFunction = createNodes[1]; + const createNodesFunction = createNodesV2[1]; let context: CreateNodesContext; beforeEach(async () => { @@ -59,37 +59,52 @@ describe('nx-stylelint/plugin', () => { '' ); - const nodes = await createNodesFunction('apps/my-app/.stylelintrc.json', {}, context); + const nodes = await createNodesFunction(['apps/my-app/.stylelintrc.json'], {}, context); expect(nodes).toMatchInlineSnapshot(` - { - "projects": { - "apps/my-app": { - "root": "apps/my-app", - "targets": { - "stylelint": { - "cache": true, - "command": "stylelint "**/*.css"", - "inputs": [ - "default", - "^default", - "{projectRoot}/.stylelintrc.json", - "{workspaceRoot}/.stylelintrc.json", - "{workspaceRoot}/apps/.stylelintrc.yaml", - { - "externalDependencies": [ - "stylelint", + [ + [ + "apps/my-app/.stylelintrc.json", + { + "projects": { + "apps/my-app": { + "root": "apps/my-app", + "targets": { + "stylelint": { + "cache": true, + "command": "stylelint "**/*.css"", + "inputs": [ + "default", + "^default", + "{projectRoot}/.stylelintrc.json", + "{workspaceRoot}/.stylelintrc.json", + "{workspaceRoot}/apps/.stylelintrc.yaml", + { + "externalDependencies": [ + "stylelint", + ], + }, ], + "metadata": { + "description": "Runs Stylelint on project", + "help": { + "command": "npx stylelint --help", + "example": {}, + }, + "technologies": [ + "stylelint", + ], + }, + "options": { + "cwd": "apps/my-app", + }, }, - ], - "options": { - "cwd": "apps/my-app", }, }, }, }, - }, - } + ], + ] `); }); @@ -105,8 +120,8 @@ describe('nx-stylelint/plugin', () => { '' ); - const nodes = await createNodesFunction('apps/my-app/.stylelintrc.json', {}, context); + const nodes = await createNodesFunction(['apps/my-app/.stylelintrc.json'], {}, context); - expect(nodes).toStrictEqual({}); + expect(nodes).toStrictEqual([['apps/my-app/.stylelintrc.json', {}]]); }); }); diff --git a/nx-stylelint/src/plugins/plugin.ts b/nx-stylelint/src/plugins/plugin.ts index c02bf757..eb95fbff 100644 --- a/nx-stylelint/src/plugins/plugin.ts +++ b/nx-stylelint/src/plugins/plugin.ts @@ -1,96 +1,134 @@ import { - CreateDependencies, CreateNodes, + CreateNodesContext, + CreateNodesV2, + ProjectConfiguration, TargetConfiguration, - cacheDir, + createNodesFromFiles, + getPackageManagerCommand, + logger, readJsonFile, writeJsonFile, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; -import { existsSync } from 'node:fs'; +import { existsSync, readdirSync } from 'node:fs'; import * as nodePath from 'node:path'; import { getInputConfigFiles } from '../utils/config-file'; +import { hashObject } from 'nx/src/hasher/file-hasher'; +import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; + +const pmc = getPackageManagerCommand(); + +const STYLELINT_CONFIG_FILES_GLOB = + '**/{.stylelintrc,.stylelintrc.{json,yml,yaml,js,cjs,mjs,ts,cts,mts},stylelint.config.{js,cjs,mjs,ts,cts,mts}}'; export interface StylelintPluginOptions { targetName?: string; extensions?: string[]; } -const cachePath = nodePath.join(cacheDir, 'stylelint.hash'); -const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; - -const calculatedTargets: Record> = {}; - -function readTargetsCache(): Record> { - return readJsonFile(cachePath); +function readTargetsCache(cachePath: string): Record { + return existsSync(cachePath) ? readJsonFile(cachePath) : {}; } -function writeTargetsToCache(targets: Record>) { - writeJsonFile(cachePath, targets); +function writeTargetsToCache(cachePath: string, results: Record) { + writeJsonFile(cachePath, results); } -export const createDependencies: CreateDependencies = () => { - writeTargetsToCache(calculatedTargets); - return []; -}; +export const createNodesV2: CreateNodesV2 = [ + STYLELINT_CONFIG_FILES_GLOB, + async (projectConfigurationFiles, options, context) => { + const optionsHash = hashObject(options ?? {}); + const cachePath = nodePath.join(workspaceDataDirectory, `jest-${optionsHash}.hash`); + const targetsCache = readTargetsCache(cachePath); + + try { + return await createNodesFromFiles( + (configFile, options, context) => createNodesInternal(configFile, options, context, targetsCache), + projectConfigurationFiles, + options, + context + ); + } finally { + writeTargetsToCache(cachePath, targetsCache); + } + }, +]; export const createNodes: CreateNodes = [ - '**/.stylelintrc.{json,yml,yaml,js,cjs,mjs,ts}', + STYLELINT_CONFIG_FILES_GLOB, async (configFilePath, options, context) => { - const projectRoot = nodePath.dirname(configFilePath); - - // Do not create a project if package.json and project.json isn't there. - if ( - !existsSync(nodePath.join(context.workspaceRoot, projectRoot, 'package.json')) && - !existsSync(nodePath.join(context.workspaceRoot, projectRoot, 'project.json')) - ) - return {}; - - const isStandaloneWorkspace = - projectRoot === '.' && - existsSync(nodePath.join(context.workspaceRoot, projectRoot, 'src')) && - existsSync(nodePath.join(context.workspaceRoot, projectRoot, 'package.json')); - - if (projectRoot === '.' && !isStandaloneWorkspace) return {}; - - const normalizedOptions = normalizeOptions(options); - const hash = await calculateHashForCreateNodes(projectRoot, normalizedOptions, context); - const targets = targetsCache[hash] ?? (await buildStylelintTargets(configFilePath, projectRoot, normalizedOptions)); - - calculatedTargets[hash] = targets; - - return { - projects: { - [projectRoot]: { - root: projectRoot, - targets: targets, - }, - }, - }; + logger.warn( + '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' + ); + return createNodesInternal(configFilePath, options, context, {}); }, ]; -async function buildStylelintTargets( +async function createNodesInternal( + configFilePath: string, + options: StylelintPluginOptions | undefined, + context: CreateNodesContext, + targetsCache: Record +) { + const projectRoot = nodePath.dirname(configFilePath); + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(nodePath.join(context.workspaceRoot, projectRoot)); + if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { + return {}; + } + + const normalizedOptions = normalizeOptions(options); + + const hash = (await calculateHashForCreateNodes(projectRoot, normalizedOptions, context)) + configFilePath; + + targetsCache[hash] ??= await stylelintTarget(configFilePath, projectRoot, normalizedOptions); + const target = targetsCache[hash]; + + const project: ProjectConfiguration = { + root: projectRoot, + targets: { + [normalizedOptions.targetName]: target, + }, + }; + + return { + projects: { + [projectRoot]: project, + }, + }; +} + +async function stylelintTarget( configFilePath: string, projectRoot: string, options: Required -): Promise> { +): Promise { const inputConfigFiles = await getInputConfigFiles(configFilePath, projectRoot); + const glob = + options.extensions.length === 1 ? `**/*.${options.extensions[0]}` : `**/*.{${options.extensions.join(',')}}`; + return { - [options.targetName]: { - command: `stylelint "${getLintFileGlob(options.extensions)}"`, - cache: true, - options: { - cwd: projectRoot, + command: `stylelint "${glob}"`, + cache: true, + options: { + cwd: projectRoot, + }, + inputs: [ + 'default', + // Certain lint rules can be impacted by changes to dependencies + '^default', + ...inputConfigFiles, + { externalDependencies: ['stylelint'] }, + ], + metadata: { + technologies: ['stylelint'], + description: 'Runs Stylelint on project', + help: { + command: `${pmc.exec} stylelint --help`, + example: {}, }, - inputs: [ - 'default', - // Certain lint rules can be impacted by changes to dependencies - '^default', - ...inputConfigFiles, - { externalDependencies: ['stylelint'] }, - ], }, }; } @@ -107,8 +145,3 @@ function normalizeOptions(options: StylelintPluginOptions | undefined): Required extensions: extensions ?? ['css'], }; } - -function getLintFileGlob(extensions: string[]): string { - if (extensions.length === 1) return `**/*.${extensions[0]}`; - return `**/*.{${extensions.join(',')}}`; -} diff --git a/nx-stylelint/src/utils/globs.ts b/nx-stylelint/src/utils/globs.ts new file mode 100644 index 00000000..969f6a8a --- /dev/null +++ b/nx-stylelint/src/utils/globs.ts @@ -0,0 +1,4 @@ +export function combineGlobPatterns(...patterns: (string | string[])[]) { + const p = patterns.flat(); + return p.length > 1 ? '{' + p.join(',') + '}' : p.length === 1 ? p[0] : ''; +}