create-prisma-php-app 4.4.4-beta → 4.4.4

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,9 +1,13 @@
1
- import { createProxyMiddleware } from "http-proxy-middleware";
1
+ import {
2
+ createProxyMiddleware,
3
+ responseInterceptor,
4
+ } from "http-proxy-middleware";
2
5
  import { writeFileSync, existsSync, mkdirSync } from "fs";
6
+ import { networkInterfaces } from "os";
3
7
  import browserSync, { BrowserSyncInstance } from "browser-sync";
4
8
  import prismaPhpConfigJson from "../prisma-php.json";
5
9
  import { generateFileListJson } from "./files-list.js";
6
- import { join, dirname } from "path";
10
+ import { join, dirname, relative } from "path";
7
11
  import { getFileMeta, PUBLIC_DIR, SRC_DIR } from "./utils.js";
8
12
  import { updateAllClassLogs } from "./class-log.js";
9
13
  import {
@@ -13,11 +17,25 @@ import {
13
17
  } from "./class-imports";
14
18
  import { checkComponentImports } from "./component-import-checker";
15
19
  import { DebouncedWorker, createSrcWatcher, DEFAULT_AWF } from "./utils.js";
20
+ import chalk from "chalk";
16
21
 
17
22
  const { __dirname } = getFileMeta();
18
-
19
23
  const bs: BrowserSyncInstance = browserSync.create();
20
24
 
25
+ const PUBLIC_IGNORE_DIRS = [""];
26
+
27
+ function getExternalIP(): string | null {
28
+ const nets = networkInterfaces();
29
+ for (const name of Object.keys(nets)) {
30
+ for (const net of nets[name]!) {
31
+ if (net.family === "IPv4" && !net.internal) {
32
+ return net.address;
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
21
39
  const pipeline = new DebouncedWorker(
22
40
  async () => {
23
41
  await generateFileListJson();
@@ -45,18 +63,18 @@ const pipeline = new DebouncedWorker(
45
63
  }
46
64
  },
47
65
  350,
48
- "bs-pipeline"
66
+ "bs-pipeline",
49
67
  );
50
68
 
51
69
  const publicPipeline = new DebouncedWorker(
52
70
  async () => {
53
- console.log("→ Public directory changed, reloading browser...");
71
+ console.log(chalk.cyan("→ Public directory changed, reloading browser..."));
54
72
  if (bs.active) {
55
73
  bs.reload();
56
74
  }
57
75
  },
58
76
  350,
59
- "bs-public-pipeline"
77
+ "bs-public-pipeline",
60
78
  );
61
79
 
62
80
  createSrcWatcher(join(SRC_DIR, "**", "*"), {
@@ -68,7 +86,19 @@ createSrcWatcher(join(SRC_DIR, "**", "*"), {
68
86
  });
69
87
 
70
88
  createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
71
- onEvent: (_ev, _abs, rel) => publicPipeline.schedule(rel),
89
+ onEvent: (_ev, abs, _) => {
90
+ const relFromPublic = relative(PUBLIC_DIR, abs);
91
+ const normalized = relFromPublic.replace(/\\/g, "/");
92
+
93
+ const segments = normalized.split("/").filter(Boolean);
94
+ const firstSegment = segments[0] || "";
95
+
96
+ if (PUBLIC_IGNORE_DIRS.includes(firstSegment)) {
97
+ return;
98
+ }
99
+
100
+ publicPipeline.schedule(relFromPublic);
101
+ },
72
102
  awaitWriteFinish: DEFAULT_AWF,
73
103
  logPrefix: "watch-public",
74
104
  usePolling: true,
@@ -77,16 +107,17 @@ createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
77
107
 
78
108
  const viteFlagFile = join(__dirname, "..", ".pp", ".vite-build-complete");
79
109
  mkdirSync(dirname(viteFlagFile), { recursive: true });
80
- writeFileSync(viteFlagFile, "");
81
110
 
82
111
  if (!existsSync(viteFlagFile)) {
83
112
  writeFileSync(viteFlagFile, "0");
113
+ } else {
114
+ writeFileSync(viteFlagFile, "");
84
115
  }
85
116
 
86
117
  createSrcWatcher(viteFlagFile, {
87
118
  onEvent: (ev) => {
88
119
  if (ev === "change" && bs.active) {
89
- console.log("→ Vite build complete, reloading browser...");
120
+ console.log(chalk.green("→ Vite build complete, reloading browser..."));
90
121
  bs.reload();
91
122
  }
92
123
  },
@@ -99,6 +130,7 @@ createSrcWatcher(viteFlagFile, {
99
130
  bs.init(
100
131
  {
101
132
  proxy: "http://localhost:3000",
133
+ online: true,
102
134
  middleware: [
103
135
  (_req, res, next) => {
104
136
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
@@ -106,36 +138,142 @@ bs.init(
106
138
  res.setHeader("Expires", "0");
107
139
  next();
108
140
  },
141
+
142
+ (req, _, next) => {
143
+ const time = new Date().toLocaleTimeString();
144
+ console.log(
145
+ `${chalk.gray(time)} ${chalk.cyan("[Proxy]")} ${chalk.bold(req.method)} ${req.url}`,
146
+ );
147
+ next();
148
+ },
149
+
109
150
  createProxyMiddleware({
110
151
  target: prismaPhpConfigJson.bsTarget,
111
152
  changeOrigin: true,
112
153
  pathRewrite: {},
154
+ selfHandleResponse: true,
155
+
156
+ on: {
157
+ proxyReq: (proxyReq, req, _res) => {
158
+ proxyReq.setHeader("Accept-Encoding", "");
159
+
160
+ const sendsJson =
161
+ req.headers["content-type"]?.includes("application/json");
162
+ const asksJson =
163
+ req.headers["accept"]?.includes("application/json");
164
+
165
+ if (!sendsJson && !asksJson) return;
166
+
167
+ const originalWrite = proxyReq.write;
168
+ proxyReq.write = function (data, ...args) {
169
+ if (data) {
170
+ try {
171
+ const body = data.toString();
172
+ const json = JSON.parse(body);
173
+ console.log(
174
+ chalk.blue("→ API Request:"),
175
+ JSON.stringify(json, null, 2),
176
+ );
177
+ } catch {
178
+ if (data.toString().trim() !== "") {
179
+ console.log(chalk.blue("→ API Request:"), data.toString());
180
+ }
181
+ }
182
+ }
183
+ // @ts-ignore
184
+ return originalWrite.call(proxyReq, data, ...args);
185
+ };
186
+ },
187
+
188
+ proxyRes: responseInterceptor(
189
+ async (responseBuffer, proxyRes, _req, _res) => {
190
+ const contentType = proxyRes.headers["content-type"] || "";
191
+
192
+ if (!contentType.includes("application/json")) {
193
+ return responseBuffer;
194
+ }
195
+
196
+ try {
197
+ const body = responseBuffer.toString("utf8");
198
+ console.log(
199
+ chalk.green("← API Response:"),
200
+ JSON.stringify(JSON.parse(body), null, 2),
201
+ );
202
+ console.log(
203
+ chalk.gray("----------------------------------------"),
204
+ );
205
+ } catch (e) {
206
+ console.log(
207
+ chalk.red("← API Response (Parse Error):"),
208
+ responseBuffer.toString(),
209
+ );
210
+ }
211
+
212
+ return responseBuffer;
213
+ },
214
+ ),
215
+
216
+ error: (err) => {
217
+ console.error(chalk.red("Proxy Error:"), err);
218
+ },
219
+ },
113
220
  }),
114
221
  ],
115
-
116
222
  notify: false,
117
223
  open: false,
118
224
  ghostMode: false,
119
225
  codeSync: true,
226
+ logLevel: "silent",
120
227
  },
121
228
  (err, bsInstance) => {
122
229
  if (err) {
123
- console.error("BrowserSync failed to start:", err);
230
+ console.error(chalk.red("BrowserSync failed to start:"), err);
124
231
  return;
125
232
  }
126
233
 
234
+ const bsPort = bsInstance.getOption("port");
127
235
  const urls = bsInstance.getOption("urls");
236
+ const localUrl = urls.get("local") || `http://localhost:${bsPort}`;
237
+ const externalIP = getExternalIP();
238
+ const externalUrl =
239
+ urls.get("external") ||
240
+ (externalIP ? `http://${externalIP}:${bsPort}` : null);
241
+ const uiUrl = urls.get("ui");
242
+ const uiExtUrl = urls.get("ui-external");
243
+
244
+ console.log("");
245
+ console.log(chalk.green.bold("✔ Ports Configured:"));
246
+ console.log(
247
+ ` ${chalk.blue.bold("Frontend (BrowserSync):")} ${chalk.magenta(localUrl)}`,
248
+ );
249
+ console.log(
250
+ ` ${chalk.yellow.bold("Backend (PHP Target):")} ${chalk.magenta(
251
+ prismaPhpConfigJson.bsTarget || "http://localhost:80",
252
+ )}`,
253
+ );
254
+ console.log(chalk.gray(" ------------------------------------"));
255
+
256
+ if (externalUrl) {
257
+ console.log(
258
+ ` ${chalk.bold("External:")} ${chalk.magenta(externalUrl)}`,
259
+ );
260
+ }
261
+
262
+ if (uiUrl) {
263
+ console.log(` ${chalk.bold("UI:")} ${chalk.magenta(uiUrl)}`);
264
+ }
265
+
128
266
  const out = {
129
- local: urls.get("local"),
130
- external: urls.get("external"),
131
- ui: urls.get("ui"),
132
- uiExternal: urls.get("ui-external"),
267
+ local: localUrl,
268
+ external: externalUrl,
269
+ ui: uiUrl,
270
+ uiExternal: uiExtUrl,
133
271
  };
134
272
 
135
273
  writeFileSync(
136
274
  join(__dirname, "bs-config.json"),
137
- JSON.stringify(out, null, 2)
275
+ JSON.stringify(out, null, 2),
138
276
  );
139
- console.log("\n\x1b[90mPress Ctrl+C to stop.\x1b[0m\n");
140
- }
277
+ console.log(`\n${chalk.gray("Press Ctrl+C to stop.")}\n`);
278
+ },
141
279
  );
@@ -13,7 +13,7 @@ const newProjectName = basename(join(__dirname, ".."));
13
13
 
14
14
  function updateProjectNameInConfig(
15
15
  filePath: string,
16
- newProjectName: string
16
+ newProjectName: string,
17
17
  ): void {
18
18
  const filePathDir = dirname(filePath);
19
19
 
@@ -23,7 +23,7 @@ function updateProjectNameInConfig(
23
23
 
24
24
  const targetPath = getTargetPath(
25
25
  filePathDir,
26
- prismaPhpConfigJson.phpEnvironment
26
+ prismaPhpConfigJson.phpEnvironment,
27
27
  );
28
28
 
29
29
  prismaPhpConfigJson.bsTarget = `http://localhost${targetPath}`;
@@ -39,59 +39,84 @@ function updateProjectNameInConfig(
39
39
  return;
40
40
  }
41
41
  console.log(
42
- "The project name, PHP path, and other paths have been updated successfully."
42
+ "The project name, PHP path, and other paths have been updated successfully.",
43
43
  );
44
- }
44
+ },
45
45
  );
46
46
  }
47
47
 
48
48
  function getTargetPath(fullPath: string, environment: string): string {
49
49
  const normalizedPath = normalize(fullPath);
50
50
 
51
- if (process.env.CI === "true") {
51
+ // ---- CI / Railway / Docker safe-guards ----
52
+ // GitHub Actions etc.
53
+ if (process.env.CI === "true") return "/";
54
+
55
+ // Railway commonly exposes these (not guaranteed, but helpful)
56
+ if (process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID)
57
+ return "/";
58
+
59
+ // Docker/containers: your app root is usually /app
60
+ // If you're not inside an AMP stack folder, don't crash.
61
+ const lower = normalizedPath.toLowerCase();
62
+ if (
63
+ lower === "/app" ||
64
+ lower.startsWith("/app" + sep) ||
65
+ lower.startsWith("/app/")
66
+ ) {
52
67
  return "/";
53
68
  }
54
69
 
55
- const webDirectories: { [key: string]: string } = {
70
+ // ---- Local AMP detection map (your original logic) ----
71
+ const webDirectories: Record<string, string> = {
56
72
  XAMPP: join("htdocs"),
57
73
  WAMP: join("www"),
58
74
  MAMP: join("htdocs"),
59
75
  LAMP: join("var", "www", "html"),
60
76
  LEMP: join("usr", "share", "nginx", "html"),
61
77
  AMPPS: join("www"),
62
- UniformServer: join("www"),
63
- EasyPHP: join("data", "localweb"),
78
+ UNIFORMSERVER: join("www"),
79
+ EASYPHP: join("data", "localweb"),
64
80
  };
65
81
 
66
- const webDir = webDirectories[environment.toUpperCase()];
67
- if (!webDir) {
68
- throw new Error(`Unsupported environment: ${environment}`);
69
- }
82
+ const envKey = (environment || "").toUpperCase();
83
+ const webDir = webDirectories[envKey];
70
84
 
71
- const indexOfWebDir = normalizedPath
72
- .toLowerCase()
73
- .indexOf(normalize(webDir).toLowerCase());
74
- if (indexOfWebDir === -1) {
75
- throw new Error(`Web directory not found in path: ${webDir}`);
76
- }
85
+ // If phpEnvironment is missing/unknown, don't crash in non-local contexts.
86
+ if (!webDir) return "/";
77
87
 
78
- const startIndex = indexOfWebDir + webDir.length;
88
+ const webDirNorm = normalize(webDir).toLowerCase();
89
+ const idx = lower.indexOf(webDirNorm);
90
+
91
+ // If we can't find htdocs/www/etc, fall back instead of throwing.
92
+ if (idx === -1) return "/";
93
+
94
+ const startIndex = idx + webDir.length;
79
95
  const subPath = normalizedPath.slice(startIndex);
96
+
80
97
  const safeSeparatorRegex = new RegExp(
81
98
  sep.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"),
82
- "g"
99
+ "g",
83
100
  );
84
- const finalPath = subPath.replace(safeSeparatorRegex, "/") + "/";
85
101
 
86
- return finalPath;
102
+ const finalPath = (subPath.replace(safeSeparatorRegex, "/") || "/") + "/";
103
+
104
+ // Ensure it starts with "/"
105
+ return finalPath.startsWith("/") ? finalPath : `/${finalPath}`;
87
106
  }
88
107
 
89
108
  const configFilePath = join(__dirname, "..", "prisma-php.json");
90
109
 
91
- updateProjectNameInConfig(configFilePath, newProjectName);
110
+ const isLocal =
111
+ !process.env.CI &&
112
+ !process.env.RAILWAY_ENVIRONMENT &&
113
+ !process.env.RAILWAY_PROJECT_ID;
114
+ if (isLocal) {
115
+ updateProjectNameInConfig(configFilePath, newProjectName);
116
+ }
92
117
 
93
118
  export const deleteFilesIfExist = async (
94
- filePaths: string[]
119
+ filePaths: string[],
95
120
  ): Promise<void> => {
96
121
  for (const filePath of filePaths) {
97
122
  try {
@@ -113,7 +138,7 @@ export const deleteFilesIfExist = async (
113
138
  };
114
139
 
115
140
  export async function deleteDirectoriesIfExist(
116
- dirPaths: string[]
141
+ dirPaths: string[],
117
142
  ): Promise<void> {
118
143
  for (const dirPath of dirPaths) {
119
144
  try {
@@ -0,0 +1,301 @@
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
+
33
+ interface GlobalDeclaration {
34
+ name: string;
35
+ importPath: string;
36
+ exportName: string;
37
+ isNamespace: boolean;
38
+ }
39
+
40
+ function parseGlobalSingletons(
41
+ content: string,
42
+ filePath: string
43
+ ): GlobalDeclaration[] {
44
+ const sf = ts.createSourceFile(
45
+ filePath,
46
+ content,
47
+ ts.ScriptTarget.Latest,
48
+ true
49
+ );
50
+
51
+ const globals: GlobalDeclaration[] = [];
52
+ const importMap = new Map<
53
+ string,
54
+ { path: string; originalName: string; isNamespace: boolean }
55
+ >();
56
+
57
+ sf.statements.forEach((stmt) => {
58
+ if (ts.isImportDeclaration(stmt) && stmt.importClause) {
59
+ const moduleSpecifier = (stmt.moduleSpecifier as ts.StringLiteral).text;
60
+
61
+ if (stmt.importClause.namedBindings) {
62
+ if (ts.isNamedImports(stmt.importClause.namedBindings)) {
63
+ stmt.importClause.namedBindings.elements.forEach((element) => {
64
+ const localName = element.name.text;
65
+ const importedName = element.propertyName
66
+ ? element.propertyName.text
67
+ : localName;
68
+
69
+ importMap.set(localName, {
70
+ path: moduleSpecifier,
71
+ originalName: importedName,
72
+ isNamespace: false,
73
+ });
74
+ });
75
+ } else if (ts.isNamespaceImport(stmt.importClause.namedBindings)) {
76
+ const localName = stmt.importClause.namedBindings.name.text;
77
+ importMap.set(localName, {
78
+ path: moduleSpecifier,
79
+ originalName: localName,
80
+ isNamespace: true,
81
+ });
82
+ }
83
+ } else if (stmt.importClause.name) {
84
+ const localName = stmt.importClause.name.text;
85
+ importMap.set(localName, {
86
+ path: moduleSpecifier,
87
+ originalName: "default",
88
+ isNamespace: false,
89
+ });
90
+ }
91
+ }
92
+ });
93
+
94
+ function visit(node: ts.Node) {
95
+ if (
96
+ ts.isCallExpression(node) &&
97
+ ts.isIdentifier(node.expression) &&
98
+ node.expression.text === "createGlobalSingleton"
99
+ ) {
100
+ if (node.arguments.length >= 2) {
101
+ const nameArg = node.arguments[0];
102
+ const valueArg = node.arguments[1];
103
+
104
+ if (ts.isStringLiteral(nameArg) && ts.isIdentifier(valueArg)) {
105
+ const name = nameArg.text;
106
+ const variable = valueArg.text;
107
+ const importInfo = importMap.get(variable);
108
+
109
+ if (importInfo) {
110
+ globals.push({
111
+ name,
112
+ importPath: importInfo.path,
113
+ exportName: importInfo.originalName,
114
+ isNamespace: importInfo.isNamespace,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ts.forEachChild(node, visit);
121
+ }
122
+
123
+ visit(sf);
124
+ return globals;
125
+ }
126
+
127
+ function generateDtsWithTypeChecker(
128
+ globals: GlobalDeclaration[],
129
+ dtsPath: string,
130
+ mainPath: string
131
+ ) {
132
+ const configPath = ts.findConfigFile(
133
+ process.cwd(),
134
+ ts.sys.fileExists,
135
+ "tsconfig.json"
136
+ );
137
+ const { config } = configPath
138
+ ? ts.readConfigFile(configPath, ts.sys.readFile)
139
+ : { config: {} };
140
+ const parsedConfig = ts.parseJsonConfigFileContent(
141
+ config,
142
+ ts.sys,
143
+ process.cwd()
144
+ );
145
+
146
+ const program = ts.createProgram(
147
+ parsedConfig.fileNames,
148
+ parsedConfig.options
149
+ );
150
+ const checker = program.getTypeChecker();
151
+ const sourceFile = program.getSourceFile(mainPath);
152
+
153
+ if (!sourceFile) {
154
+ generateFallbackDts(globals, dtsPath);
155
+ return;
156
+ }
157
+
158
+ const signatures = new Map<string, string>();
159
+ const importMap = new Map<string, ts.ImportDeclaration>();
160
+
161
+ sourceFile.statements.forEach((stmt) => {
162
+ if (ts.isImportDeclaration(stmt)) {
163
+ if (stmt.importClause?.namedBindings) {
164
+ if (ts.isNamedImports(stmt.importClause.namedBindings)) {
165
+ stmt.importClause.namedBindings.elements.forEach((element) => {
166
+ importMap.set(element.name.text, stmt);
167
+ });
168
+ } else if (ts.isNamespaceImport(stmt.importClause.namedBindings)) {
169
+ importMap.set(stmt.importClause.namedBindings.name.text, stmt);
170
+ }
171
+ } else if (stmt.importClause?.name) {
172
+ importMap.set(stmt.importClause.name.text, stmt);
173
+ }
174
+ }
175
+ });
176
+
177
+ globals.forEach(({ name, exportName, importPath, isNamespace }) => {
178
+ const isExternalLibrary =
179
+ !importPath.startsWith(".") && !importPath.startsWith("/");
180
+
181
+ if (isExternalLibrary) {
182
+ if (isNamespace) {
183
+ signatures.set(name, `typeof import("${importPath}")`);
184
+ } else {
185
+ signatures.set(name, `typeof import("${importPath}").${exportName}`);
186
+ }
187
+ return;
188
+ }
189
+
190
+ try {
191
+ const importDecl =
192
+ importMap.get(exportName === "default" ? name : exportName) ||
193
+ importMap.get(isNamespace ? name : exportName);
194
+ let symbol: ts.Symbol | undefined;
195
+
196
+ if (importDecl && importDecl.importClause) {
197
+ if (importDecl.importClause.namedBindings) {
198
+ if (ts.isNamedImports(importDecl.importClause.namedBindings)) {
199
+ const importSpec =
200
+ importDecl.importClause.namedBindings.elements.find(
201
+ (el) =>
202
+ (el.propertyName?.text || el.name.text) === exportName ||
203
+ el.name.text === exportName
204
+ );
205
+ if (importSpec)
206
+ symbol = checker.getSymbolAtLocation(importSpec.name);
207
+ } else if (
208
+ ts.isNamespaceImport(importDecl.importClause.namedBindings)
209
+ ) {
210
+ symbol = checker.getSymbolAtLocation(
211
+ importDecl.importClause.namedBindings.name
212
+ );
213
+ }
214
+ } else if (importDecl.importClause.name) {
215
+ symbol = checker.getSymbolAtLocation(importDecl.importClause.name);
216
+ }
217
+ }
218
+
219
+ if (symbol) {
220
+ const aliasedSymbol = checker.getAliasedSymbol(symbol);
221
+ const targetSymbol = aliasedSymbol || symbol;
222
+ const type = checker.getTypeOfSymbolAtLocation(
223
+ targetSymbol,
224
+ targetSymbol.valueDeclaration!
225
+ );
226
+ const signature = checker.typeToString(
227
+ type,
228
+ undefined,
229
+ ts.TypeFormatFlags.NoTruncation |
230
+ ts.TypeFormatFlags.UseFullyQualifiedType
231
+ );
232
+
233
+ if (signature !== "any") {
234
+ signatures.set(name, signature);
235
+ return;
236
+ }
237
+ }
238
+ } catch (error) {
239
+ console.warn(`Failed to resolve type for ${name}`);
240
+ }
241
+
242
+ // Fallback
243
+ signatures.set(name, "any");
244
+ });
245
+
246
+ const declarations = globals
247
+ .map(({ name, importPath }) => {
248
+ const sig = signatures.get(name) || "any";
249
+ return ` // @source: ${importPath}\n const ${name}: ${sig};`;
250
+ })
251
+ .join("\n");
252
+
253
+ const windowDeclarations = globals
254
+ .map(({ name }) => ` ${name}: typeof globalThis.${name};`)
255
+ .join("\n");
256
+
257
+ const content = `// Auto-generated by Vite plugin
258
+ // Do not edit manually - regenerate with: npm run dev or npm run build
259
+ // Source: ts/main.ts
260
+
261
+ declare global {
262
+ ${declarations}
263
+
264
+ interface Window {
265
+ ${windowDeclarations}
266
+ }
267
+ }
268
+
269
+ export {};
270
+ `;
271
+
272
+ const dir = path.dirname(dtsPath);
273
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
274
+ writeFileSync(dtsPath, content, "utf-8");
275
+ console.log(`✅ Generated ${path.relative(process.cwd(), dtsPath)}`);
276
+ }
277
+
278
+ function generateFallbackDts(globals: GlobalDeclaration[], dtsPath: string) {
279
+ const declarations = globals
280
+ .map(
281
+ ({ name, importPath }) =>
282
+ ` // @source: ${importPath}\n const ${name}: any;`
283
+ )
284
+ .join("\n");
285
+
286
+ const windowDeclarations = globals
287
+ .map(({ name }) => ` ${name}: typeof globalThis.${name};`)
288
+ .join("\n");
289
+
290
+ const content = `// Auto-generated by Vite plugin
291
+ declare global {
292
+ ${declarations}
293
+
294
+ interface Window {
295
+ ${windowDeclarations}
296
+ }
297
+ }
298
+ export {};
299
+ `;
300
+ writeFileSync(dtsPath, content, "utf-8");
301
+ }