Skip to content

Commit 0f420f8

Browse files
feat: added configEmoji Addon to blockESLintPlugin (#2194)
## PR Checklist - [x] Addresses an existing open issue: fixes #2190 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Uses a similar AST parsing strategy as `blockESLintIntake` to find a `const config = `. 🎁
1 parent 1345137 commit 0f420f8

File tree

4 files changed

+361
-5
lines changed

4 files changed

+361
-5
lines changed

Diff for: src/blocks/blockESLintPlugin.test.ts

+275-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { testBlock } from "bingo-stratum-testers";
2-
import { describe, expect, test } from "vitest";
1+
import { testBlock, testIntake } from "bingo-stratum-testers";
2+
import { describe, expect, it, test } from "vitest";
33

44
import { blockESLintPlugin } from "./blockESLintPlugin.js";
55
import { optionsBase } from "./options.fakes.js";
66

77
describe("blockESLintPlugin", () => {
8-
test("without options or mode", () => {
8+
test("without addons, mode, or options", () => {
99
const creation = testBlock(blockESLintPlugin, {
1010
options: optionsBase,
1111
});
@@ -506,4 +506,276 @@ describe("blockESLintPlugin", () => {
506506
}
507507
`);
508508
});
509+
510+
test("addons", () => {
511+
const creation = testBlock(blockESLintPlugin, {
512+
addons: {
513+
configEmoji: [
514+
["recommended", "✅"],
515+
["legacy-recommended", "✔️"],
516+
],
517+
},
518+
options: optionsBase,
519+
});
520+
521+
expect(creation).toMatchInlineSnapshot(`
522+
{
523+
"addons": [
524+
{
525+
"addons": {
526+
"words": [
527+
"eslint-doc-generatorrc",
528+
],
529+
},
530+
"block": [Function],
531+
},
532+
{
533+
"addons": {
534+
"sections": {
535+
"Building": {
536+
"innerSections": [
537+
{
538+
"contents": "
539+
Run [\`eslint-doc-generator\`](https://github.com/bmish/eslint-doc-generator) to generate Markdown files documenting rules.
540+
541+
\`\`\`shell
542+
pnpm build:docs
543+
\`\`\`
544+
",
545+
"heading": "Building Docs",
546+
},
547+
],
548+
},
549+
"Linting": {
550+
"contents": {
551+
"items": [
552+
"- \`pnpm lint:docs\` ([eslint-doc-generator](https://github.com/bmish/eslint-doc-generator)): Generates and validates documentation for ESLint rules",
553+
],
554+
},
555+
},
556+
},
557+
},
558+
"block": [Function],
559+
},
560+
{
561+
"addons": {
562+
"extensions": [
563+
"eslintPlugin.configs["flat/recommended"]",
564+
],
565+
"ignores": [
566+
".eslint-doc-generatorrc.js",
567+
"docs/rules/*/*.ts",
568+
],
569+
"imports": [
570+
{
571+
"source": {
572+
"packageName": "eslint-plugin-eslint-plugin",
573+
"version": "6.4.0",
574+
},
575+
"specifier": "eslintPlugin",
576+
},
577+
],
578+
},
579+
"block": [Function],
580+
},
581+
{
582+
"addons": {
583+
"jobs": [
584+
{
585+
"name": "Lint Docs",
586+
"steps": [
587+
{
588+
"run": "pnpm build || exit 0",
589+
},
590+
{
591+
"run": "pnpm lint:docs",
592+
},
593+
],
594+
},
595+
],
596+
},
597+
"block": [Function],
598+
},
599+
{
600+
"addons": {
601+
"properties": {
602+
"devDependencies": {
603+
"eslint-doc-generator": "2.1.0",
604+
"eslint-plugin-eslint-plugin": "6.4.0",
605+
},
606+
"scripts": {
607+
"build:docs": "pnpm build --no-dts && eslint-doc-generator",
608+
"lint:docs": "eslint-doc-generator --check",
609+
},
610+
},
611+
},
612+
"block": [Function],
613+
},
614+
{
615+
"addons": {
616+
"coverage": {
617+
"exclude": [
618+
"src/index.ts",
619+
"src/rules/index.ts",
620+
],
621+
},
622+
},
623+
"block": [Function],
624+
},
625+
],
626+
"files": {
627+
".eslint-doc-generatorrc.js": "import prettier from "prettier";
628+
629+
/** @type {import('eslint-doc-generator').GenerateOptions} */
630+
const config = {
631+
configEmoji: [["recommended","✅"],["legacy-recommended","✔️"]],
632+
postprocess: async (content, path) =>
633+
prettier.format(content, {
634+
...(await prettier.resolveConfig(path)),
635+
parser: "markdown",
636+
}),
637+
ruleDocTitleFormat: "name",
638+
};
639+
640+
export default config;
641+
",
642+
},
643+
}
644+
`);
645+
});
646+
647+
describe("intake", () => {
648+
it("returns nothing when .eslint-doc-generatorrc.js and .eslint-doc-generatorrc.mjs do not exist", () => {
649+
const actual = testIntake(blockESLintPlugin, {
650+
files: {},
651+
options: optionsBase,
652+
});
653+
654+
expect(actual).toEqual(undefined);
655+
});
656+
657+
it("returns nothing when .eslint-doc-generatorrc.js does not have a to const config =", () => {
658+
const actual = testIntake(blockESLintPlugin, {
659+
files: {
660+
".eslint-doc-generatorrc.js": [`const other = {};`],
661+
},
662+
options: optionsBase,
663+
});
664+
665+
expect(actual).toEqual(undefined);
666+
});
667+
668+
it("returns nothing when .eslint-doc-generatorrc.js passes nothing to config =", () => {
669+
const actual = testIntake(blockESLintPlugin, {
670+
files: {
671+
".eslint-doc-generatorrc.js": [`const config = {};`],
672+
},
673+
options: optionsBase,
674+
});
675+
676+
expect(actual).toEqual(undefined);
677+
});
678+
679+
it("returns nothing when .eslint-doc-generatorrc.js passes invalid syntax to config =", () => {
680+
const actual = testIntake(blockESLintPlugin, {
681+
files: {
682+
".eslint-doc-generatorrc.js": [`const config = { ! }`],
683+
},
684+
options: optionsBase,
685+
});
686+
687+
expect(actual).toEqual(undefined);
688+
});
689+
690+
it("returns nothing when .eslint-doc-generatorrc.js passes unrelated properties to config =", () => {
691+
const actual = testIntake(blockESLintPlugin, {
692+
files: {
693+
".eslint-doc-generatorrc.js": [`const config = { other: true }`],
694+
},
695+
options: optionsBase,
696+
});
697+
698+
expect(actual).toEqual(undefined);
699+
});
700+
701+
it("returns configEmoji when it exists alone in .eslint-doc-generatorrc.js", () => {
702+
const actual = testIntake(blockESLintPlugin, {
703+
files: {
704+
".eslint-doc-generatorrc.js": [
705+
`const config = { configEmoji: [["recommended", "✅"]] }`,
706+
],
707+
},
708+
options: optionsBase,
709+
});
710+
711+
expect(actual).toEqual({
712+
configEmoji: [["recommended", "✅"]],
713+
});
714+
});
715+
});
716+
717+
it("returns configEmoji when it exists alone in .eslint-doc-generatorrc.mjs", () => {
718+
const actual = testIntake(blockESLintPlugin, {
719+
files: {
720+
".eslint-doc-generatorrc.mjs": [
721+
`const config = { configEmoji: [["recommended", "✅"]] }`,
722+
],
723+
},
724+
options: optionsBase,
725+
});
726+
727+
expect(actual).toEqual({
728+
configEmoji: [["recommended", "✅"]],
729+
});
730+
});
731+
732+
it("returns configEmoji when it exists with other data in .eslint-doc-generatorrc.js", () => {
733+
const actual = testIntake(blockESLintPlugin, {
734+
files: {
735+
".eslint-doc-generatorrc.js": [
736+
`const config = { configEmoji: [["recommended", "✅"]], other: true }`,
737+
],
738+
},
739+
options: optionsBase,
740+
});
741+
742+
expect(actual).toEqual({
743+
configEmoji: [["recommended", "✅"]],
744+
});
745+
});
746+
747+
it("returns configEmoji when it exists with other, non-JSON5 data in a full .eslint-doc-generatorrc.js", () => {
748+
const actual = testIntake(blockESLintPlugin, {
749+
files: {
750+
".eslint-doc-generatorrc.js": [
751+
`import prettier from "prettier";
752+
753+
/** @type {import('eslint-doc-generator').GenerateOptions} */
754+
const config = {
755+
configEmoji: [
756+
["recommended", "✅"],
757+
["legacy-recommended", "✔️"],
758+
],
759+
postprocess: async (content, path) =>
760+
prettier.format(content, {
761+
...(await prettier.resolveConfig(path)),
762+
parser: "markdown",
763+
}),
764+
ruleDocTitleFormat: "name",
765+
};
766+
767+
export default config;
768+
`,
769+
],
770+
},
771+
options: optionsBase,
772+
});
773+
774+
expect(actual).toEqual({
775+
configEmoji: [
776+
["recommended", "✅"],
777+
["legacy-recommended", "✔️"],
778+
],
779+
});
780+
});
509781
});

Diff for: src/blocks/blockESLintPlugin.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@ import { blockESLint } from "./blockESLint.js";
55
import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
66
import { blockPackageJson } from "./blockPackageJson.js";
77
import { blockVitest } from "./blockVitest.js";
8+
import { blockESLintPluginIntake } from "./eslint/blockESLintPluginIntake.js";
9+
import { zConfigEmoji } from "./eslint/schemas.js";
10+
import { intakeFile } from "./intake/intakeFile.js";
811

912
export const blockESLintPlugin = base.createBlock({
1013
about: {
1114
name: "ESLint Plugin",
1215
},
13-
produce({ options }) {
16+
addons: {
17+
configEmoji: zConfigEmoji,
18+
},
19+
intake({ files }) {
20+
const docGeneratorConfigRaw = intakeFile(files, [
21+
[".eslint-doc-generatorrc.js", ".eslint-doc-generatorrc.mjs"],
22+
]);
23+
24+
return docGeneratorConfigRaw
25+
? blockESLintPluginIntake(docGeneratorConfigRaw[0])
26+
: undefined;
27+
},
28+
produce({ addons, options }) {
29+
const { configEmoji } = addons;
1430
const configFileName = `.eslint-doc-generatorrc.${options.type === "commonjs" ? "mjs" : "js"}`;
1531

1632
return {
@@ -90,7 +106,7 @@ pnpm build:docs
90106
91107
/** @type {import('eslint-doc-generator').GenerateOptions} */
92108
const config = {
93-
postprocess: async (content, path) =>
109+
${configEmoji ? `configEmoji: ${JSON.stringify(configEmoji)},\n\t` : ""}postprocess: async (content, path) =>
94110
prettier.format(content, {
95111
...(await prettier.resolveConfig(path)),
96112
parser: "markdown",

Diff for: src/blocks/eslint/blockESLintPluginIntake.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
AST_NODE_TYPES,
3+
parse as parseAST,
4+
TSESTree,
5+
} from "@typescript-eslint/typescript-estree";
6+
import JSON5 from "json5";
7+
8+
import { tryCatch } from "../../utils/tryCatch.js";
9+
import { zConfigEmoji } from "./schemas.js";
10+
11+
export function blockESLintPluginIntake(sourceText: string) {
12+
const ast = tryCatch(() =>
13+
parseAST(sourceText, {
14+
comment: true,
15+
loc: true,
16+
range: true,
17+
}),
18+
);
19+
if (!ast) {
20+
return undefined;
21+
}
22+
23+
const config = findConfig(ast.body);
24+
if (!config) {
25+
return undefined;
26+
}
27+
28+
const configEmoji = findConfigEmoji(config.properties);
29+
if (!configEmoji) {
30+
return undefined;
31+
}
32+
33+
const { data } = zConfigEmoji.safeParse(
34+
JSON5.parse(sourceText.slice(...configEmoji.range)),
35+
);
36+
37+
return data && { configEmoji: data };
38+
39+
function findConfig(body: TSESTree.ProgramStatement[]) {
40+
for (const node of body) {
41+
if (
42+
node.type === AST_NODE_TYPES.VariableDeclaration &&
43+
node.declarations[0].id.type === AST_NODE_TYPES.Identifier &&
44+
node.declarations[0].id.name === "config" &&
45+
node.declarations[0].init?.type === AST_NODE_TYPES.ObjectExpression
46+
) {
47+
return node.declarations[0].init;
48+
}
49+
}
50+
}
51+
52+
function findConfigEmoji(properties: TSESTree.ObjectLiteralElement[]) {
53+
for (const node of properties) {
54+
if (
55+
node.type === AST_NODE_TYPES.Property &&
56+
node.key.type === AST_NODE_TYPES.Identifier &&
57+
node.key.name === "configEmoji" &&
58+
node.value.type === AST_NODE_TYPES.ArrayExpression
59+
) {
60+
return node.value;
61+
}
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)