create-prisma-php-app 4.0.0-alpha.19 → 4.0.0-alpha.20

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.
@@ -1,53 +1,52 @@
1
- import { spawn, ChildProcess } from "child_process";
2
1
  import { join } from "path";
3
- import chokidar from "chokidar";
4
- import { getFileMeta } from "./utils.js";
5
-
6
- const { __dirname } = getFileMeta();
7
-
8
- const phpPath = "php";
2
+ import {
3
+ createRestartableProcess,
4
+ createSrcWatcher,
5
+ DebouncedWorker,
6
+ DEFAULT_AWF,
7
+ onExit,
8
+ } from "./utils.js";
9
+
10
+ // Config
11
+ const phpPath = process.env.PHP_PATH ?? "php";
12
+ const SRC_DIR = join(process.cwd(), "src");
9
13
  const serverScriptPath = join(
10
- __dirname,
11
- "..",
12
- "src",
14
+ SRC_DIR,
13
15
  "Lib",
14
16
  "Websocket",
15
17
  "websocket-server.php"
16
18
  );
17
19
 
18
- let serverProcess: ChildProcess | null = null;
19
-
20
- const restartServer = (): void => {
21
- if (serverProcess) {
22
- console.log("Stopping WebSocket server...");
23
- serverProcess.kill("SIGINT");
24
- serverProcess = null;
25
- }
26
-
27
- console.log("Starting WebSocket server...");
28
- serverProcess = spawn(phpPath, [serverScriptPath]);
29
-
30
- serverProcess.stdout?.on("data", (data: Buffer) => {
31
- console.log(`WebSocket Server: ${data.toString()}`);
32
- });
33
-
34
- serverProcess.stderr?.on("data", (data: Buffer) => {
35
- console.error(`WebSocket Server Error: ${data.toString()}`);
36
- });
37
-
38
- serverProcess.on("close", (code: number) => {
39
- console.log(`WebSocket server exited with code ${code}`);
40
- });
41
- };
42
-
43
- // Initial start
44
- restartServer();
20
+ // Restartable WS server
21
+ const ws = createRestartableProcess({
22
+ name: "WebSocket",
23
+ cmd: phpPath,
24
+ args: [serverScriptPath],
25
+ windowsKillTree: true,
26
+ });
27
+
28
+ ws.start();
29
+
30
+ // Debounced restarter
31
+ const restarter = new DebouncedWorker(
32
+ async () => {
33
+ await ws.restart("file change");
34
+ },
35
+ 400,
36
+ "ws-restart"
37
+ );
45
38
 
46
- // Watch for changes
47
- chokidar
48
- .watch(join(__dirname, "..", "src", "Websocket", "**", "*"))
49
- .on("change", (path: string) => {
50
- const fileChanged = path.split("\\").pop();
51
- console.log(`File changed: src/Lib/Websocket/${fileChanged}`);
52
- restartServer();
53
- });
39
+ // Watch ./src recursively; restart on code/data file changes
40
+ createSrcWatcher(SRC_DIR, {
41
+ exts: [".php", ".ts", ".js", ".json"],
42
+ onEvent: (ev, _abs, rel) => restarter.schedule(`${ev}: ${rel}`),
43
+ awaitWriteFinish: DEFAULT_AWF,
44
+ logPrefix: "WS watch",
45
+ usePolling: true,
46
+ interval: 1000,
47
+ });
48
+
49
+ // Graceful shutdown
50
+ onExit(async () => {
51
+ await ws.stop();
52
+ });
@@ -1,5 +1,8 @@
1
1
  import { fileURLToPath } from "url";
2
2
  import { dirname } from "path";
3
+ import chokidar, { FSWatcher } from "chokidar";
4
+ import { spawn, ChildProcess, execFile } from "child_process";
5
+ import { relative } from "path";
3
6
 
