startx 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.prettierrc.js +2 -2
  2. package/.vscode/settings.json +2 -1
  3. package/apps/core-server/package.json +1 -1
  4. package/apps/core-server/src/index.ts +0 -1
  5. package/apps/startx-cli/dist/index.mjs +2 -2
  6. package/apps/startx-cli/src/commands/init.ts +179 -257
  7. package/apps/startx-cli/src/configs/deps.ts +6 -1
  8. package/apps/startx-cli/src/configs/files.ts +3 -4
  9. package/apps/startx-cli/src/configs/scripts.ts +25 -7
  10. package/apps/startx-cli/src/types.ts +18 -6
  11. package/apps/startx-cli/src/utils/cli-utils.ts +60 -49
  12. package/apps/startx-cli/src/utils/file-handler.ts +14 -5
  13. package/biome.json +1 -1
  14. package/configs/eslint-config/eslint.config.ts +0 -0
  15. package/configs/eslint-config/src/configs/base.ts +32 -79
  16. package/configs/eslint-config/src/configs/extend.ts +2 -2
  17. package/configs/eslint-config/src/configs/frontend.ts +29 -19
  18. package/configs/eslint-config/src/configs/node.ts +46 -6
  19. package/configs/vitest-config/package.json +3 -2
  20. package/package.json +3 -2
  21. package/packages/@repo/db/drizzle.config.ts +14 -0
  22. package/packages/@repo/db/package.json +5 -1
  23. package/packages/@repo/db/src/index.ts +1 -1
  24. package/packages/@repo/lib/src/otp-module/index.ts +6 -13
  25. package/packages/@repo/lib/tsconfig.json +2 -1
  26. package/packages/@repo/mail/eslint.config.ts +2 -2
  27. package/packages/@repo/mail/src/emails/admin/OtpEmail.tsx +3 -9
  28. package/packages/@repo/mail/src/emails/emails.ts +1 -0
  29. package/packages/@repo/mail/src/index.ts +10 -9
  30. package/packages/@repo/mail/tsconfig.json +4 -2
  31. package/packages/ui/package.json +1 -0
  32. package/packages/ui/src/components/ui/command.tsx +5 -15
  33. package/pnpm-workspace.yaml +1 -0
  34. package/turbo.json +9 -0
  35. package/apps/startx-cli/src/utils/config.ts +0 -104
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/naming-convention */
2
-
3
1
  import type { SCRIPT } from "../types";
4
2
 
