Skip to content

Commit

Permalink
refactor: restructure generators
Browse files Browse the repository at this point in the history
  • Loading branch information
Phillip9587 committed Apr 12, 2024
1 parent d46eb89 commit d60e49a
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 151 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.validate": ["json"]
"eslint.validate": ["json"],
"typescript.tsdk": "node_modules/typescript/lib"
}
58 changes: 18 additions & 40 deletions nx-stylelint/src/generators/configuration/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ import {
formatFiles,
joinPathFragments,
logger,
offsetFromRoot,
readProjectConfiguration,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import type { Config } from 'stylelint';
import type { FormatterType } from 'stylelint';
import type { LintExecutorSchema } from '../../executors/lint/schema';
import { createProjectStylelintConfigFile } from '../../utils/config-file';
import { defaultFormatter, isCoreFormatter } from '../../utils/formatter';
import { initGenerator } from '../init/generator';
import type { ConfigurationGeneratorSchema } from './schema';

interface NormalizedSchema extends ConfigurationGeneratorSchema {
interface NormalizedSchema extends Required<ConfigurationGeneratorSchema> {
projectRoot: string;
stylelintTargetExists: boolean;
}
Expand All @@ -35,8 +34,8 @@ export async function configurationGenerator(

logger.info(`Adding Stylelint configuration and target to '${options.project}' ...\n`);

createStylelintConfig(host, normalizedOptions);
addStylelintTarget(host, normalizedOptions);
createProjectStylelintConfigFile(host, normalizedOptions.projectRoot, normalizedOptions.scss);
addTarget(host, normalizedOptions);

if (options.skipFormat !== true) await formatFiles(host);

Expand All @@ -48,33 +47,34 @@ export default configurationGenerator;
function normalizeSchema(tree: Tree, options: ConfigurationGeneratorSchema): NormalizedSchema {
const projectConfig = readProjectConfiguration(tree, options.project);

const validFormatter = isCoreFormatter(options.formatter);
if (options.formatter && !validFormatter) {
let formatter: FormatterType = defaultFormatter;
if (isCoreFormatter(options.formatter)) {
formatter = options.formatter;
} else if (options.formatter) {
logger.error(
`Given formatter '${options.formatter}' is not a stylelint core formatter. Falling back to 'string' formatter.`,
`Given formatter '${options.formatter}' is not a stylelint core formatter. Falling back to '${defaultFormatter}' formatter.`,
);
}

return {
...options,
formatter: validFormatter ? options.formatter : defaultFormatter,
project: options.project,
formatter,
scss: !!options.scss,
skipFormat: !!options.skipFormat,
projectRoot: projectConfig.root,
stylelintTargetExists: !!projectConfig.targets?.['stylelint'],
};
}

function addStylelintTarget(tree: Tree, options: NormalizedSchema) {
function addTarget(tree: Tree, options: NormalizedSchema) {
const projectConfig = readProjectConfiguration(tree, options.project);

const targetOptions: Partial<LintExecutorSchema> = {
const targetOptions = {
lintFilePatterns: [joinPathFragments(options.projectRoot, '**', '*.css')],
formatter: options.formatter === 'string' ? undefined : options.formatter,
};
} satisfies Partial<LintExecutorSchema>;

if (options.scss) {
targetOptions.lintFilePatterns ??= [];
targetOptions.lintFilePatterns.push(joinPathFragments(options.projectRoot, '**', '*.scss'));
}
if (options.scss) targetOptions.lintFilePatterns.push(joinPathFragments(options.projectRoot, '**', '*.scss'));

projectConfig.targets = {
...projectConfig.targets,
Expand All @@ -86,25 +86,3 @@ function addStylelintTarget(tree: Tree, options: NormalizedSchema) {
};
updateProjectConfiguration(tree, options.project, projectConfig);
}

function createStylelintConfig(tree: Tree, options: NormalizedSchema) {
const config = {
extends: [joinPathFragments(offsetFromRoot(options.projectRoot), '.stylelintrc.json')],
ignoreFiles: ['!**/*'],
overrides: [
{
files: ['**/*.css'],
rules: {},
},
],
};

if (options.scss) {
config.overrides.push({
files: ['**/*.scss'],
rules: {},
});
}

writeJson<Config>(tree, joinPathFragments(options.projectRoot, '.stylelintrc.json'), config);
}
30 changes: 0 additions & 30 deletions nx-stylelint/src/generators/init/generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,34 +133,4 @@ You can then migrate your custom rule configuration into the created stylelint c
expect(nxConfig.namedInputs?.['production']).toBeUndefined();
});
});

describe('VSCode Extension', () => {
it('should add stylelint vscode extension to vscode extension recommendations when they exist', async () => {
writeJson(tree, '.vscode/extensions.json', { recommendations: [] });

await generator(tree, defaultOptions);

const extensions = readJson(tree, '.vscode/extensions.json');
expect(extensions.recommendations).toContain('stylelint.vscode-stylelint');
});

it('should add stylelint vscode extension and recommendations array to vscode extension file if it exists', async () => {
writeJson(tree, '.vscode/extensions.json', {});

await generator(tree, defaultOptions);

const extensions = readJson(tree, '.vscode/extensions.json');
expect(extensions.recommendations).toContain('stylelint.vscode-stylelint');
});

it('should not add stylelint vscode extension to vscode extension recommendations when it the extension already exists in recommendations', async () => {
writeJson(tree, '.vscode/extensions.json', { recommendations: ['stylelint.vscode-stylelint'] });

await generator(tree, defaultOptions);

const extensions = readJson(tree, '.vscode/extensions.json');
expect(extensions.recommendations).toHaveLength(1);
expect(extensions.recommendations).toContain('stylelint.vscode-stylelint');
});
});
});
111 changes: 36 additions & 75 deletions nx-stylelint/src/generators/init/generator.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
updateNxJson as devkitUpdateNxJson,
updateNxJson,
formatFiles,
joinPathFragments,
logger,
readJson,
readNxJson,
stripIndents,
updateJson,
writeJson,
} from '@nx/devkit';
import type { Config } from 'stylelint';
import { stylelintConfigFilePattern } from '../../utils/config-file';
import { ROOT_STYLELINT_CONFIG_SCSS_OVERRIDE } from '../../utils/config';
import {
STYLELINT_CONFIG_FILE,
STYLELINT_CONFIG_FILE_PATTERN,
createRootStylelintConfigFile,
isCompatibleRootConfig,
} from '../../utils/config-file';
import {
stylelintConfigStandardScssVersion,
stylelintConfigStandardVersion,
stylelintVSCodeExtension,
stylelintVersion,
} from '../../utils/versions';
import { addStylelintVSCodeExtension } from '../../utils/vscode';
import type { InitGeneratorSchema } from './schema';

