diff --git a/.gitignore b/.gitignore
index 8d79ba4..6e62ea5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,6 @@ node_modules
# build folder
dist
# misc
+.vscode
.DS_Store
*.tgz
\ No newline at end of file
diff --git a/lib/rules/prefer-title-case.ts b/lib/rules/prefer-title-case.ts
index 83e9cd0..0697bec 100644
--- a/lib/rules/prefer-title-case.ts
+++ b/lib/rules/prefer-title-case.ts
@@ -3,39 +3,36 @@ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
import { isActionComponent, createRule } from "../utils";
import { titleCase } from "../title-case";
-export default createRule({
- create: (context) => {
+interface PreferTitleCaseOptions {
+ extraFixedCaseWords?: string[];
+}
+
+export default createRule<[PreferTitleCaseOptions], "isNotTitleCased">({
+ create: (context, [options = {}]) => {
+ const extraFixedCaseWords = options.extraFixedCaseWords ?? [];
return {
JSXOpeningElement: (node) => {
if (isActionComponent(node.name)) {
const titleAttribute = node.attributes.find((attribute) => {
- return (
- attribute.type === "JSXAttribute" &&
- attribute.name.name === "title"
- );
+ return attribute.type === "JSXAttribute" && attribute.name.name === "title";
});
if (titleAttribute) {
- const value =
- titleAttribute.type === "JSXAttribute" && titleAttribute.value;
+ const value = titleAttribute.type === "JSXAttribute" && titleAttribute.value;
// In the case of a simple string
//
if (value) {
- if (
- value.type === AST_NODE_TYPES.Literal &&
- typeof value.value === "string"
- ) {
+ if (value.type === AST_NODE_TYPES.Literal && typeof value.value === "string") {
const originalTitle = value.value;
- const formattedTitle = titleCase(originalTitle);
+ const formattedTitle = titleCase(originalTitle, extraFixedCaseWords);
if (originalTitle !== formattedTitle) {
context.report({
node: value,
messageId: "isNotTitleCased",
- fix: (fixer) =>
- fixer.replaceText(value, `"${formattedTitle}"`),
+ fix: (fixer) => fixer.replaceText(value, `"${formattedTitle}"`),
});
}
}
@@ -51,14 +48,13 @@ export default createRule({
expression.value &&
typeof expression.value === "string"
) {
- const formattedTitle = titleCase(expression.value);
+ const formattedTitle = titleCase(expression.value, extraFixedCaseWords);
if (expression.value !== formattedTitle) {
context.report({
node: expression,
messageId: "isNotTitleCased",
- fix: (fixer) =>
- fixer.replaceText(expression, `"${formattedTitle}"`),
+ fix: (fixer) => fixer.replaceText(expression, `"${formattedTitle}"`),
});
}
}
@@ -74,14 +70,13 @@ export default createRule({
consequent.value &&
typeof consequent.value === "string"
) {
- const formattedTitle = titleCase(consequent.value);
+ const formattedTitle = titleCase(consequent.value, extraFixedCaseWords);
if (consequent.value !== formattedTitle) {
context.report({
node: consequent,
messageId: "isNotTitleCased",
- fix: (fixer) =>
- fixer.replaceText(consequent, `"${formattedTitle}"`),
+ fix: (fixer) => fixer.replaceText(consequent, `"${formattedTitle}"`),
});
}
}
@@ -91,14 +86,13 @@ export default createRule({
alternate.value &&
typeof alternate.value === "string"
) {
- const formattedTitle = titleCase(alternate.value);
+ const formattedTitle = titleCase(alternate.value, extraFixedCaseWords);
if (alternate.value !== formattedTitle) {
context.report({
node: alternate,
messageId: "isNotTitleCased",
- fix: (fixer) =>
- fixer.replaceText(alternate, `"${formattedTitle}"`),
+ fix: (fixer) => fixer.replaceText(alternate, `"${formattedTitle}"`),
});
}
}
@@ -119,7 +113,7 @@ export default createRule({
quasi.value &&
typeof quasi.value.raw === "string"
) {
- const formattedTitle = titleCase(quasi.value.raw);
+ const formattedTitle = titleCase(quasi.value.raw, extraFixedCaseWords);
if (quasi.value.raw !== formattedTitle) {
hasQuasiWithoutTitleCase = true;
@@ -152,7 +146,21 @@ export default createRule({
docs: {
description: "Prefer Title Case",
},
- schema: [],
+ schema: [
+ {
+ type: "object",
+ properties: {
+ extraFixedCaseWords: {
+ type: "array",
+ items: {
+ type: "string",
+ },
+ uniqueItems: true,
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
},
- defaultOptions: [],
+ defaultOptions: [{}],
});
diff --git a/lib/title-case.ts b/lib/title-case.ts
index 2962c75..0875954 100644
--- a/lib/title-case.ts
+++ b/lib/title-case.ts
@@ -15,7 +15,7 @@
// - The second word in a hyphenated compound (except for `Built-in` and `Plug-in`)
// - High-Level Events
// - 32-Bit Addressing
-export function titleCase(s: string): string {
+export function titleCase(s: string, extraFixedCaseWords?: string[]): string {
// Define the words that should not be capitalized unless they are the first or last word in the title or follow a colon.
const noCaps: { [key: string]: boolean } = {
and: true,
@@ -73,7 +73,7 @@ export function titleCase(s: string): string {
"plug-in": "Plug-in",
"pub.dev": "pub.dev",
"ray.so": "ray.so",
- "servicenow": "ServiceNow",
+ servicenow: "ServiceNow",
svg: "SVG",
totp: "TOTP",
url: "URL",
@@ -83,6 +83,13 @@ export function titleCase(s: string): string {
xkcd: "xkcd",
};
+ // Merge user-provided extra fixed case words and built-in words
+ // Built-in words have higher priority
+ const allFixedCaseWords = {
+ ...Object.fromEntries((extraFixedCaseWords || []).map((word) => [word.toLowerCase(), word])),
+ ...fixedCaseWords,
+ };
+
// Replace all instances of '...' with '…'
s = s.replace(/\.\.\./g, "…");
@@ -107,17 +114,13 @@ export function titleCase(s: string): string {
const lowerWord = word.toLowerCase();
const ok = noCaps[lowerWord];
const isArticle = articles[lowerWord];
- const fixedCase = fixedCaseWords[lowerWord];
+ const fixedCase = allFixedCaseWords[lowerWord];
if (fixedCase) {
words[i] = fixedCase;
} else if (word.startsWith("http://") || word.startsWith("https://")) {
words[i] = word;
- } else if (
- (!ok && !isArticle) ||
- i === 0 ||
- (isArticle && words[i - 1].endsWith(":"))
- ) {
+ } else if ((!ok && !isArticle) || i === 0 || (isArticle && words[i - 1].endsWith(":"))) {
words[i] = word
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
diff --git a/package-lock.json b/package-lock.json
index 318276f..c8d2e21 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@raycast/eslint-plugin",
- "version": "2.0.6",
+ "version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@raycast/eslint-plugin",
- "version": "2.0.6",
+ "version": "2.1.0",
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.26.1"
diff --git a/package.json b/package.json
index 936b849..85977dc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@raycast/eslint-plugin",
- "version": "2.0.7",
+ "version": "2.1.0",
"description": "ESLint plugin designed to help Raycast's extensions authors follow best practices",
"author": "Raycast Technologies Ltd.",
"keywords": [
diff --git a/tests/prefer-title-case.test.ts b/tests/prefer-title-case.test.ts
index 68cdd59..875b1ff 100644
--- a/tests/prefer-title-case.test.ts
+++ b/tests/prefer-title-case.test.ts
@@ -62,6 +62,37 @@ ruleTester.run("prefer-title-case", rule, {
`,
},
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ },
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ },
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ },
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ },
+ // Built-in words have higher priority than user-provided words
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["github", "DevTool"] }],
+ },
],
invalid: [
{
@@ -176,10 +207,7 @@ ruleTester.run("prefer-title-case", rule, {
code: `
`,
- errors: [
- { messageId: "isNotTitleCased" },
- { messageId: "isNotTitleCased" },
- ],
+ errors: [{ messageId: "isNotTitleCased" }, { messageId: "isNotTitleCased" }],
output: `
`,
@@ -196,5 +224,42 @@ ruleTester.run("prefer-title-case", rule, {
code: "",
errors: [{ messageId: "isNotTitleCased" }],
},
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp"] }],
+ errors: [{ messageId: "isNotTitleCased" }],
+ output: `
+
+ `,
+ },
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ errors: [{ messageId: "isNotTitleCased" }],
+ output: `
+
+ `,
+ },
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ errors: [{ messageId: "isNotTitleCased" }, { messageId: "isNotTitleCased" }],
+ output: `
+
+ `,
+ },
+ {
+ code: `
+
+ `,
+ options: [{ extraFixedCaseWords: ["MyApp", "DevTool"] }],
+ errors: [{ messageId: "isNotTitleCased" }],
+ },
],
});