5
3
  export const scripts: SCRIPT = {
@@ -10,7 +8,7 @@ export const scripts: SCRIPT = {
10
8
  },
11
9
  {
12
10
  script: "tsx watch src/index.ts",
13
- tags: ["runnable", "node", "backend", "dev"],
11
+ tags: ["runnable", "node", "backend", "express"],
14
12
  },
15
13
  {
16
14
  script: "email dev --port 3014 --dir ./src",
@@ -24,7 +22,7 @@ export const scripts: SCRIPT = {
24
22
  },
25
23
  {
26
24
  script: "tsx watch --inspect src/index.ts",
27
- tags: ["backend", "node", "runnable", "dev"],
25
+ tags: ["backend", "node", "runnable", "express"],
28
26
  },
29
27
  ],
30
28
  "bun:dev": [
@@ -34,7 +32,7 @@ export const scripts: SCRIPT = {
34
32
  },
35
33
  {
36
34
  script: "bun --watch src/index.ts",
37
- tags: ["backend", "node", "runnable", "dev"],
35
+ tags: ["backend", "node", "runnable", "express"],
38
36
  },
39
37
  ],
40
38
  "build": [
@@ -44,7 +42,7 @@ export const scripts: SCRIPT = {
44
42
  },
45
43
  {
46
44
  script: "tsdown --config-loader unrun",
47
- tags: ["runnable", "node"],
45
+ tags: ["runnable", "node", "tsdown"],
48
46
  },
49
47
  ],
50
48
  "cli": [
@@ -69,7 +67,7 @@ export const scripts: SCRIPT = {
69
67
  ],
70
68
  "lint": [
71
69
  {
72
- script: "turbo run eslint",
70
+ script: "turbo run lint",
73
71
  tags: ["node", "eslint", "root"],
74
72
  },
75
73
  {
@@ -107,6 +105,26 @@ export const scripts: SCRIPT = {
107
105
  tags: ["node"],
108
106
  },
109
107
  ],
108
+ "db:push": [
109
+ {
110
+ script: "drizzle-kit push",
111
+ tags: ["drizzle", "db"],
112
+ },
113
+ {
114
+ script: "turbo run db:push",
115
+ tags: ["db", "root"],
116
+ },
117
+ ],
118
+ "db:studio": [
119
+ {
120
+ script: "drizzle-kit studio",
121
+ tags: ["drizzle", "db"],
122
+ },
123
+ {
124
+ script: "turbo run db:studio",
125
+ tags: ["db", "root"],
126
+ },
127
+ ],
110
128
  "typecheck": [
111
129
  {
112
130
  script: "turbo run typecheck",
@@ -2,25 +2,37 @@
2
2
  import type { PackageJson } from "type-fest";
3
3
 
4
4
  export type TAGS =
5
- | "required"
5
+ // Always installs
6
6
  | "common"
7
+ // Used for apps
7
8
  | "runnable"
9
+ // Formatter tags
8
10
  | "biome"
9
11
  | "prettier"
10
- | "vitest"
11
12
  | "eslint"
13
+ // Testing tags
14
+ | "vitest"
15
+ // Build tags
12
16
  | "tsdown"
17
+ // Backend tags
13
18
  | "node"
14
19
  | "backend"
15
- | "dev"
20
+ | "express"
21
+ // Frontend tags
16
22
  | "frontend"
23
+ | "react"
24
+ // Cli tags
17
25
  | "cli"
18
26
  | "commander"
19
- | "react"
20
- | "extra"
27
+ // Mail
21
28
  | "mail"
29
+ // Never installs or ignore
22
30
  | "never"
23
- | "root";
31
+ // For handling workspace only files and scripts
32
+ | "root"
33
+ // Database tags
34
+ | "db"
35
+ | "drizzle";
24
36
 
25
37
  export type SCRIPT = Record<string, Array<{ script: string; tags: TAGS[] }>>;
26
38
 
@@ -6,84 +6,94 @@ import z from "zod";
6
6
 
7
7
  import type { PnpmWorkspace, StartXPackageJson } from "../types";
8
8
 
9
- export type RawPackageItem = {
9
+ export type PackageItem = {
10
10
  type: "apps" | "configs" | "packages";
11
11
  path: string;
12
+ relativePath: string;
12
13
  name: string;
14
+ packageJson: StartXPackageJson;
13
15
  };
14
16
 
15
17
  const ENV = defineEnv({
16
18
  STARTX_ENV: z.enum(["development", "production", "test", "staging"]).default("production"),
17
19
  });
20
+
18
21
  export class CliUtils {
19
22
  static getDirectory() {
20
23
  const __filename = fileURLToPath(import.meta.url);
21
24
  let template = path.dirname(__filename);
22
25
  const workspace = process.cwd();
26
+
23
27
  if (ENV.STARTX_ENV === "development") {
24
28
  template = path.resolve(template, "../../../../");
25
- } else template = path.resolve(template, "../../../");
29
+ } else {
30
+ template = path.resolve(template, "../../../");
31
+ }
26
32
 
27
33
  return {
28
34
  template,
29
35
  workspace,
30
36
  };
31
37
  }
32
- static async getPackageList() {
33
- const packages: RawPackageItem[] = [];
38
+
39
+ static async getPackageList(): Promise<PackageItem[]> {
34
40
  const cliDirectory = this.getDirectory().template;
35
- const safeListDir = async (dir: string): Promise<string[]> => {
41
+
42
+ const fetchPackages = async (
43
+ subPath: string,
44
+ type: "apps" | "configs" | "packages",
45
+ namePrefix = "",
46
+ filterFn?: (name: string) => boolean
47
+ ): Promise<PackageItem[]> => {
48
+ const dirPath = path.join(cliDirectory, ...subPath.split("/"));
49
+
36
50
  try {
37
- return await fsTool.listDirectories({ dir });
51
+ let names = await fsTool.listDirectories({ dir: dirPath });
52
+ if (filterFn) names = names.filter(filterFn);
53
+
54
+ const packages = await Promise.all(
55
+ names.map(async name => {
56
+ const pkgPath = path.join(dirPath, name);
57
+ const relativePath = path.relative(cliDirectory, pkgPath);
58
+ const pkgName = namePrefix ? `${namePrefix}${name}` : name;
59
+ let packageJson;
60
+
61
+ try {
62
+ packageJson = await this.parsePackageJson({ dir: pkgPath });
63
+ } catch {
64
+ packageJson = null;
65
+ }
66
+
67
+ if (!packageJson) {
68
+ console.error(`Ignoring this package failed to read package.json: ${pkgName}`);
69
+ return null;
70
+ }
71
+
72
+ return {
73
+ type,
74
+ path: pkgPath,
75
+ relativePath,
76
+ name: pkgName,
77
+ packageJson,
78
+ };
79
+ })
80
+ );
81
+
82
+ return packages.filter((pkg): pkg is PackageItem => pkg !== null);
38
83
  } catch (error) {
39
- console.error("Error listing directory:", error);
84
+ console.error(`Error reading directory ${dirPath}:`, error);
40
85
  return [];
41
86
  }
42
87
  };
43
88
 
44
- const [availableApps, availableConfigs, availablePackages, availableRepoPackages] =
45
- await Promise.all([
46
- safeListDir(path.join(cliDirectory, "apps")),
47
- safeListDir(path.join(cliDirectory, "configs")),
48
- safeListDir(path.join(cliDirectory, "packages")),
49
- safeListDir(path.join(cliDirectory, "packages", "@repo")),
50
- ]);
51
-
52
- // 2. Map Apps
53
- availableApps.forEach(name => {
54
- packages.push({
55
- type: "apps",
56
- path: path.join(cliDirectory, "apps", name),
57
- name,
58
- });
59
- });
89
+ const results = await Promise.all([
90
+ fetchPackages("apps", "apps"),
91
+ fetchPackages("configs", "configs"),
92
+ fetchPackages("packages", "packages", "", name => name !== "@repo"),
93
+ fetchPackages("packages/@repo", "packages", "@repo/"),
94
+ ]);
60
95
 
61
- availableConfigs.forEach(name => {
62
- packages.push({
63
- type: "configs",
64
- path: path.join(cliDirectory, "configs", name),
65
- name,
66
- });
67
- });
68
-
69
- availablePackages.forEach(name => {
70
- if (name === "@repo") return;
71
- packages.push({
72
- type: "packages",
73
- path: path.join(cliDirectory, "packages", name),
74
- name,
75
- });
76
- });
77
-
78
- availableRepoPackages.forEach(name => {
79
- packages.push({
80
- type: "packages",
81
- path: path.join(cliDirectory, "packages", "@repo", name),
82
- name: `@repo/${name}`,
83
- });
84
- });
85
-
86
- return packages;
96
+ return results.flat();
87
97
  }
88
98
 
89
99
  static async parsePackageJson({ dir, file = "package" }: { dir: string; file?: string }) {
@@ -93,6 +103,7 @@ export class CliUtils {
93
103
  });
94
104
  return packageJson;
95
105
  }
106
+
96
107
  static async parsePnpmWorkspace({ dir }: { dir: string }) {
97
108
  const yaml = await fsTool.readYamlFile({
98
109
  file: "pnpm-workspace",
@@ -5,9 +5,7 @@ import type { StartXPackageJson, TAGS } from "../types";
5
5
 
6
6
  export class FileHandler {
7
7
  private static objSorter(obj: Record<string, unknown>, sorter: string[] = []) {
8
- const cleaned = Object.fromEntries(
9
- Object.entries(obj).filter(([, v]) => v !== null && v !== undefined)
10
- );
8
+ const cleaned = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== null && v !== undefined));
11
9
 
12
10
  const sortedEntries: Array<[string, unknown]> = [];
13
11
 
@@ -63,13 +61,20 @@ export class FileHandler {
63
61
  const devDependencies = filterDeps(props.app.devDependencies as Record<string, string>);
64
62
 
65
63
  // Removing all workspace dependencies
64
+ for (const [key, value] of Object.entries(dependencies)) {
65
+ if (value.includes("workspace:")) {
66
+ delete dependencies[key];
67
+ }
68
+ }
69
+
70
+ // Removing all workspace dev dependencies
66
71
  for (const [key, value] of Object.entries(devDependencies)) {
67
72
  if (value.includes("workspace:")) {
68
73
  delete devDependencies[key];
69
74
  }
70
75
  }
71
76
 
72
- // Adding required
77
+ // Adding props required dependencies
73
78
  if (props.dependencies) {
74
79
  for (const [key, value] of Object.entries(props.dependencies)) {
75
80
  if (!dependencies[key]) {
@@ -78,6 +83,10 @@ export class FileHandler {
78
83
  }
79
84
  }
80
85
 
86
+ // Adding required dev & devDeps
87
+ props.app.startx?.requiredDevDeps?.forEach(e => (devDependencies[e] = "workspace:^"));
88
+ props.app.startx?.requiredDeps?.forEach(e => (dependencies[e] = "workspace:^"));
89
+
81
90
  // Adding rest
82
91
  for (const [key, value] of Object.entries(DepCheck)) {
83
92
  if (!value.tags.every(tag => tags.includes(tag))) continue;
@@ -85,7 +94,7 @@ export class FileHandler {
85
94
  const isDev = value.isDevDependency;
86
95
  if (isDev && !devDependencies[key]) {
87
96
  devDependencies[key] = value.version;
88
- } else if (!dependencies[key]) {
97
+ } else if (!isDev && !dependencies[key]) {
89
98
  dependencies[key] = value.version;
90
99
  }
91
100
  }
package/biome.json CHANGED
@@ -28,7 +28,7 @@
28
28
  "indentStyle": "tab",
29
29
  "indentWidth": 2,
30
30
  "lineEnding": "lf",
31
- "lineWidth": 100,
31
+ "lineWidth": 120,
32
32
  "attributePosition": "auto"
33
33
  },
34
34
  "assist": { "actions": { "source": { "organizeImports": "off" } } },
File without changes
@@ -1,20 +1,15 @@
1
1
  import { fixupPluginRules } from "@eslint/compat";
2
- import js from "@eslint/js";
3
- import stylisticPlugin from "@stylistic/eslint-plugin";
4
- import { defineConfig } from "eslint/config";
5
2
  import eslintConfigPrettier from "eslint-config-prettier";
6
- import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
7
3
  import importPlugin from "eslint-plugin-import-x";
8
4
  import lodashPlugin from "eslint-plugin-lodash";
9
- import reactPlugin from "eslint-plugin-react";
10
- import reactHooksPlugin from "eslint-plugin-react-hooks";
11
5
  import unicornPlugin from "eslint-plugin-unicorn";
12
6
  import unusedImportsPlugin from "eslint-plugin-unused-imports";
13
7
  import globals from "globals";
14
8
  import tseslint from "typescript-eslint";
15
9
  import { localRulesPlugin } from "../plugin.js";
16
10
 
17
- export const baseConfig = defineConfig(
11
+ // Exported as a standard array for Flat Config
12
+ export const baseConfig = tseslint.config(
18
13
  // 1. Global Ignores
19
14
  {
20
15
  ignores: [
@@ -29,11 +24,11 @@ export const baseConfig = defineConfig(
29
24
  "vite.config.ts",
30
25
  "vitest.config.ts",
31
26
  "tsdown.config.ts",
27
+ "drizzle.config.ts",
32
28
  ],
33
29
  },
34
30
 
35
31
  // 2. Base Configurations
36
- js.configs.recommended,
37
32
  ...tseslint.configs.recommended,
38
33
  ...tseslint.configs.recommendedTypeChecked,
39
34
  importPlugin.flatConfigs.recommended as any,
@@ -43,17 +38,14 @@ export const baseConfig = defineConfig(
43
38
  {
44
39
  plugins: {
45
40
  "unused-imports": fixupPluginRules(unusedImportsPlugin),
46
- "react-hooks": fixupPluginRules(reactHooksPlugin as any),
47
41
  lodash: fixupPluginRules(lodashPlugin),
48
- "@stylistic": stylisticPlugin as any,
49
42
  unicorn: unicornPlugin,
50
- react: reactPlugin,
43
+ "local-rules": localRulesPlugin,
51
44
  },
52
45
  languageOptions: {
53
- ecmaVersion: 2022,
46
+ ecmaVersion: "latest",
54
47
  sourceType: "module",
55
48
  globals: {
56
- ...globals.browser,
57
49
  ...globals.node,
58
50
  ...globals.es2021,
59
51
  },
@@ -62,25 +54,16 @@ export const baseConfig = defineConfig(
62
54
  tsconfigRootDir: import.meta.dirname,
63
55
  },
64
56
  },
65
- settings: {
66
- "import-x/resolver-next": [createTypeScriptImportResolver()],
67
- react: {
68
- version: "detect",
69
- },
70
- },
71
57
  rules: {
72
58
  // Core Rules
73
59
  "no-void": ["error", { allowAsStatement: true }],
74
60
  "no-constant-binary-expression": "error",
75
- "no-console": ["warn", { allow: ["warn", "error"] }],
76
- eqeqeq: "warn",
61
+ "no-console": ["warn", { allow: ["warn", "error", "info"] }], // Allowed info for debugging
62
+ eqeqeq: ["warn", "always", { null: "ignore" }],
77
63
  "object-shorthand": "warn",
78
64
  "prefer-const": "warn",
79
65
 
80
- "id-denylist": "off",
81
- "import-x/no-named-as-default": "off",
82
-
83
- // TypeScript
66
+ // TypeScript Quality of Life
84
67
  "@typescript-eslint/await-thenable": "error",
85
68
  "@typescript-eslint/no-floating-promises": ["error", { ignoreVoid: true }],
86
69
  "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
@@ -90,75 +73,60 @@ export const baseConfig = defineConfig(
90
73
  "@typescript-eslint/no-unsafe-argument": "warn",
91
74
  "@typescript-eslint/no-unsafe-call": "warn",
92
75
  "@typescript-eslint/no-unsafe-return": "off",
93
- "@typescript-eslint/no-unsafe-member-access": ["error", { "allowOptionalChaining": true }],
76
+ "@typescript-eslint/no-unsafe-member-access": ["warn", { allowOptionalChaining: true }],
94
77
  "@typescript-eslint/array-type": ["warn", { default: "array-simple" }],
95
- "@typescript-eslint/consistent-type-assertions": "warn",
96
- "@typescript-eslint/consistent-type-imports": "warn",
97
- "@typescript-eslint/consistent-type-exports": "warn",
98
78
  "@typescript-eslint/return-await": ["warn", "always"],
79
+ "@typescript-eslint/no-empty-object-type": ["warn"],
99
80
 
100
- "@typescript-eslint/explicit-member-accessibility": "off",
101
-
81
+ // Naming Conventions (Relaxed for APIs and strict for standard code)
102
82
  "@typescript-eslint/naming-convention": [
103
83
  "warn",
104
84
  { selector: "default", format: ["camelCase"] },
105
-
106
85
  { selector: "import", format: ["camelCase", "PascalCase"] },
107
-
108
86
  {
109
87
  selector: "variable",
110
88
  format: ["camelCase", "snake_case", "UPPER_CASE", "PascalCase"],
111
- leadingUnderscore: "allowSingleOrDouble",
112
- trailingUnderscore: "allowSingleOrDouble",
89
+ leadingUnderscore: "allow",
113
90
  },
114
-
115
91
  {
116
92
  selector: "parameter",
117
93
  format: ["camelCase"],
118
- leadingUnderscore: "allowSingleOrDouble",
94
+ leadingUnderscore: "allow",
119
95
  },
120
-
121
96
  { selector: "typeLike", format: ["PascalCase"] },
122
- { selector: "enumMember", format: ["UPPER_CASE", "PascalCase"] },
123
- ],
124
- "@typescript-eslint/no-restricted-types": [
125
- "warn",
126
97
  {
127
- types: {
128
- Object: { message: "Use object instead", fixWith: "object" },
129
- String: { message: "Use string instead", fixWith: "string" },
130
- Boolean: { message: "Use boolean instead", fixWith: "boolean" },
131
- Number: { message: "Use number instead", fixWith: "number" },
132
- Symbol: { message: "Use symbol instead", fixWith: "symbol" },
133
- },
98
+ selector: ["objectLiteralProperty", "typeProperty"],
99
+ format: null, // Critical: Allows API payloads with snake_case or headers
134
100
  },
135
101
  ],
136
- "@typescript-eslint/no-unused-vars": "warn",
137
- "@typescript-eslint/no-empty-object-type": "warn",
138
- // Stylistic
139
- "@stylistic/member-delimiter-style": [
102
+
103
+ // Handled entirely by unused-imports
104
+ "@typescript-eslint/no-unused-vars": "off",
105
+ "unused-imports/no-unused-imports": "warn",
106
+ "unused-imports/no-unused-vars": [
140
107
  "warn",
141
108
  {
142
- multiline: { delimiter: "semi", requireLast: true },
143
- singleline: { delimiter: "semi", requireLast: false },
109
+ vars: "all",
110
+ varsIgnorePattern: "^_",
111
+ args: "after-used",
112
+ argsIgnorePattern: "^_",
144
113
  },
145
114
  ],
146
115
 
116
+ // Imports
147
117
  "import-x/no-cycle": "off",
148
- "import-x/no-default-export": "off",
149
-
118
+ "import-x/no-named-as-default": "off",
150
119
  "import-x/no-duplicates": "warn",
151
120
  "import-x/order": [
152
121
  "warn",
153
122
  {
154
123
  alphabetize: { order: "asc", caseInsensitive: true },
155
124
  groups: [["builtin", "external"], "internal", ["parent", "index", "sibling"], "object"],
156
- "newlines-between": "always",
125
+ // "newlines-between": "always",
157
126
  },
158
127
  ],
159
128
 
160
129
  // Plugins
161
- "unused-imports/no-unused-imports": "warn",
162
130
  "unicorn/no-unnecessary-await": "warn",
163
131
  "unicorn/no-useless-promise-resolve-reject": "warn",
164
132
  "lodash/path-style": ["warn", "as-needed"],
@@ -166,32 +134,17 @@ export const baseConfig = defineConfig(
166
134
  },
167
135
  },
168
136
 
169
- // 4. React Specific Overrides
137
+ // 4. Test Overrides
170
138
  {
171
- files: ["**/*.tsx", "**/*.jsx"],
172
- ...reactPlugin.configs.flat.recommended,
173
- ...reactPlugin.configs.flat["jsx-runtime"],
174
- rules: {
175
- ...reactHooksPlugin.configs.recommended.rules,
176
- "react/prop-types": "off",
177
- "react/react-in-jsx-scope": "off",
178
- "react/jsx-no-leaked-render": "error", // Security/Perf rule (Keep as error)
179
- },
180
- },
181
-
182
- // 5. Test Overrides
183
- {
184
- files: ["test/**/*.ts", "**/__tests__/*.ts", "**/*.test.ts", "**/*.cy.ts"],
139
+ files: ["test/**/*.ts", "**/__tests__/*.ts", "**/*.test.ts", "**/*.cy.ts", "**/*.spec.ts"],
185
140
  rules: {
186
141
  "local-rules/no-plain-errors": "off",
187
142
  "@typescript-eslint/unbound-method": "off",
188
- "import-x/no-default-export": "off",
143
+ "@typescript-eslint/no-unsafe-call": "off",
144
+ "@typescript-eslint/no-unsafe-member-access": "off",
189
145
  },
190
146
  },
191
147
 
192
- // 6. Local Rules
193
- localRulesPlugin.configs.recommended,
194
-
195
- // 7. Prettier (Must be last to cleanly override stylistic rules)
148
+ // 5. Prettier (Must be absolutely last)
196
149
  eslintConfigPrettier
197
150
  );
@@ -1,3 +1,3 @@
1
- import { defineConfig } from "eslint/config";
1
+ import tseslint from "typescript-eslint";
2
2
 
3
- export const extend = defineConfig;
3
+ export const extend = tseslint.config;
@@ -1,23 +1,21 @@
1
1
  import { fixupPluginRules } from "@eslint/compat";
2
- import { defineConfig } from "eslint/config";
2
+ import eslintConfigPrettier from "eslint-config-prettier";
3
3
  import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
4
4
  import reactPlugin from "eslint-plugin-react";
5
5
  import reactHooksPlugin from "eslint-plugin-react-hooks";
6
6
  import globals from "globals";
7
+ import tseslint from "typescript-eslint";
8
+
7
9
  import { baseConfig } from "./base.js";
8
- export const frontendConfig = defineConfig(
9
- ...baseConfig,
10
+
11
+ const baseWithoutPrettier = baseConfig.filter(config => config !== eslintConfigPrettier);
12
+
13
+ export const frontendConfig = tseslint.config(
14
+ ...baseWithoutPrettier,
10
15
 
11
16
  // 1. Frontend Ignores
12
17
  {
13
- ignores: [
14
- "**/dist/**",
15
- "**/coverage/**",
16
- "**/storybook-static/**",
17
- "**/*.snap",
18
- "**/*.d.ts",
19
- "vite.config.ts",
20
- ],
18
+ ignores: ["**/coverage/**", "**/storybook-static/**", "**/*.snap", "**/*.d.ts"],
21
19
  },
22
20
 
23
21
  // 2. Browser/React Globals & Settings
@@ -37,8 +35,8 @@ export const frontendConfig = defineConfig(
37
35
  },
38
36
 
39
37
  // 3. React Recommended Configs
40
- reactPlugin.configs.flat.recommended,
41
- reactPlugin.configs.flat["jsx-runtime"],
38
+ reactPlugin.configs.flat.recommended as any,
39
+ reactPlugin.configs.flat["jsx-runtime"] as any,
42
40
 
43
41
  // 4. React, Hooks, and Accessibility Rules
44
42
  {
@@ -52,6 +50,7 @@ export const frontendConfig = defineConfig(
52
50
  ...jsxA11yPlugin.configs.recommended.rules,
53
51
 
54
52
  // --- Naming Convention Override for React ---
53
+ // Allows PascalCase for functional components while keeping variables camelCase
55
54
  "@typescript-eslint/naming-convention": [
56
55
  "warn",
57
56
  { selector: "default", format: ["camelCase"] },
@@ -60,17 +59,18 @@ export const frontendConfig = defineConfig(
60
59
  {
61
60
  selector: "variable",
62
61
  format: ["camelCase", "snake_case", "UPPER_CASE", "PascalCase"],
63
- leadingUnderscore: "allowSingleOrDouble",
64
- trailingUnderscore: "allowSingleOrDouble",
62
+ leadingUnderscore: "allow",
65
63
  },
66
64
  { selector: "typeLike", format: ["PascalCase"] },
67
- { selector: "enumMember", format: ["UPPER_CASE", "PascalCase"] },
68
65
  ],
69
66
 
70
67
  // --- React Specifics ---
71
- "react/prop-types": "off",
68
+ "react/prop-types": "off", // Using TS interfaces instead
69
+ "react/react-in-jsx-scope": "off", // Not needed for React 17+
70
+ "react/jsx-uses-react": "off", // Not needed for React 17+
72
71
  "react/display-name": "warn",
73
72
  "react/no-unescaped-entities": "warn",
73
+ "react/jsx-no-leaked-render": ["error", { validStrategies: ["ternary", "coerce"] }],
74
74
 
75
75
  // --- React Hooks Specifics ---
76
76
  "react-hooks/rules-of-hooks": "error",
@@ -81,7 +81,17 @@ export const frontendConfig = defineConfig(
81
81
  "jsx-a11y/click-events-have-key-events": "warn",
82
82
  "jsx-a11y/no-static-element-interactions": "warn",
83
83
  "jsx-a11y/anchor-is-valid": "warn",
84
- "jsx-a11y/no-onchange": "off",
84
+ "jsx-a11y/no-onchange": "off", // Deprecated rule, fine to turn off
85
+ },
86
+ },
87
+
88
+ {
89
+ files: ["**/emails/**/*.tsx", "**/emails/**/*.jsx"],
90
+ rules: {
91
+ "react/react-in-jsx-scope": "error",
92
+ "react/jsx-uses-react": "error",
85
93
  },
86
- }
94
+ },
95
+
96
+ eslintConfigPrettier
87
97
  );