create-caspian-app 0.0.1

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.
@@ -0,0 +1,173 @@
1
+ import { spawn, ChildProcess } from "child_process";
2
+ import { platform } from "os";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { Socket } from "net";
6
+
7
+ let pythonProcess: ChildProcess | null = null;
8
+ let isRestarting = false;
9
+
10
+ function isWindows(): boolean {
11
+ return platform() === "win32";
12
+ }
13
+
14
+ function getVenvPythonPath(): string {
15
+ const venvPython = isWindows()
16
+ ? join(".venv", "Scripts", "python.exe")
17
+ : join(".venv", "bin", "python");
18
+
19
+ if (!existsSync(venvPython)) {
20
+ console.warn(`⚠ Virtual environment not found, using system python`);
21
+ return isWindows() ? "python" : "python3";
22
+ }
23
+ return venvPython;
24
+ }
25
+
26
+ export function waitForPort(port: number, timeout = 10000): Promise<boolean> {
27
+ const start = Date.now();
28
+ return new Promise((resolve) => {
29
+ const check = () => {
30
+ if (Date.now() - start > timeout) {
31
+ resolve(false);
32
+ return;
33
+ }
34
+ const socket = new Socket();
35
+ socket.setTimeout(200);
36
+ socket.on("connect", () => {
37
+ socket.destroy();
38
+ resolve(true);
39
+ });
40
+ socket.on("timeout", () => {
41
+ socket.destroy();
42
+ setTimeout(check, 100);
43
+ });
44
+ socket.on("error", () => {
45
+ socket.destroy();
46
+ setTimeout(check, 100);
47
+ });
48
+ socket.connect(port, "127.0.0.1");
49
+ };
50
+ check();
51
+ });
52
+ }
53
+
54
+ export function waitForPortRelease(
55
+ port: number,
56
+ timeout = 5000
57
+ ): Promise<boolean> {
58
+ const start = Date.now();
59
+ return new Promise((resolve) => {
60
+ const check = () => {
61
+ if (Date.now() - start > timeout) {
62
+ resolve(false);
63
+ return;
64
+ }
65
+ const socket = new Socket();
66
+ socket.setTimeout(200);
67
+ socket.on("connect", () => {
68
+ socket.destroy();
69
+ setTimeout(check, 100);
70
+ });
71
+ socket.on("timeout", () => {
72
+ socket.destroy();
73
+ resolve(true);
74
+ });
75
+ socket.on("error", (err: any) => {
76
+ socket.destroy();
77
+ if (err.code === "ECONNREFUSED") {
78
+ resolve(true);
79
+ } else {
80
+ setTimeout(check, 100);
81
+ }
82
+ });
83
+ socket.connect(port, "127.0.0.1");
84
+ };
85
+ check();
86
+ });
87
+ }
88
+
89
+ async function killProcessTree(child: ChildProcess): Promise<void> {
90
+ if (!child || child.exitCode !== null) return;
91
+ const pid = child.pid;
92
+ if (!pid) return;
93
+
94
+ if (isWindows()) {
95
+ try {
96
+ await new Promise<void>((resolve) => {
97
+ const killer = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
98
+ stdio: "ignore",
99
+ shell: true,
100
+ windowsHide: true,
101
+ });
102
+ killer.on("exit", () => resolve());
103
+ killer.on("error", () => resolve());
104
+ });
105
+ } catch (e) {}
106
+ } else {
107
+ try {
108
+ process.kill(-pid, "SIGKILL");
109
+ } catch {
110
+ try {
111
+ child.kill("SIGKILL");
112
+ } catch {}
113
+ }
114
+ }
115
+ }
116
+
117
+ function spawnPython(port: number): ChildProcess {
118
+ const pythonPath = getVenvPythonPath();
119
+ const args = ["-u", "main.py"];
120
+
121
+ console.log(`→ Starting Python server on port ${port}...`);
122
+
123
+ const child = spawn(pythonPath, args, {
124
+ stdio: "inherit",
125
+ shell: false,
126
+ detached: !isWindows(),
127
+ env: { ...process.env, PYTHONUNBUFFERED: "1", PORT: String(port) },
128
+ });
129
+
130
+ child.on("error", (err) => console.error("Failed to start Python:", err));
131
+ return child;
132
+ }
133
+
134
+ export function startPythonServer(port: number): void {
135
+ if (pythonProcess && pythonProcess.exitCode === null) return;
136
+ pythonProcess = spawnPython(port);
137
+ }
138
+
139
+ export async function restartPythonServer(port: number): Promise<void> {
140
+ if (isRestarting) return;
141
+ isRestarting = true;
142
+
143
+ try {
144
+ console.log("→ Restarting Python server...");
145
+ const prev = pythonProcess;
146
+ pythonProcess = null;
147
+
148
+ if (prev) {
149
+ await killProcessTree(prev);
150
+ await waitForPortRelease(port);
151
+ }
152
+
153
+ pythonProcess = spawnPython(port);
154
+ } finally {
155
+ isRestarting = false;
156
+ }
157
+ }
158
+
159
+ export function stopPythonServer(): void {
160
+ const prev = pythonProcess;
161
+ pythonProcess = null;
162
+ if (prev) killProcessTree(prev);
163
+ }
164
+
165
+ process.on("exit", () => stopPythonServer());
166
+ process.on("SIGINT", () => {
167
+ stopPythonServer();
168
+ process.exit(0);
169
+ });
170
+ process.on("SIGTERM", () => {
171
+ stopPythonServer();
172
+ process.exit(0);
173
+ });
@@ -0,0 +1,239 @@
1
+ import { fileURLToPath } from "url";
2
+ import { dirname } from "path";
3
+ import chokidar, { FSWatcher } from "chokidar";
4
+ import { spawn, ChildProcess, execFile } from "child_process";
5
+ import { relative } from "path";
6
+
7
+ export const PUBLIC_DIR = "public";
8
+ export const SRC_DIR = "src";
9
+ export const APP_DIR = "src/app";
10
+
11
+ export function getFileMeta() {
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ return { __filename, __dirname };
15
+ }
16
+
17
+ export type WatchEvent = "add" | "addDir" | "change" | "unlink" | "unlinkDir";
18
+
19
+ export const DEFAULT_IGNORES: (string | RegExp)[] = [
20
+ /(^|[\/\\])\../,
21
+ "**/node_modules/**",
22
+ "**/vendor/**",
23
+ "**/dist/**",
24
+ "**/build/**",
25
+ "**/.cache/**",
26
+ "**/*.log",
27
+ "**/*.tmp",
28
+ "**/*.swp",
29
+ ];
30
+
31
+ export const DEFAULT_AWF = { stabilityThreshold: 300, pollInterval: 100 };
32
+
33
+ export function createSrcWatcher(
34
+ root: string,
35
+ opts: {
36
+ exts?: string[];
37
+ onEvent: (event: WatchEvent, absPath: string, relPath: string) => void;
38
+ ignored?: (string | RegExp)[];
39
+ awaitWriteFinish?: { stabilityThreshold: number; pollInterval: number };
40
+ logPrefix?: string;
41
+ usePolling?: boolean;
42
+ interval?: number;
43
+ }
44
+ ): FSWatcher {
45
+ const {
46
+ exts,
47
+ onEvent,
48
+ ignored = DEFAULT_IGNORES,
49
+ awaitWriteFinish = DEFAULT_AWF,
50
+ logPrefix = "watch",
51
+ usePolling = true,
52
+ } = opts;
53
+
54
+ const watcher = chokidar.watch(root, {
55
+ ignoreInitial: true,
56
+ persistent: true,
57
+ ignored,
58
+ awaitWriteFinish,
59
+ usePolling,
60
+ interval: opts.interval ?? 1000,
61
+ });
62
+
63
+ watcher
64
+ .on("ready", () => {
65
+ console.log(`[${logPrefix}] Watching ${root.replace(/\\/g, "/")}/**/*`);
66
+ })
67
+ .on("all", (event: WatchEvent, filePath: string) => {
68
+ if (exts && exts.length > 0) {
69
+ const ok = exts.some((ext) => filePath.endsWith(ext));
70
+ if (!ok) return;
71
+ }
72
+ const rel = relative(root, filePath).replace(/\\/g, "/");
73
+ if (event === "add" || event === "change" || event === "unlink") {
74
+ onEvent(event, filePath, rel);
75
+ }
76
+ })
77
+ .on("error", (err) => console.error(`[${logPrefix}] Error:`, err));
78
+
79
+ return watcher;
80
+ }
81
+
82
+ export class DebouncedWorker {
83
+ private timer: NodeJS.Timeout | null = null;
84
+ private running = false;
85
+ private queued = false;
86
+
87
+ constructor(
88
+ private work: () => Promise<void> | void,
89
+ private debounceMs = 350,
90
+ private name = "worker"
91
+ ) {}
92
+
93
+ schedule(reason?: string) {
94
+ if (reason) console.log(`[${this.name}] ${reason} → scheduled`);
95
+ if (this.timer) clearTimeout(this.timer);
96
+ this.timer = setTimeout(() => {
97
+ this.timer = null;
98
+ this.runNow().catch(() => {});
99
+ }, this.debounceMs);
100
+ }
101
+
102
+ private async runNow() {
103
+ if (this.running) {
104
+ this.queued = true;
105
+ return;
106
+ }
107
+ this.running = true;
108
+ try {
109
+ await this.work();
110
+ } catch (err) {
111
+ console.error(`[${this.name}] error:`, err);
112
+ } finally {
113
+ this.running = false;
114
+ if (this.queued) {
115
+ this.queued = false;
116
+ this.runNow().catch(() => {});
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ export function createRestartableProcess(spec: {
123
+ name: string;
124
+ cmd: string;
125
+ args?: string[];
126
+ stdio?: "inherit" | [any, any, any];
127
+ gracefulSignal?: NodeJS.Signals;
128
+ forceKillAfterMs?: number;
129
+ windowsKillTree?: boolean;
130
+ onStdout?: (buf: Buffer) => void;
131
+ onStderr?: (buf: Buffer) => void;
132
+ }) {
133
+ const {
134
+ name,
135
+ cmd,
136
+ args = [],
137
+ stdio = ["ignore", "pipe", "pipe"],
138
+ gracefulSignal = "SIGINT",
139
+ forceKillAfterMs = 2000,
140
+ windowsKillTree = true,
141
+ onStdout,
142
+ onStderr,
143
+ } = spec;
144
+
145
+ let child: ChildProcess | null = null;
146
+
147
+ function start() {
148
+ console.log(`[${name}] Starting: ${cmd} ${args.join(" ")}`.trim());
149
+ child = spawn(cmd, args, { stdio, windowsHide: true });
150
+
151
+ child.stdout?.on("data", (buf: Buffer) => {
152
+ if (onStdout) onStdout(buf);
153
+ else process.stdout.write(`[${name}] ${buf.toString()}`);
154
+ });
155
+
156
+ child.stderr?.on("data", (buf: Buffer) => {
157
+ if (onStderr) onStderr(buf);
158
+ else process.stderr.write(`[${name}:err] ${buf.toString()}`);
159
+ });
160
+
161
+ child.on("close", (code) => {
162
+ console.log(`[${name}] Exited with code ${code}`);
163
+ });
164
+
165
+ child.on("error", (err) => {
166
+ console.error(`[${name}] Failed to start:`, err);
167
+ });
168
+
169
+ return child;
170
+ }
171
+
172
+ function killOnWindows(pid: number): Promise<void> {
173
+ return new Promise((resolve) => {
174
+ const cp = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
175
+ resolve()
176
+ );
177
+ cp.on("error", () => resolve());
178
+ });
179
+ }
180
+
181
+ async function stop(): Promise<void> {
182
+ if (!child || child.killed) return;
183
+ const pid = child.pid!;
184
+ console.log(`[${name}] Stopping…`);
185
+
186
+ if (process.platform === "win32" && windowsKillTree) {
187
+ await killOnWindows(pid);
188
+ child = null;
189
+ return;
190
+ }
191
+
192
+ await new Promise<void>((resolve) => {
193
+ const done = () => resolve();
194
+ child!.once("close", done).once("exit", done).once("disconnect", done);
195
+ try {
196
+ child!.kill(gracefulSignal);
197
+ } catch {
198
+ resolve();
199
+ }
200
+ setTimeout(() => {
201
+ if (child && !child.killed) {
202
+ try {
203
+ process.kill(pid, "SIGKILL");
204
+ } catch {}
205
+ }
206
+ }, forceKillAfterMs);
207
+ });
208
+ child = null;
209
+ }
210
+
211
+ async function restart(reason?: string) {
212
+ if (reason) console.log(`[${name}] Restart requested: ${reason}`);
213
+ await stop();
214
+ return start();
215
+ }
216
+
217
+ function getChild() {
218
+ return child;
219
+ }
220
+
221
+ return { start, stop, restart, getChild };
222
+ }
223
+
224
+ export function onExit(fn: () => Promise<void> | void) {
225
+ const wrap = (sig: string) => async () => {
226
+ console.log(`[proc] Received ${sig}, shutting down…`);
227
+ try {
228
+ await fn();
229
+ } finally {
230
+ process.exit(0);
231
+ }
232
+ };
233
+ process.on("SIGINT", wrap("SIGINT"));
234
+ process.on("SIGTERM", wrap("SIGTERM"));
235
+ process.on("uncaughtException", async (err) => {
236
+ console.error("[proc] Uncaught exception:", err);
237
+ await wrap("uncaughtException")();
238
+ });
239
+ }
@@ -0,0 +1,246 @@
1
+ import { Plugin } from "vite";
2
+ import path from "path";
3
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
4
+ import ts from "typescript";
5
+
6
+ export function generateGlobalTypes(): Plugin {
7
+ const dtsPath = path.resolve(process.cwd(), ".pp", "global-functions.d.ts");
8
+
9
+ return {
10
+ name: "generate-global-types",
11
+
12
+ buildStart() {
13
+ const mainPath = path.resolve(process.cwd(), "ts", "main.ts");
14
+
15
+ if (!existsSync(mainPath)) {
16
+ console.warn("⚠️ ts/main.ts not found, skipping type generation");
17
+ return;
18
+ }
19
+
20
+ const content = readFileSync(mainPath, "utf-8");
21
+ const globals = parseGlobalSingletons(content, mainPath);
22
+
23
+ if (globals.length === 0) {
24
+ console.warn("⚠️ No createGlobalSingleton calls found");
25
+ return;
26
+ }
27
+
28
+ generateDtsWithTypeChecker(globals, dtsPath, mainPath);
29
+ },
30
+ };
31
+ }
32
+ interface GlobalDeclaration {
33
+ name: string;
34
+ importPath: string;
35
+ exportName: string;
36
+ }
37
+
38
+ function parseGlobalSingletons(
39
+ content: string,
40
+ filePath: string
41
+ ): GlobalDeclaration[] {
42
+ const sf = ts.createSourceFile(
43
+ filePath,
44
+ content,
45
+ ts.ScriptTarget.Latest,
46
+ true
47
+ );
48
+
49
+ const globals: GlobalDeclaration[] = [];
50
+ const importMap = new Map<string, { path: string; originalName: string }>();
51
+
52
+ sf.statements.forEach((stmt) => {
53
+ if (ts.isImportDeclaration(stmt) && stmt.importClause) {
54
+ const moduleSpecifier = (stmt.moduleSpecifier as ts.StringLiteral).text;
55
+
56
+ if (stmt.importClause.namedBindings) {
57
+ if (ts.isNamedImports(stmt.importClause.namedBindings)) {
58
+ stmt.importClause.namedBindings.elements.forEach((element) => {
59
+ const localName = element.name.text;
60
+ const importedName = element.propertyName
61
+ ? element.propertyName.text
62
+ : localName;
63
+
64
+ importMap.set(localName, {
65
+ path: moduleSpecifier,
66
+ originalName: importedName,
67
+ });
68
+ });
69
+ }
70
+ }
71
+ }
72
+ });
73
+
74
+ function visit(node: ts.Node) {
75
+ if (
76
+ ts.isCallExpression(node) &&
77
+ ts.isIdentifier(node.expression) &&
78
+ node.expression.text === "createGlobalSingleton"
79
+ ) {
80
+ if (node.arguments.length >= 2) {
81
+ const nameArg = node.arguments[0];
82
+ const valueArg = node.arguments[1];
83
+
84
+ if (ts.isStringLiteral(nameArg) && ts.isIdentifier(valueArg)) {
85
+ const name = nameArg.text;
86
+ const variable = valueArg.text;
87
+ const importInfo = importMap.get(variable);
88
+
89
+ if (importInfo) {
90
+ globals.push({
91
+ name,
92
+ importPath: importInfo.path,
93
+ exportName: importInfo.originalName,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ ts.forEachChild(node, visit);
101
+ }
102
+
103
+ visit(sf);
104
+ return globals;
105
+ }
106
+
107
+ function generateDtsWithTypeChecker(
108
+ globals: GlobalDeclaration[],
109
+ dtsPath: string,
110
+ mainPath: string
111
+ ) {
112
+ const configPath = ts.findConfigFile(
113
+ process.cwd(),
114
+ ts.sys.fileExists,
115
+ "tsconfig.json"
116
+ );
117
+
118
+ const { config } = configPath
119
+ ? ts.readConfigFile(configPath, ts.sys.readFile)
120
+ : { config: {} };
121
+
122
+ const { options } = ts.parseJsonConfigFileContent(
123
+ config,
124
+ ts.sys,
125
+ process.cwd()
126
+ );
127
+
128
+ const program = ts.createProgram([mainPath], options);
129
+ const checker = program.getTypeChecker();
130
+ const sourceFile = program.getSourceFile(mainPath);
131
+
132
+ if (!sourceFile) {
133
+ console.warn("⚠️ Could not load main.ts for type checking");
134
+ generateFallbackDts(globals, dtsPath);
135
+ return;
136
+ }
137
+
138
+ const signatures = new Map<string, string>();
139
+
140
+ const importMap = new Map<string, ts.ImportDeclaration>();
141
+ sourceFile.statements.forEach((stmt) => {
142
+ if (ts.isImportDeclaration(stmt) && stmt.importClause?.namedBindings) {
143
+ if (ts.isNamedImports(stmt.importClause.namedBindings)) {
144
+ stmt.importClause.namedBindings.elements.forEach((element) => {
145
+ importMap.set(element.name.text, stmt);
146
+ });
147
+ }
148
+ }
149
+ });
150
+
151
+ globals.forEach(({ name, exportName }) => {
152
+ try {
153
+ const importDecl = importMap.get(exportName);
154
+ if (!importDecl || !importDecl.importClause?.namedBindings) {
155
+ signatures.set(name, "(...args: any[]) => any");
156
+ return;
157
+ }
158
+
159
+ if (ts.isNamedImports(importDecl.importClause.namedBindings)) {
160
+ const importSpec = importDecl.importClause.namedBindings.elements.find(
161
+ (el) => el.name.text === exportName
162
+ );
163
+
164
+ if (importSpec) {
165
+ const symbol = checker.getSymbolAtLocation(importSpec.name);
166
+ if (symbol) {
167
+ const type = checker.getTypeOfSymbolAtLocation(
168
+ symbol,
169
+ importSpec.name
170
+ );
171
+ const signature = checker.typeToString(
172
+ type,
173
+ undefined,
174
+ ts.TypeFormatFlags.NoTruncation
175
+ );
176
+ signatures.set(name, signature);
177
+ return;
178
+ }
179
+ }
180
+ }
181
+
182
+ signatures.set(name, "(...args: any[]) => any");
183
+ } catch (error) {
184
+ console.warn(`⚠️ Failed to extract type for ${name}:`, error);
185
+ signatures.set(name, "(...args: any[]) => any");
186
+ }
187
+ });
188
+
189
+ const declarations = globals
190
+ .map(({ name }) => {
191
+ const sig = signatures.get(name) || "(...args: any[]) => any";
192
+ return ` const ${name}: ${sig};`;
193
+ })
194
+ .join("\n");
195
+
196
+ const windowDeclarations = globals
197
+ .map(({ name }) => ` ${name}: typeof globalThis.${name};`)
198
+ .join("\n");
199
+
200
+ const content = `// Auto-generated by Vite plugin
201
+ // Do not edit manually - regenerate with: npm run dev or npm run build
202
+ // Source: ts/main.ts
203
+
204
+ declare global {
205
+ ${declarations}
206
+
207
+ interface Window {
208
+ ${windowDeclarations}
209
+ }
210
+ }
211
+
212
+ export {};
213
+ `;
214
+
215
+ const dir = path.dirname(dtsPath);
216
+ if (!existsSync(dir)) {
217
+ mkdirSync(dir, { recursive: true });
218
+ }
219
+
220
+ writeFileSync(dtsPath, content, "utf-8");
221
+ console.log(`✅ Generated ${path.relative(process.cwd(), dtsPath)}`);
222
+ }
223
+
224
+ function generateFallbackDts(globals: GlobalDeclaration[], dtsPath: string) {
225
+ const declarations = globals
226
+ .map(({ name }) => ` const ${name}: (...args: any[]) => any;`)
227
+ .join("\n");
228
+
229
+ const windowDeclarations = globals
230
+ .map(({ name }) => ` ${name}: typeof globalThis.${name};`)
231
+ .join("\n");
232
+
233
+ const content = `// Auto-generated by Vite plugin
234
+ declare global {
235
+ ${declarations}
236
+
237
+ interface Window {
238
+ ${windowDeclarations}
239
+ }
240
+ }
241
+
242
+ export {};
243
+ `;
244
+
245
+ writeFileSync(dtsPath, content, "utf-8");
246
+ }