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.
- package/README.md +418 -0
- package/dist/.prettierrc +10 -0
- package/dist/app-gitignore +28 -0
- package/dist/caspian.js +2 -0
- package/dist/index.js +2 -0
- package/dist/main.py +525 -0
- package/dist/postcss.config.js +6 -0
- package/dist/public/css/styles.css +1 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/js/main.js +1 -0
- package/dist/public/js/pp-reactive-v1.js +1 -0
- package/dist/pyproject.toml +9 -0
- package/dist/settings/bs-config.json +7 -0
- package/dist/settings/bs-config.ts +291 -0
- package/dist/settings/build.ts +19 -0
- package/dist/settings/component-map.json +1361 -0
- package/dist/settings/component-map.ts +381 -0
- package/dist/settings/files-list.json +36 -0
- package/dist/settings/files-list.ts +49 -0
- package/dist/settings/project-name.ts +119 -0
- package/dist/settings/python-server.ts +173 -0
- package/dist/settings/utils.ts +239 -0
- package/dist/settings/vite-plugins/generate-global-types.ts +246 -0
- package/dist/src/app/error.html +130 -0
- package/dist/src/app/globals.css +24 -0
- package/dist/src/app/index.html +159 -0
- package/dist/src/app/layout.html +22 -0
- package/dist/src/app/not-found.html +18 -0
- package/dist/ts/global-functions.ts +35 -0
- package/dist/ts/main.ts +5 -0
- package/dist/tsconfig.json +111 -0
- package/dist/vite.config.ts +61 -0
- package/package.json +42 -0
- package/tsconfig.json +49 -0
|
@@ -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
|
+
}
|