Skip to content

Commit

Permalink
feat: add create-nodes plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Phillip9587 committed Apr 12, 2024
1 parent a09d913 commit ad3c262
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 0 deletions.
1 change: 1 addition & 0 deletions nx-stylelint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"executors": "./executors.json",
"dependencies": {
"@nx/devkit": "^18.0.0",
"cosmiconfig": "^9.0.0",
"tslib": "^2.6.2"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions nx-stylelint/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createNodes, type StylelintPluginOptions } from './src/plugins/plugin';
112 changes: 112 additions & 0 deletions nx-stylelint/src/plugins/plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { CreateNodesContext } from '@nx/devkit';
import { vol } from 'memfs';
import { createNodes } from './plugin';

jest.mock('node:fs', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('memfs').fs;
});
jest.mock('fs', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('memfs').fs;
});
jest.mock('fs/promises', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('memfs').fs.promises;
});

describe('nx-stylelint/plugin', () => {
const createNodesFunction = createNodes[1];
let context: CreateNodesContext;

beforeEach(async () => {
context = {
nxJsonConfiguration: {
// These defaults should be overridden by plugin
targetDefaults: {
stylelint: {
cache: false,
inputs: ['foo', '^foo'],
},
},
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
},
workspaceRoot: '',
};
});

afterEach(() => {
vol.reset();
jest.resetModules();
});

it('should create nodes for nested project', async () => {
vol.fromJSON(
{
'apps/my-app/.stylelintrc.json': JSON.stringify({
extends: ['../.stylelintrc.yaml', 'stylelint-config-standard'],
}),
'apps/my-app/project.json': `{}`,
'apps/.stylelintrc.yaml': `extends:
- ../.stylelintrc.json`,
'.stylelintrc.json': JSON.stringify({ extends: ['stylelint-config-standard'] }),
'package.json': `{}`,
},
'',
);

const nodes = await createNodesFunction('apps/my-app/.stylelintrc.json', {}, context);

expect(nodes).toMatchInlineSnapshot(`
{
"projects": {
"apps/my-app": {
"targets": {
"stylelint": {
"cache": true,
"command": "stylelint",
"inputs": [
"default",
"{projectRoot}/.stylelintrc.json",
"{workspaceRoot}/apps/.stylelintrc.yaml",
"{workspaceRoot}/.stylelintrc.json",
{
"externalDependencies": [
"stylelint",
],
},
],
"options": {
"args": [
""**/*.css"",
],
"cwd": "apps/my-app",
},
},
},
},
},
}
`);
});

it('should not create targets when there is no project.json or package.json', async () => {
vol.fromJSON(
{
'apps/my-app/.stylelintrc.json': JSON.stringify({
extends: ['../../.stylelintrc.json'],
}),
'.stylelintrc.json': JSON.stringify({ extends: ['stylelint-config-standard'] }),
'package.json': `{}`,
},
'',
);

const nodes = await createNodesFunction('apps/my-app/.stylelintrc.json', {}, context);

expect(nodes).toStrictEqual({});
});
});
70 changes: 70 additions & 0 deletions nx-stylelint/src/plugins/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { CreateNodes, joinPathFragments, workspaceRoot } from '@nx/devkit';
import { existsSync } from 'node:fs';
import * as nodePath from 'node:path';
import { readAffectingStylelintConfigFiles } from '../utils/config-file';

export interface StylelintPluginOptions {
targetName?: string;
lintFilePatterns?: string | string[];
}

export const createNodes: CreateNodes<StylelintPluginOptions> = [
'**/.stylelintrc.{json,yml,yaml,js,cjs,mjs}',
async (configFilePath, options, context) => {
const { targetName, lintFilePatterns } = normalizeOptions(options);

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 inputConfigFiles = await getInputConfigFiles(configFilePath, projectRoot);

return {
projects: {
[projectRoot]: {
targets: {
[targetName]: {
command: `stylelint`,
cache: true,
options: {
cwd: projectRoot,
args: [...lintFilePatterns.map((pattern) => `"${pattern}"`)],
},
inputs: ['default', ...inputConfigFiles, { externalDependencies: ['stylelint'] }],
},
},
},
},
};
},
];