const NX_JSON_WARNING = `nx.json not found. Create a nx.json file and rerun the generator with 'nx g nx-stylelint:init'.`;

/** nx-stylelint:init generator */
export async function initGenerator(tree: Tree, options: InitGeneratorSchema): Promise<GeneratorCallback> {
const installTask = updateDependencies(tree, !!options.scss);

if (!tree.exists('.stylelintrc.json')) createRecommendedStylelintConfiguration(tree, !!options.scss);
if (!tree.exists('.stylelintrc.json')) createRootStylelintConfigFile(tree, !!options.scss);
else if (options.scss === true && isCompatibleRootConfig(tree)) addScssToStylelintConfiguration(tree);
else {
logger.info(
Expand All @@ -37,8 +43,10 @@ You can then migrate your custom rule configuration into the created stylelint c
);
}

updateNxJson(tree);
updateVSCodeExtensions(tree);
addTargetDefaults(tree);
updateProductionFileset(tree);

addStylelintVSCodeExtension(tree);

if (options.skipFormat !== true) await formatFiles(tree);
return installTask;
Expand All @@ -61,82 +69,44 @@ function updateDependencies(tree: Tree, scss: boolean): GeneratorCallback {
return addDependenciesToPackageJson(tree, {}, devDependencies);
}

/** Adds the Stylelint VSCode Extension to the recommenden Extensions if the file exists */
function updateVSCodeExtensions(tree: Tree): void {
if (!tree.exists('.vscode/extensions.json')) return;

updateJson(tree, '.vscode/extensions.json', (json) => {
json.recommendations ??= [];

if (Array.isArray(json.recommendations) && !json.recommendations.includes(stylelintVSCodeExtension))
json.recommendations.push(stylelintVSCodeExtension);

return json;
});
}

/** Adds the root .stylelintrc.json file to the targetDefaults and stylelint target to the cacheable operations of the default task runner */
function updateNxJson(tree: Tree) {
function addTargetDefaults(tree: Tree): void {
const nxJson = readNxJson(tree);
if (!nxJson) {
logger.warn(
stripIndents`nx.json not found. Create a nx.json file and rerun the generator with 'nx run nx-stylelint:init' to configure nx-stylelint inputs and taskrunner options.`,
);
logger.warn(NX_JSON_WARNING);
return;
}

// remove stylelint config files from production inputs
const stylelintProjectConfigFilePattern = `!${joinPathFragments('{projectRoot}', stylelintConfigFilePattern)}`;
if (
nxJson.namedInputs?.['production'] &&
!nxJson.namedInputs?.['production'].includes(stylelintProjectConfigFilePattern)
) {
nxJson.namedInputs?.['production'].push(stylelintProjectConfigFilePattern);
}

// Set targetDefault for stylelint
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['stylelint'] ??= {};
nxJson.targetDefaults['stylelint'].inputs ??= ['default'];
nxJson.targetDefaults['stylelint'].cache = true;

const rootStylelintConfigurationFile = joinPathFragments('{workspaceRoot}', stylelintConfigFilePattern);
const rootStylelintConfigurationFile = joinPathFragments('{workspaceRoot}', STYLELINT_CONFIG_FILE_PATTERN);
if (!nxJson.targetDefaults['stylelint'].inputs.includes(rootStylelintConfigurationFile))
nxJson.targetDefaults['stylelint'].inputs.push(rootStylelintConfigurationFile);

devkitUpdateNxJson(tree, nxJson);
updateNxJson(tree, nxJson);
}

function createRecommendedStylelintConfiguration(tree: Tree, scss: boolean) {
const config = {
ignoreFiles: ['**/*'],
overrides: [
{
files: ['**/*.css'],
extends: ['stylelint-config-standard'],
rules: {},
},
],
rules: {},
};

if (scss)
config.overrides.push({
files: ['**/*.scss'],
extends: ['stylelint-config-standard-scss'],
rules: {},
});

writeJson<Config>(tree, '.stylelintrc.json', config);
}
function updateProductionFileset(tree: Tree) {
const nxJson = readNxJson(tree);
if (!nxJson) {
logger.warn(NX_JSON_WARNING);
return;
}

const negatedStylelintProjectConfigFilePattern = `!${joinPathFragments('{projectRoot}', STYLELINT_CONFIG_FILE_PATTERN)}`;
if (
nxJson.namedInputs?.['production'] &&
!nxJson.namedInputs?.['production'].includes(negatedStylelintProjectConfigFilePattern)
)
nxJson.namedInputs?.['production'].push(negatedStylelintProjectConfigFilePattern);

function isCompatibleRootConfig(tree: Tree): boolean {
const config = readJson<Config>(tree, '.stylelintrc.json');
return config.ignoreFiles === '**/*' || (Array.isArray(config.ignoreFiles) && config.ignoreFiles.includes('**/*'));
updateNxJson(tree, nxJson);
}

function addScssToStylelintConfiguration(tree: Tree) {
updateJson<Config, Config>(tree, '.stylelintrc.json', (value) => {
updateJson<Config, Config>(tree, STYLELINT_CONFIG_FILE, (value) => {
if (
value.overrides?.find(
(item) =>
Expand All @@ -149,16 +119,7 @@ function addScssToStylelintConfiguration(tree: Tree) {

return {
...value,
overrides: Array.from(
new Set([
...(value.overrides ?? []),
{
files: ['**/*.scss'],
extends: ['stylelint-config-standard-scss'],
rules: {},
},
]),
),
overrides: Array.from(new Set([...(value.overrides ?? []), ROOT_STYLELINT_CONFIG_SCSS_OVERRIDE])),
};
});
}
36 changes: 35 additions & 1 deletion nx-stylelint/src/utils/config-file.ts
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
export const stylelintConfigFilePattern = '.stylelintrc(.(json|yml|yaml|js))?';
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';

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

export const STYLELINT_CONFIG_FILE = '.stylelintrc.json';

export const createRootStylelintConfigFile = (tree: Tree, withScssSupport: boolean) =>
writeJson<Config>(tree, STYLELINT_CONFIG_FILE, withScssSupport ? ROOT_STYLELINT_CONFIG_SCSS : ROOT_STYLELINT_CONFIG);

export function createProjectStylelintConfigFile(tree: Tree, projectRoot: string, withScssSupport: boolean) {
const config = {
extends: [joinPathFragments(offsetFromRoot(projectRoot), STYLELINT_CONFIG_FILE)],
ignoreFiles: ['!**/*'],
overrides: [
{
files: ['**/*.css'],
rules: {},
},
],
};

if (withScssSupport) {
config.overrides.push(PROJECT_STYLELINT_CONFIG_SCSS_OVERRIDE);
}

writeJson<Config>(tree, joinPathFragments(projectRoot, STYLELINT_CONFIG_FILE), config);
}

export function isCompatibleRootConfig(tree: Tree): boolean {
if (!tree.exists(STYLELINT_CONFIG_FILE)) return false;
const config = readJson<Config>(tree, STYLELINT_CONFIG_FILE);
return config.ignoreFiles === '**/*' || (Array.isArray(config.ignoreFiles) && config.ignoreFiles.includes('**/*'));
}
35 changes: 35 additions & 0 deletions nx-stylelint/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Config } from 'stylelint';

type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[]
? ElementType
: never;

type ConfigOverride = ArrayElement<NonNullable<Config['overrides']>>;

export const ROOT_STYLELINT_CONFIG = {
ignoreFiles: ['**/*'],
overrides: [
{
files: ['**/*.css'],
extends: ['stylelint-config-standard'],
rules: {},
},
],
rules: {},
} satisfies Config;

export const ROOT_STYLELINT_CONFIG_SCSS_OVERRIDE: ConfigOverride = {
files: ['**/*.scss'],
extends: ['stylelint-config-standard-scss'],
rules: {},
};

export const ROOT_STYLELINT_CONFIG_SCSS = {
...ROOT_STYLELINT_CONFIG,
overrides: [...ROOT_STYLELINT_CONFIG.overrides, ROOT_STYLELINT_CONFIG_SCSS_OVERRIDE],
} satisfies Config;

export const PROJECT_STYLELINT_CONFIG_SCSS_OVERRIDE = {
files: ['**/*.scss'],
rules: {},
} satisfies ConfigOverride;

0 comments on commit d60e49a

Please sign in to comment.