4
7
  /**
5
8
  * Retrieves the file metadata including the filename and directory name.
@@ -12,3 +15,240 @@ export function getFileMeta() {
12
15
  const __dirname = dirname(__filename);
13
16
  return { __filename, __dirname };
14
17
  }
18
+
19
+ export type WatchEvent = "add" | "addDir" | "change" | "unlink" | "unlinkDir";
20
+
21
+ export const DEFAULT_IGNORES: (string | RegExp)[] = [
22
+ /(^|[\/\\])\../, // dotfiles
23
+ "**/node_modules/**",
24
+ "**/vendor/**",
25
+ "**/dist/**",
26
+ "**/build/**",
27
+ "**/.cache/**",
28
+ "**/*.log",
29
+ "**/*.tmp",
30
+ "**/*.swp",
31
+ ];
32
+
33
+ export const DEFAULT_AWF = { stabilityThreshold: 300, pollInterval: 100 };
34
+
35
+ /**
36
+ * Create a chokidar watcher for a given root. Optionally filter by file extensions.
37
+ */
38
+ export function createSrcWatcher(
39
+ root: string,
40
+ opts: {
41
+ exts?: string[]; // e.g. ['.php','.ts']
42
+ onEvent: (event: WatchEvent, absPath: string, relPath: string) => void;
43
+ ignored?: (string | RegExp)[];
44
+ awaitWriteFinish?: { stabilityThreshold: number; pollInterval: number };
45
+ logPrefix?: string;
46
+ usePolling?: boolean;
47
+ interval?: number;
48
+ }
49
+ ): FSWatcher {
50
+ const {
51
+ exts,
52
+ onEvent,
53
+ ignored = DEFAULT_IGNORES,
54
+ awaitWriteFinish = DEFAULT_AWF,
55
+ logPrefix = "watch",
56
+ usePolling = true,
57
+ } = opts;
58
+
59
+ const watcher = chokidar.watch(root, {
60
+ ignoreInitial: true,
61
+ persistent: true,
62
+ ignored,
63
+ awaitWriteFinish,
64
+ usePolling,
65
+ interval: opts.interval ?? 1000,
66
+ });
67
+
68
+ watcher
69
+ .on("ready", () => {
70
+ console.log(`[${logPrefix}] Watching ${root.replace(/\\/g, "/")}/**/*`);
71
+ })
72
+ .on("all", (event: WatchEvent, filePath: string) => {
73
+ // Optional extension filter
74
+ if (exts && exts.length > 0) {
75
+ const ok = exts.some((ext) => filePath.endsWith(ext));
76
+ if (!ok) return;
77
+ }
78
+ const rel = relative(root, filePath).replace(/\\/g, "/");
79
+ if (event === "add" || event === "change" || event === "unlink") {
80
+ onEvent(event, filePath, rel);
81
+ }
82
+ })
83
+ .on("error", (err) => console.error(`[${logPrefix}] Error:`, err));
84
+
85
+ return watcher;
86
+ }
87
+
88
+ /**
89
+ * Debounced worker that ensures only one run at a time; extra runs get queued once.
90
+ */
91
+ export class DebouncedWorker {
92
+ private timer: NodeJS.Timeout | null = null;
93
+ private running = false;
94
+ private queued = false;
95
+
96
+ constructor(
97
+ private work: () => Promise<void> | void,
98
+ private debounceMs = 350,
99
+ private name = "worker"
100
+ ) {}
101
+
102
+ schedule(reason?: string) {
103
+ if (reason) console.log(`[${this.name}] ${reason} → scheduled`);
104
+ if (this.timer) clearTimeout(this.timer);
105
+ this.timer = setTimeout(() => {
106
+ this.timer = null;
107
+ this.runNow().catch(() => {});
108
+ }, this.debounceMs);
109
+ }
110
+
111
+ private async runNow() {
112
+ if (this.running) {
113
+ this.queued = true;
114
+ return;
115
+ }
116
+ this.running = true;
117
+ try {
118
+ await this.work();
119
+ } catch (err) {
120
+ console.error(`[${this.name}] error:`, err);
121
+ } finally {
122
+ this.running = false;
123
+ if (this.queued) {
124
+ this.queued = false;
125
+ this.runNow().catch(() => {});
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Cross-platform restartable process.
133
+ */
134
+ export function createRestartableProcess(spec: {
135
+ name: string;
136
+ cmd: string;
137
+ args?: string[];
138
+ stdio?: "inherit" | [any, any, any];
139
+ gracefulSignal?: NodeJS.Signals;
140
+ forceKillAfterMs?: number;
141
+ windowsKillTree?: boolean;
142
+ onStdout?: (buf: Buffer) => void;
143
+ onStderr?: (buf: Buffer) => void;
144
+ }) {
145
+ const {
146
+ name,
147
+ cmd,
148
+ args = [],
149
+ stdio = ["ignore", "pipe", "pipe"],
150
+ gracefulSignal = "SIGINT",
151
+ forceKillAfterMs = 2000,
152
+ windowsKillTree = true,
153
+ onStdout,
154
+ onStderr,
155
+ } = spec;
156
+
157
+ let child: ChildProcess | null = null;
158
+
159
+ function start() {
160
+ console.log(`[${name}] Starting: ${cmd} ${args.join(" ")}`.trim());
161
+ child = spawn(cmd, args, { stdio, windowsHide: true });
162
+
163
+ child.stdout?.on("data", (buf: Buffer) => {
164
+ if (onStdout) onStdout(buf);
165
+ else process.stdout.write(`[${name}] ${buf.toString()}`);
166
+ });
167
+
168
+ child.stderr?.on("data", (buf: Buffer) => {
169
+ if (onStderr) onStderr(buf);
170
+ else process.stderr.write(`[${name}:err] ${buf.toString()}`);
171
+ });
172
+
173
+ child.on("close", (code) => {
174
+ console.log(`[${name}] Exited with code ${code}`);
175
+ });
176
+
177
+ child.on("error", (err) => {
178
+ console.error(`[${name}] Failed to start:`, err);
179
+ });
180
+
181
+ return child;
182
+ }
183
+
184
+ function killOnWindows(pid: number): Promise<void> {
185
+ return new Promise((resolve) => {
186
+ const cp = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
187
+ resolve()
188
+ );
189
+ cp.on("error", () => resolve());
190
+ });
191
+ }
192
+
193
+ async function stop(): Promise<void> {
194
+ if (!child || child.killed) return;
195
+ const pid = child.pid!;
196
+ console.log(`[${name}] Stopping…`);
197
+
198
+ if (process.platform === "win32" && windowsKillTree) {
199
+ await killOnWindows(pid);
200
+ child = null;
201
+ return;
202
+ }
203
+
204
+ await new Promise<void>((resolve) => {
205
+ const done = () => resolve();
206
+ child!.once("close", done).once("exit", done).once("disconnect", done);
207
+ try {
208
+ child!.kill(gracefulSignal);
209
+ } catch {
210
+ resolve();
211
+ }
212
+ setTimeout(() => {
213
+ if (child && !child.killed) {
214
+ try {
215
+ process.kill(pid, "SIGKILL");
216
+ } catch {}
217
+ }
218
+ }, forceKillAfterMs);
219
+ });
220
+ child = null;
221
+ }
222
+
223
+ async function restart(reason?: string) {
224
+ if (reason) console.log(`[${name}] Restart requested: ${reason}`);
225
+ await stop();
226
+ return start();
227
+ }
228
+
229
+ function getChild() {
230
+ return child;
231
+ }
232
+
233
+ return { start, stop, restart, getChild };
234
+ }
235
+
236
+ /**
237
+ * Register shutdown cleanup callbacks.
238
+ */
239
+ export function onExit(fn: () => Promise<void> | void) {
240
+ const wrap = (sig: string) => async () => {
241
+ console.log(`[proc] Received ${sig}, shutting down…`);
242
+ try {
243
+ await fn();
244
+ } finally {
245
+ process.exit(0);
246
+ }
247
+ };
248
+ process.on("SIGINT", wrap("SIGINT"));
249
+ process.on("SIGTERM", wrap("SIGTERM"));
250
+ process.on("uncaughtException", async (err) => {
251
+ console.error("[proc] Uncaught exception:", err);
252
+ await wrap("uncaughtException")();
253
+ });
254
+ }