async function getInputConfigFiles(configFilePath: string, projectRoot: string): Promise<string[]> {
return [...(await readAffectingStylelintConfigFiles(configFilePath))]
.filter((p) => p.startsWith(workspaceRoot))
.map((configFilePath) => {
if (configFilePath.startsWith(workspaceRoot)) {
configFilePath = nodePath.relative(workspaceRoot, configFilePath);

if (configFilePath.startsWith(projectRoot)) {
configFilePath = joinPathFragments('{projectRoot}', configFilePath.substring(projectRoot.length));
} else {
configFilePath = joinPathFragments('{workspaceRoot}', configFilePath);
}
}

return configFilePath;
});
}

function normalizeOptions(options: StylelintPluginOptions | undefined) {
return {
targetName: options?.targetName ?? 'stylelint',
lintFilePatterns: options?.lintFilePatterns ? [options?.lintFilePatterns].flat() : ['**/*.css'],
};
}
50 changes: 50 additions & 0 deletions nx-stylelint/src/utils/config-file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { type Tree, joinPathFragments, offsetFromRoot, readJson, writeJson } from '@nx/devkit';
import type { Config } from 'stylelint';
import { PROJECT_STYLELINT_CONFIG_SCSS_OVERRIDE, ROOT_STYLELINT_CONFIG, ROOT_STYLELINT_CONFIG_SCSS } from './config';
import { workspaceRoot } from '@nx/devkit';
import { cosmiconfig } from 'cosmiconfig';
import { existsSync } from 'node:fs';
import { dirname, isAbsolute, join } from 'node:path';
import { isRelativePath } from './path';

export const STYLELINT_CONFIG_FILE_PATTERN = '.stylelintrc(.(json|yml|yaml|js))?';

Expand Down Expand Up @@ -33,3 +38,48 @@ export function isCompatibleRootConfig(tree: Tree): boolean {
const config = readJson<Config>(tree, STYLELINT_CONFIG_FILE);
return config.ignoreFiles === '**/*' || (Array.isArray(config.ignoreFiles) && config.ignoreFiles.includes('**/*'));
}

const explorer = cosmiconfig('stylelint', { cache: false, stopDir: workspaceRoot });

export async function readAffectingStylelintConfigFiles(filePath: string): Promise<Set<string>> {
if (!existsSync(filePath)) return new Set();

try {
const result = await explorer.load(filePath);
if (!result) return new Set();

const stylelintConfigFiles = new Set<string>([result.filepath]);

if (result.config?.['extends']) {
const extendsItems = new Set<string>();
if (typeof result.config?.['extends'] === 'string') {
extendsItems.add(result.config['extends']);
} else if (Array.isArray(result.config?.['extends'])) {
for (const value of result.config['extends'].filter((v) => typeof v === 'string')) {
extendsItems.add(value);
}
}

if (extendsItems.size > 0) {
for (const extendFilePath of extendsItems) {
if (isAbsolute(extendFilePath)) {
for (const value of await readAffectingStylelintConfigFiles(extendFilePath)) {
stylelintConfigFiles.add(value);
}
} else if (isRelativePath(extendFilePath)) {
const path = join(dirname(filePath), extendFilePath);
for (const value of await readAffectingStylelintConfigFiles(path)) {
stylelintConfigFiles.add(value);
}
}
}
}
}

return stylelintConfigFiles;
} catch (err) {
console.error(err);
/* empty */
throw err;
}
}
3 changes: 3 additions & 0 deletions nx-stylelint/src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isRelativePath(path: string): boolean {
return path === '.' || path === '..' || path.startsWith('./') || path.startsWith('../');
}
1 change: 1 addition & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/node": "18.19.31",
"@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0",
"cosmiconfig": "9.0.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"husky": "9.0.11",
Expand Down

0 comments on commit ad3c262

Please sign in to comment.