@dyyz1993/create-agent 2.0.0 → 2.1.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.
- package/package.json +1 -1
- package/src/commands/create.ts +31 -31
- package/src/commands/workspace.ts +112 -105
- package/src/lib/copy.ts +24 -1
- package/templates/agent/electron/main.js +46 -0
- package/templates/agent/electron/preload.js +5 -0
- package/templates/agent/electron-builder.json +40 -0
- package/templates/agent/eslint.config.mjs +17 -2
- package/templates/agent/package.json +69 -3
- package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
- package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
- package/templates/agent/src/bun/index.ts +96 -41
- package/templates/agent/src/bun/three.d.ts +1 -0
- package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
- package/templates/agent/src/gateway/http-routes.ts +1 -1
- package/templates/agent/src/gateway/ws-handler.ts +84 -56
- package/templates/agent/src/mainview/App.tsx +29 -26
- package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
- package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
- package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
- package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
- package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
- package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
- package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
- package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
- package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
- package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
- package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
- package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/agent/src/mainview/lib/api-client.ts +179 -172
- package/templates/agent/src/mainview/main.tsx +4 -10
- package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/agent/src/server-config.ts +34 -32
- package/templates/agent/src/server.ts +42 -54
- package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
- package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
- package/templates/agent/src/shared/handlers/bash.ts +65 -65
- package/templates/agent/src/shared/handlers/chat.ts +189 -155
- package/templates/agent/src/shared/handlers/debug.ts +12 -0
- package/templates/agent/src/shared/handlers/feed.ts +31 -32
- package/templates/agent/src/shared/handlers/git.ts +286 -243
- package/templates/agent/src/shared/handlers/index.ts +1 -0
- package/templates/agent/src/shared/handlers/rules.ts +31 -31
- package/templates/agent/src/shared/handlers/todo.ts +31 -31
- package/templates/agent/src/shared/http-routes.ts +250 -0
- package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
- package/templates/agent/src/shared/lib/bash-security.ts +43 -43
- package/templates/agent/src/shared/lib/logger.ts +117 -95
- package/templates/agent/src/shared/lib/web-server.ts +128 -0
- package/templates/agent/src/shared/modules/debug.ts +8 -0
- package/templates/agent/src/shared/rpc-schema.ts +16 -3
- package/templates/agent/test-upload.txt +0 -0
- package/templates/agent/tsconfig.ipc.json +14 -0
- package/templates/agent/tsconfig.json +13 -3
- package/templates/chat/eslint.config.mjs +15 -2
- package/templates/chat/package.json +13 -2
- package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
- package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
- package/templates/chat/src/__tests__/server-config.test.ts +46 -44
- package/templates/chat/src/bun/index.ts +94 -44
- package/templates/chat/src/bun/three.d.ts +1 -0
- package/templates/chat/src/gateway/http-routes.ts +1 -1
- package/templates/chat/src/gateway/ws-handler.ts +84 -56
- package/templates/chat/src/mainview/App.tsx +6 -0
- package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
- package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
- package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
- package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
- package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/chat/src/mainview/lib/api-client.ts +180 -156
- package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
- package/templates/chat/src/mainview/main.tsx +10 -6
- package/templates/chat/src/server-config.ts +33 -31
- package/templates/chat/src/server.ts +42 -60
- package/templates/chat/src/shared/handlers/chat.ts +196 -162
- package/templates/chat/src/shared/handlers/debug.ts +12 -0
- package/templates/chat/src/shared/handlers/index.ts +1 -0
- package/templates/chat/src/shared/http-routes.ts +250 -0
- package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
- package/templates/chat/src/shared/lib/logger.ts +108 -95
- package/templates/chat/src/shared/lib/web-server.ts +128 -0
- package/templates/chat/src/shared/modules/debug.ts +8 -0
- package/templates/chat/src/shared/rpc-schema.ts +3 -2
- package/templates/chat/test-upload.txt +0 -0
- package/templates/chat/tsconfig.ipc.json +14 -0
- package/templates/chat/tsconfig.json +12 -2
- package/templates/general/eslint.config.mjs +15 -2
- package/templates/general/package.json +13 -2
- package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
- package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
- package/templates/general/src/__tests__/server-config.test.ts +46 -44
- package/templates/general/src/bun/index.ts +94 -44
- package/templates/general/src/bun/three.d.ts +1 -0
- package/templates/general/src/gateway/http-routes.ts +1 -1
- package/templates/general/src/gateway/ws-handler.ts +84 -56
- package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
- package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
- package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
- package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
- package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
- package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
- package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
- package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
- package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/general/src/mainview/lib/api-client.ts +180 -156
- package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
- package/templates/general/src/mainview/main.tsx +10 -6
- package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/general/src/server-config.ts +33 -31
- package/templates/general/src/server.ts +42 -60
- package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
- package/templates/general/src/shared/handlers/chat.ts +189 -155
- package/templates/general/src/shared/handlers/debug.ts +12 -0
- package/templates/general/src/shared/handlers/feed.ts +38 -40
- package/templates/general/src/shared/handlers/git.ts +286 -243
- package/templates/general/src/shared/handlers/index.ts +1 -0
- package/templates/general/src/shared/http-routes.ts +250 -0
- package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
- package/templates/general/src/shared/lib/logger.ts +117 -95
- package/templates/general/src/shared/lib/web-server.ts +128 -0
- package/templates/general/src/shared/modules/debug.ts +8 -0
- package/templates/general/src/shared/rpc-schema.ts +12 -2
- package/templates/general/test-upload.txt +0 -0
- package/templates/general/tsconfig.ipc.json +14 -0
- package/templates/general/tsconfig.json +12 -2
- package/templates/shared/__tests__/cors-security.test.ts +165 -0
- package/templates/shared/__tests__/file-path-security.test.ts +260 -0
- package/templates/shared/__tests__/http-routes.test.ts +174 -0
- package/templates/shared/__tests__/logger.test.ts +186 -0
- package/templates/shared/components/ErrorBoundary.tsx +59 -0
- package/templates/shared/env.d.ts +11 -0
- package/templates/shared/http-routes.ts +250 -0
- package/templates/shared/logger.ts +129 -0
- package/templates/shared/test-upload.txt +0 -0
- package/templates/shared/vite-base.config.ts +92 -0
- package/templates/shared/vitest-base.config.ts +42 -0
- package/templates/agent/eslint-plugin-rpc/index.js +0 -50
- package/templates/agent/eslint-plugin-rpc/package.json +0 -6
- package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
- package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
- package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
- package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
- package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
- package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
- package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
- package/templates/chat/eslint-plugin-rpc/index.js +0 -50
- package/templates/chat/eslint-plugin-rpc/package.json +0 -6
- package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
- package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
- package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
- package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
- package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
- package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
- package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
- package/templates/general/eslint-plugin-rpc/index.js +0 -50
- package/templates/general/eslint-plugin-rpc/package.json +0 -6
- package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
- package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
- package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
- package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
- package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
- package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
- package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { appendFile, mkdir as mkdirAsync, readdir as readdirAsync, unlink as unlinkAsync, stat as statAsync } from "fs/promises";
|
|
2
|
+
import { existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
6
|
+
|
|
7
|
+
interface LogEntry {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
module: string;
|
|
11
|
+
message: string;
|
|
12
|
+
data?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let _logDir: string | null = null;
|
|
16
|
+
let _maxAgeDays = 30;
|
|
17
|
+
|
|
18
|
+
export function configureLogDir(dir: string, options?: { maxAgeDays?: number }): void {
|
|
19
|
+
_logDir = dir;
|
|
20
|
+
if (options?.maxAgeDays !== undefined) {
|
|
21
|
+
_maxAgeDays = options.maxAgeDays;
|
|
22
|
+
}
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
cleanOldLogs(dir, _maxAgeDays);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getLogDir(): string {
|
|
30
|
+
if (!_logDir) {
|
|
31
|
+
_logDir = "logs";
|
|
32
|
+
if (!existsSync(_logDir)) {
|
|
33
|
+
mkdirSync(_logDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return _logDir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function cleanOldLogs(dir: string, maxAgeDays: number): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
if (!existsSync(dir)) return;
|
|
42
|
+
const files = await readdirAsync(dir);
|
|
43
|
+
const cutoff = Date.now() - maxAgeDays * 86400000;
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
if (!f.endsWith(".log")) continue;
|
|
46
|
+
const filePath = join(dir, f);
|
|
47
|
+
const s = await statAsync(filePath);
|
|
48
|
+
if (s.mtimeMs < cutoff) {
|
|
49
|
+
await unlinkAsync(filePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatLine(entry: LogEntry): string {
|
|
56
|
+
const base = `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.module}] ${entry.message}`;
|
|
57
|
+
return entry.data ? `${base} ${JSON.stringify(entry.data)}` : base;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let writeQueue: Promise<void> = Promise.resolve();
|
|
61
|
+
|
|
62
|
+
function writeToFile(line: string): void {
|
|
63
|
+
writeQueue = writeQueue.then(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
66
|
+
const dir = getLogDir();
|
|
67
|
+
if (!existsSync(dir)) {
|
|
68
|
+
await mkdirAsync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
await appendFile(join(dir, `${date}.log`), `${line}\n`);
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class Logger {
|
|
76
|
+
private readonly module: string;
|
|
77
|
+
|
|
78
|
+
constructor(module: string) {
|
|
79
|
+
this.module = module;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
debug(message: string, data?: Record<string, unknown>): void {
|
|
83
|
+
this.write("debug", message, data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
info(message: string, data?: Record<string, unknown>): void {
|
|
87
|
+
this.write("info", message, data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
warn(message: string, data?: Record<string, unknown>): void {
|
|
91
|
+
this.write("warn", message, data);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
error(message: string, data?: Record<string, unknown>): void {
|
|
95
|
+
this.write("error", message, data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private write(level: LogLevel, message: string, data?: Record<string, unknown>): void {
|
|
99
|
+
const entry: LogEntry = {
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
level,
|
|
102
|
+
module: this.module,
|
|
103
|
+
message,
|
|
104
|
+
...(data ? { data } : {}),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const line = formatLine(entry);
|
|
108
|
+
|
|
109
|
+
if (level === "error") {
|
|
110
|
+
console.error(line);
|
|
111
|
+
} else if (level === "warn") {
|
|
112
|
+
console.warn(line);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(line);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (process.env.NODE_ENV !== "production" || level !== "debug") {
|
|
118
|
+
writeToFile(line);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createLogger(module: string): Logger {
|
|
124
|
+
return new Logger(module);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function flushLogs(): Promise<void> {
|
|
128
|
+
return writeQueue;
|
|
129
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { readFileSync, existsSync } from "fs";
|
|
5
|
+
export function createViteConfig(dirname: string) {
|
|
6
|
+
const PORT_FILE = resolve(dirname, ".server-port");
|
|
7
|
+
|
|
8
|
+
function getBackendPort(): number {
|
|
9
|
+
if (existsSync(PORT_FILE)) {
|
|
10
|
+
try {
|
|
11
|
+
return parseInt(readFileSync(PORT_FILE, "utf-8").trim());
|
|
12
|
+
} catch {
|
|
13
|
+
/* fall through */
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return parseInt(process.env.PORT || "3100");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const backendPort = getBackendPort();
|
|
20
|
+
|
|
21
|
+
return defineConfig({
|
|
22
|
+
plugins: [react()],
|
|
23
|
+
root: "src/mainview",
|
|
24
|
+
build: {
|
|
25
|
+
outDir: "../../dist",
|
|
26
|
+
emptyOutDir: true,
|
|
27
|
+
target: "es2020",
|
|
28
|
+
cssMinify: true,
|
|
29
|
+
minify: "esbuild",
|
|
30
|
+
reportCompressedSize: true,
|
|
31
|
+
chunkSizeWarningLimit: 1000,
|
|
32
|
+
rollupOptions: {
|
|
33
|
+
output: {
|
|
34
|
+
manualChunks: {
|
|
35
|
+
"vendor-react": ["react", "react-dom"],
|
|
36
|
+
"vendor-state": ["zustand"],
|
|
37
|
+
"vendor-i18n": ["i18next", "react-i18next"],
|
|
38
|
+
},
|
|
39
|
+
compact: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
css: {
|
|
44
|
+
devSourcemap: false,
|
|
45
|
+
},
|
|
46
|
+
server: {
|
|
47
|
+
port: parseInt(process.env.VITE_PORT || "5173"),
|
|
48
|
+
strictPort: false,
|
|
49
|
+
watch: {
|
|
50
|
+
ignored: [".workspace/**"],
|
|
51
|
+
},
|
|
52
|
+
proxy: {
|
|
53
|
+
"/health": {
|
|
54
|
+
target: `http://localhost:${backendPort}`,
|
|
55
|
+
changeOrigin: true,
|
|
56
|
+
},
|
|
57
|
+
"/info": {
|
|
58
|
+
target: `http://localhost:${backendPort}`,
|
|
59
|
+
changeOrigin: true,
|
|
60
|
+
},
|
|
61
|
+
"/file": {
|
|
62
|
+
target: `http://localhost:${backendPort}`,
|
|
63
|
+
changeOrigin: true,
|
|
64
|
+
},
|
|
65
|
+
"/upload": {
|
|
66
|
+
target: `http://localhost:${backendPort}`,
|
|
67
|
+
changeOrigin: true,
|
|
68
|
+
},
|
|
69
|
+
"/api": {
|
|
70
|
+
target: `http://localhost:${backendPort}`,
|
|
71
|
+
changeOrigin: true,
|
|
72
|
+
ws: true,
|
|
73
|
+
},
|
|
74
|
+
"/ws": { target: `ws://localhost:${backendPort}`, ws: true },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
resolve: {
|
|
78
|
+
alias: {
|
|
79
|
+
"@dyyz1993/rpc-core": resolve(
|
|
80
|
+
dirname,
|
|
81
|
+
"..",
|
|
82
|
+
"..",
|
|
83
|
+
"packages",
|
|
84
|
+
"rpc-core",
|
|
85
|
+
"src",
|
|
86
|
+
"index.ts"
|
|
87
|
+
),
|
|
88
|
+
"@shared": resolve(dirname, "..", "shared"),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
|
|
5
|
+
export function createVitestConfig(dirname: string) {
|
|
6
|
+
return defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
test: {
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: "happy-dom",
|
|
11
|
+
setupFiles: ["./src/mainview/__tests__/setup.ts"],
|
|
12
|
+
include: [
|
|
13
|
+
"src/mainview/**/*.{test,spec}.{ts,tsx}",
|
|
14
|
+
"src/shared/**/*.{test,spec}.{ts,tsx}",
|
|
15
|
+
"src/__tests__/**/*.{test,spec}.{ts,tsx}",
|
|
16
|
+
"src/gateway/**/*.{test,spec}.{ts,tsx}",
|
|
17
|
+
"../shared/**/*.{test,spec}.{ts,tsx}",
|
|
18
|
+
],
|
|
19
|
+
css: false,
|
|
20
|
+
coverage: {
|
|
21
|
+
provider: "v8",
|
|
22
|
+
reporter: ["text", "lcov"],
|
|
23
|
+
include: ["src/mainview/**/*.{ts,tsx}"],
|
|
24
|
+
exclude: ["src/mainview/**/types/**", "src/mainview/**/*.d.ts", "src/mainview/main.tsx"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
resolve: {
|
|
28
|
+
alias: {
|
|
29
|
+
"@dyyz1993/rpc-core": resolve(
|
|
30
|
+
dirname,
|
|
31
|
+
"..",
|
|
32
|
+
"..",
|
|
33
|
+
"packages",
|
|
34
|
+
"rpc-core",
|
|
35
|
+
"src",
|
|
36
|
+
"index.ts"
|
|
37
|
+
),
|
|
38
|
+
"@shared": resolve(dirname, "..", "shared"),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* eslint-plugin-rpc — RPC 模块化规范 ESLint 插件(单注册架构)
|
|
3
|
-
*
|
|
4
|
-
* 规则列表:
|
|
5
|
-
* - rpc/no-bare-method : 方法名必须使用 module.action 格式
|
|
6
|
-
* - rpc/no-direct-register : server.register() 只允许在 handlers/ 目录内使用
|
|
7
|
-
* - rpc/schema-merge-only : rpc-schema.ts 禁止直接定义方法
|
|
8
|
-
* - rpc/module-file-naming : 模块文件命名、导出、方法前缀强制规范
|
|
9
|
-
* - rpc/require-typed-register : 入口文件必须导入 registerAllHandlers
|
|
10
|
-
* - rpc/require-api-client : 前端必须通过 apiClient 调用 RPC
|
|
11
|
-
*/
|
|
12
|
-
"use strict";
|
|
13
|
-
|
|
14
|
-
const noBareMethod = require("./rules/no-bare-method");
|
|
15
|
-
const noDirectRegister = require("./rules/no-direct-register");
|
|
16
|
-
const schemaMergeOnly = require("./rules/schema-merge-only");
|
|
17
|
-
const moduleFileNaming = require("./rules/module-file-naming");
|
|
18
|
-
const requireTypedRegister = require("./rules/require-typed-register");
|
|
19
|
-
const requireApiClient = require("./rules/require-api-client");
|
|
20
|
-
const noHardcodedStrings = require("./rules/no-hardcoded-strings");
|
|
21
|
-
|
|
22
|
-
module.exports = {
|
|
23
|
-
meta: {
|
|
24
|
-
name: "eslint-plugin-rpc",
|
|
25
|
-
version: "1.0.0",
|
|
26
|
-
},
|
|
27
|
-
rules: {
|
|
28
|
-
"no-bare-method": noBareMethod,
|
|
29
|
-
"no-direct-register": noDirectRegister,
|
|
30
|
-
"schema-merge-only": schemaMergeOnly,
|
|
31
|
-
"module-file-naming": moduleFileNaming,
|
|
32
|
-
"require-typed-register": requireTypedRegister,
|
|
33
|
-
"require-api-client": requireApiClient,
|
|
34
|
-
"no-hardcoded-strings": noHardcodedStrings,
|
|
35
|
-
},
|
|
36
|
-
configs: {
|
|
37
|
-
recommended: {
|
|
38
|
-
plugins: ["rpc"],
|
|
39
|
-
rules: {
|
|
40
|
-
"rpc/no-bare-method": "error",
|
|
41
|
-
"rpc/no-direct-register": "error",
|
|
42
|
-
"rpc/schema-merge-only": "error",
|
|
43
|
-
"rpc/module-file-naming": "error",
|
|
44
|
-
"rpc/require-typed-register": "error",
|
|
45
|
-
"rpc/require-api-client": "error",
|
|
46
|
-
"rpc/no-hardcoded-strings": "warn",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
};
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview 强制 RPC 模块文件的命名和导出规范
|
|
3
|
-
*/
|
|
4
|
-
"use strict";
|
|
5
|
-
|
|
6
|
-
function getModuleName(filename) {
|
|
7
|
-
const match = filename.match(/\/modules\/([^/]+)\.[jt]sx?$/);
|
|
8
|
-
return match ? match[1] : null;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
module.exports = {
|
|
12
|
-
meta: {
|
|
13
|
-
type: "problem",
|
|
14
|
-
docs: {
|
|
15
|
-
description: "RPC 模块文件必须遵循命名和导出规范",
|
|
16
|
-
category: "RPC Conventions",
|
|
17
|
-
recommended: "error",
|
|
18
|
-
},
|
|
19
|
-
messages: {
|
|
20
|
-
missingMethodsExport:
|
|
21
|
-
'模块文件 "{{file}}" 必须导出 "{{ModuleName}}Methods" 接口。',
|
|
22
|
-
methodPrefixMismatch:
|
|
23
|
-
'方法 "{{method}}" 的前缀不匹配模块 "{{module}}"。必须以 "{{module}}." 开头。',
|
|
24
|
-
eventPrefixMismatch:
|
|
25
|
-
'事件 "{{event}}" 的前缀不匹配模块 "{{module}}"。必须以 "{{module}}." 开头。',
|
|
26
|
-
},
|
|
27
|
-
schema: [],
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
create(context) {
|
|
31
|
-
const filename = context.getFilename();
|
|
32
|
-
const moduleName = getModuleName(filename);
|
|
33
|
-
if (!moduleName) return {};
|
|
34
|
-
|
|
35
|
-
const moduleNamePascal = moduleName
|
|
36
|
-
.split(/[-_]/)
|
|
37
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
38
|
-
.join("");
|
|
39
|
-
|
|
40
|
-
let hasMethodsExport = false;
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
TSInterfaceDeclaration(node) {
|
|
44
|
-
const name = node.id.name;
|
|
45
|
-
|
|
46
|
-
if (name === `${moduleNamePascal}Methods`) {
|
|
47
|
-
hasMethodsExport = true;
|
|
48
|
-
|
|
49
|
-
if (node.body && node.body.body) {
|
|
50
|
-
for (const member of node.body.body) {
|
|
51
|
-
if (member.type === "TSPropertySignature" && member.key) {
|
|
52
|
-
const key =
|
|
53
|
-
member.key.type === "Literal" ? String(member.key.value) : null;
|
|
54
|
-
if (key && !key.startsWith(`${moduleName}.`)) {
|
|
55
|
-
context.report({
|
|
56
|
-
node: member,
|
|
57
|
-
messageId: "methodPrefixMismatch",
|
|
58
|
-
data: { method: key, module: moduleName },
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (name === `${moduleNamePascal}Events`) {
|
|
67
|
-
if (node.body && node.body.body) {
|
|
68
|
-
for (const member of node.body.body) {
|
|
69
|
-
if (member.type === "TSPropertySignature" && member.key) {
|
|
70
|
-
const key =
|
|
71
|
-
member.key.type === "Literal" ? String(member.key.value) : null;
|
|
72
|
-
if (key && !key.startsWith(`${moduleName}.`)) {
|
|
73
|
-
context.report({
|
|
74
|
-
node: member,
|
|
75
|
-
messageId: "eventPrefixMismatch",
|
|
76
|
-
data: { event: key, module: moduleName },
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
"Program:exit"() {
|
|
86
|
-
if (!hasMethodsExport) {
|
|
87
|
-
context.report({
|
|
88
|
-
loc: { line: 1, column: 0 },
|
|
89
|
-
messageId: "missingMethodsExport",
|
|
90
|
-
data: {
|
|
91
|
-
file: filename.split("/").pop(),
|
|
92
|
-
ModuleName: moduleNamePascal,
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
},
|
|
99
|
-
};
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview 禁止使用裸方法名(无模块前缀)
|
|
3
|
-
*/
|
|
4
|
-
"use strict";
|
|
5
|
-
|
|
6
|
-
const METHOD_CALL_PATTERNS = [
|
|
7
|
-
{ calleePattern: /^register$/, argIndex: 0 },
|
|
8
|
-
{ calleePattern: /\.call$/, argIndex: 0 },
|
|
9
|
-
{ calleePattern: /\.subscribe$/, argIndex: 0 },
|
|
10
|
-
{ calleePattern: /\.emitEvent$/, argIndex: 0 },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
function isModuleMethodName(name) {
|
|
14
|
-
const parts = name.split(".");
|
|
15
|
-
return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
module.exports = {
|
|
19
|
-
meta: {
|
|
20
|
-
type: "problem",
|
|
21
|
-
docs: {
|
|
22
|
-
description: "RPC 方法名必须使用 module.action 格式(禁止裸方法名)",
|
|
23
|
-
category: "RPC Conventions",
|
|
24
|
-
recommended: "error",
|
|
25
|
-
},
|
|
26
|
-
messages: {
|
|
27
|
-
bareMethod:
|
|
28
|
-
'RPC 方法名 "{{name}}" 缺少模块前缀。必须使用 "module.action" 格式,如 "{{suggestion}}"。',
|
|
29
|
-
invalidFormat:
|
|
30
|
-
'RPC 方法名 "{{name}}" 格式错误。必须使用 "module.action" 格式(单一 "." 分隔)。',
|
|
31
|
-
},
|
|
32
|
-
schema: [],
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
create(context) {
|
|
36
|
-
const filename = context.getFilename();
|
|
37
|
-
|
|
38
|
-
if (!/\.[jt]sx?$/.test(filename)) return {};
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
CallExpression(node) {
|
|
42
|
-
const { callee, arguments: args } = node;
|
|
43
|
-
|
|
44
|
-
let calleeText = "";
|
|
45
|
-
if (callee.type === "Identifier") {
|
|
46
|
-
calleeText = callee.name;
|
|
47
|
-
} else if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
|
|
48
|
-
calleeText = `.${callee.property.name}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const pattern of METHOD_CALL_PATTERNS) {
|
|
52
|
-
if (!pattern.calleePattern.test(calleeText)) continue;
|
|
53
|
-
if (args.length <= pattern.argIndex) continue;
|
|
54
|
-
|
|
55
|
-
const arg = args[pattern.argIndex];
|
|
56
|
-
if (arg.type !== "Literal" || typeof arg.value !== "string") continue;
|
|
57
|
-
|
|
58
|
-
const methodName = arg.value;
|
|
59
|
-
|
|
60
|
-
if (!methodName.includes(".")) {
|
|
61
|
-
const suggestion = `module.${methodName}`;
|
|
62
|
-
context.report({
|
|
63
|
-
node: arg,
|
|
64
|
-
messageId: "bareMethod",
|
|
65
|
-
data: { name: methodName, suggestion },
|
|
66
|
-
});
|
|
67
|
-
} else if (!isModuleMethodName(methodName)) {
|
|
68
|
-
context.report({
|
|
69
|
-
node: arg,
|
|
70
|
-
messageId: "invalidFormat",
|
|
71
|
-
data: { name: methodName },
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
},
|
|
78
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview server.register() 只允许在 shared/handlers/ 目录内使用
|
|
3
|
-
*/
|
|
4
|
-
"use strict";
|
|
5
|
-
|
|
6
|
-
module.exports = {
|
|
7
|
-
meta: {
|
|
8
|
-
type: "problem",
|
|
9
|
-
docs: {
|
|
10
|
-
description:
|
|
11
|
-
"server.register() 只允许在 shared/handlers/ 目录内使用,入口文件和其他文件禁止直接调用",
|
|
12
|
-
category: "RPC Conventions",
|
|
13
|
-
recommended: "error",
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
noDirectRegisterInEntry:
|
|
17
|
-
"入口文件禁止直接调用 server.register()。请导入并使用 registerAllHandlers() 统一注册 handler。",
|
|
18
|
-
noDirectRegisterGeneral:
|
|
19
|
-
"禁止直接调用 server.register()。Handler 注册只能在 shared/handlers/ 目录内的文件中进行。",
|
|
20
|
-
},
|
|
21
|
-
schema: [],
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
create(context) {
|
|
25
|
-
const filename = context.getFilename();
|
|
26
|
-
|
|
27
|
-
if (!/src\/.*\.[jt]sx?$/.test(filename)) return {};
|
|
28
|
-
|
|
29
|
-
if (filename.includes("shared/handlers/")) return {};
|
|
30
|
-
|
|
31
|
-
const isEntryPoint =
|
|
32
|
-
filename.endsWith("bun/index.ts") || filename.endsWith("server.ts");
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
CallExpression(node) {
|
|
36
|
-
const { callee } = node;
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
callee.type === "MemberExpression" &&
|
|
40
|
-
callee.property.type === "Identifier" &&
|
|
41
|
-
callee.property.name === "register" &&
|
|
42
|
-
callee.object.type === "Identifier"
|
|
43
|
-
) {
|
|
44
|
-
const objectName = callee.object.name;
|
|
45
|
-
|
|
46
|
-
const serverVarNames = [
|
|
47
|
-
"server",
|
|
48
|
-
"rpcServer",
|
|
49
|
-
"rpc",
|
|
50
|
-
"rpc_server",
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
if (serverVarNames.includes(objectName)) {
|
|
54
|
-
context.report({
|
|
55
|
-
node,
|
|
56
|
-
messageId: isEntryPoint
|
|
57
|
-
? "noDirectRegisterInEntry"
|
|
58
|
-
: "noDirectRegisterGeneral",
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
meta: {
|
|
5
|
-
type: "suggestion",
|
|
6
|
-
docs: {
|
|
7
|
-
description: "disallow hardcoded strings in JSX (use i18n t() instead)",
|
|
8
|
-
category: "Best Practices",
|
|
9
|
-
recommended: false,
|
|
10
|
-
},
|
|
11
|
-
fixable: null,
|
|
12
|
-
schema: [
|
|
13
|
-
{
|
|
14
|
-
type: "object",
|
|
15
|
-
properties: {
|
|
16
|
-
ignoreAttributes: {
|
|
17
|
-
type: "array",
|
|
18
|
-
items: { type: "string" },
|
|
19
|
-
},
|
|
20
|
-
ignoreComponents: {
|
|
21
|
-
type: "array",
|
|
22
|
-
items: { type: "string" },
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
additionalProperties: false,
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
messages: {
|
|
29
|
-
hardcodedString:
|
|
30
|
-
'Hardcoded string "{{text}}" should use i18n translation (t()).',
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
create(context) {
|
|
35
|
-
const options = context.options[0] || {};
|
|
36
|
-
const ignoreAttributes = new Set(options.ignoreAttributes || ["className", "style", "id", "key", "ref", "type", "name", "data-testid", "viewBox", "d", "points", "x1", "x2", "y1", "y2", "strokeWidth", "strokeLinecap", "strokeLinejoin", "fill", "stroke", "xmlns"]);
|
|
37
|
-
new Set(options.ignoreComponents || []);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
JSXText(node) {
|
|
41
|
-
const text = node.value.trim();
|
|
42
|
-
if (text.length > 1 && /[a-zA-Z]{2,}/.test(text)) {
|
|
43
|
-
context.report({
|
|
44
|
-
node,
|
|
45
|
-
messageId: "hardcodedString",
|
|
46
|
-
data: { text },
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
JSXAttribute(node) {
|
|
51
|
-
if (ignoreAttributes.has(node.name.name)) return;
|
|
52
|
-
if (
|
|
53
|
-
node.value &&
|
|
54
|
-
node.value.type === "Literal" &&
|
|
55
|
-
typeof node.value.value === "string"
|
|
56
|
-
) {
|
|
57
|
-
const text = node.value.value.trim();
|
|
58
|
-
if (text.length > 3 && /[a-zA-Z]{3,}/.test(text) && !text.startsWith("var(") && !text.startsWith("bg-") && !text.startsWith("text-") && !text.startsWith("border-")) {
|
|
59
|
-
context.report({
|
|
60
|
-
node,
|
|
61
|
-
messageId: "hardcodedString",
|
|
62
|
-
data: { text },
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
};
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview 强制前端使用 apiClient 进行 RPC 调用
|
|
3
|
-
*/
|
|
4
|
-
"use strict";
|
|
5
|
-
|
|
6
|
-
module.exports = {
|
|
7
|
-
meta: {
|
|
8
|
-
type: "problem",
|
|
9
|
-
docs: {
|
|
10
|
-
description: "前端 RPC 调用必须通过 apiClient",
|
|
11
|
-
category: "RPC Conventions",
|
|
12
|
-
recommended: "error",
|
|
13
|
-
},
|
|
14
|
-
messages: {
|
|
15
|
-
useApiClient:
|
|
16
|
-
"前端 RPC 调用必须通过 apiClient.call() / apiClient.subscribe()。禁止直接操作 WebSocket 或其他传输层。",
|
|
17
|
-
},
|
|
18
|
-
schema: [],
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
create(context) {
|
|
22
|
-
const filename = context.getFilename();
|
|
23
|
-
|
|
24
|
-
if (!/mainview\/.*\.[jt]sx?$/.test(filename)) return {};
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
NewExpression(node) {
|
|
28
|
-
if (
|
|
29
|
-
node.callee.type === "Identifier" &&
|
|
30
|
-
node.callee.name === "WebSocket"
|
|
31
|
-
) {
|
|
32
|
-
context.report({
|
|
33
|
-
node,
|
|
34
|
-
messageId: "useApiClient",
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
CallExpression(node) {
|
|
40
|
-
if (
|
|
41
|
-
node.callee.type === "MemberExpression" &&
|
|
42
|
-
node.callee.property.type === "Identifier" &&
|
|
43
|
-
node.callee.property.name === "send"
|
|
44
|
-
) {
|
|
45
|
-
if (node.callee.object.type === "Identifier") {
|
|
46
|
-
const objName = node.callee.object.name.toLowerCase();
|
|
47
|
-
if (objName.includes("websocket") || objName.includes("ws")) {
|
|
48
|
-
context.report({
|
|
49
|
-
node,
|
|
50
|
-
messageId: "useApiClient",
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
};
|