create-prisma-php-app 4.4.0-beta → 4.4.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.
@@ -1,9 +1,12 @@
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";
3
6
  import browserSync, { BrowserSyncInstance } from "browser-sync";
4
7
  import prismaPhpConfigJson from "../prisma-php.json";
5
8
  import { generateFileListJson } from "./files-list.js";
6
- import { join, dirname } from "path";
9
+ import { join, dirname, relative } from "path";
7
10
  import { getFileMeta, PUBLIC_DIR, SRC_DIR } from "./utils.js";
8
11
  import { updateAllClassLogs } from "./class-log.js";
9
12
  import {
@@ -13,11 +16,13 @@ import {
13
16
  } from "./class-imports";
14
17
  import { checkComponentImports } from "./component-import-checker";
15
18
  import { DebouncedWorker, createSrcWatcher, DEFAULT_AWF } from "./utils.js";
19
+ import chalk from "chalk";
16
20
 
17
21
  const { __dirname } = getFileMeta();
18
-
19
22
  const bs: BrowserSyncInstance = browserSync.create();
20
23
 
24
+ const PUBLIC_IGNORE_DIRS = [""];
25
+
21
26
  const pipeline = new DebouncedWorker(
22
27
  async () => {
23
28
  await generateFileListJson();
@@ -45,18 +50,18 @@ const pipeline = new DebouncedWorker(
45
50
  }
46
51
  },
47
52
  350,
48
- "bs-pipeline"
53
+ "bs-pipeline",
49
54
  );
50
55
 
51
56
  const publicPipeline = new DebouncedWorker(
52
57
  async () => {
53
- console.log("→ Public directory changed, reloading browser...");
58
+ console.log(chalk.cyan("→ Public directory changed, reloading browser..."));
54
59
  if (bs.active) {
55
60
  bs.reload();
56
61
  }
57
62
  },
58
63
  350,
59
- "bs-public-pipeline"
64
+ "bs-public-pipeline",
60
65
  );
61
66
 
62
67
  createSrcWatcher(join(SRC_DIR, "**", "*"), {
@@ -68,7 +73,19 @@ createSrcWatcher(join(SRC_DIR, "**", "*"), {
68
73
  });
69
74
 
70
75
  createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
71
- onEvent: (_ev, _abs, rel) => publicPipeline.schedule(rel),
76
+ onEvent: (_ev, abs, _) => {
77
+ const relFromPublic = relative(PUBLIC_DIR, abs);
78
+ const normalized = relFromPublic.replace(/\\/g, "/");
79
+
80
+ const segments = normalized.split("/").filter(Boolean);
81
+ const firstSegment = segments[0] || "";
82
+
83
+ if (PUBLIC_IGNORE_DIRS.includes(firstSegment)) {
84
+ return;
85
+ }
86
+
87
+ publicPipeline.schedule(relFromPublic);
88
+ },
72
89
  awaitWriteFinish: DEFAULT_AWF,
73
90
  logPrefix: "watch-public",
74
91
  usePolling: true,
@@ -77,16 +94,17 @@ createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
77
94
 
78
95
  const viteFlagFile = join(__dirname, "..", ".pp", ".vite-build-complete");
79
96
  mkdirSync(dirname(viteFlagFile), { recursive: true });
80
- writeFileSync(viteFlagFile, "");
81
97
 
82
98
  if (!existsSync(viteFlagFile)) {
83
99
  writeFileSync(viteFlagFile, "0");
100
+ } else {
101
+ writeFileSync(viteFlagFile, "");
84
102
  }
85
103
 
86
104
  createSrcWatcher(viteFlagFile, {
87
105
  onEvent: (ev) => {
88
106
  if (ev === "change" && bs.active) {
89
- console.log("→ Vite build complete, reloading browser...");
107
+ console.log(chalk.green("→ Vite build complete, reloading browser..."));
90
108
  bs.reload();
91
109
  }
92
110
  },
@@ -106,36 +124,138 @@ bs.init(
106
124
  res.setHeader("Expires", "0");
107
125
  next();
108
126
  },
127
+
128
+ (req, _, next) => {
129
+ const time = new Date().toLocaleTimeString();
130
+ console.log(
131
+ `${chalk.gray(time)} ${chalk.cyan("[Proxy]")} ${chalk.bold(req.method)} ${req.url}`,
132
+ );
133
+ next();
134
+ },
135
+
109
136
  createProxyMiddleware({
110
137
  target: prismaPhpConfigJson.bsTarget,
111
138
  changeOrigin: true,
112
139
  pathRewrite: {},
140
+ selfHandleResponse: true,
141
+
142
+ on: {
143
+ proxyReq: (proxyReq, req, _res) => {
144
+ proxyReq.setHeader("Accept-Encoding", "");
145
+
146
+ const sendsJson =
147
+ req.headers["content-type"]?.includes("application/json");
148
+ const asksJson =
149
+ req.headers["accept"]?.includes("application/json");
150
+
151
+ if (!sendsJson && !asksJson) return;
152
+
153
+ const originalWrite = proxyReq.write;
154
+ proxyReq.write = function (data, ...args) {
155
+ if (data) {
156
+ try {
157
+ const body = data.toString();
158
+ const json = JSON.parse(body);
159
+ console.log(
160
+ chalk.blue("→ API Request:"),
161
+ JSON.stringify(json, null, 2),
162
+ );
163
+ } catch {
164
+ if (data.toString().trim() !== "") {
165
+ console.log(chalk.blue("→ API Request:"), data.toString());
166
+ }
167
+ }
168
+ }
169
+ // @ts-ignore
170
+ return originalWrite.call(proxyReq, data, ...args);
171
+ };
172
+ },
173
+
174
+ proxyRes: responseInterceptor(
175
+ async (responseBuffer, proxyRes, _req, _res) => {
176
+ const contentType = proxyRes.headers["content-type"] || "";
177
+
178
+ if (!contentType.includes("application/json")) {
179
+ return responseBuffer;
180
+ }
181
+
182
+ try {
183
+ const body = responseBuffer.toString("utf8");
184
+ console.log(
185
+ chalk.green("← API Response:"),
186
+ JSON.stringify(JSON.parse(body), null, 2),
187
+ );
188
+ console.log(
189
+ chalk.gray("----------------------------------------"),
190
+ );
191
+ } catch (e) {
192
+ console.log(
193
+ chalk.red("← API Response (Parse Error):"),
194
+ responseBuffer.toString(),
195
+ );
196
+ }
197
+
198
+ return responseBuffer;
199
+ },
200
+ ),
201
+
202
+ error: (err) => {
203
+ console.error(chalk.red("Proxy Error:"), err);
204
+ },
205
+ },
113
206
  }),
114
207
  ],
115
-
116
208
  notify: false,
117
209
  open: false,
118
210
  ghostMode: false,
119
211
  codeSync: true,
212
+ logLevel: "silent",
120
213
  },
121
214
  (err, bsInstance) => {
122
215
  if (err) {
123
- console.error("BrowserSync failed to start:", err);
216
+ console.error(chalk.red("BrowserSync failed to start:"), err);
124
217
  return;
125
218
  }
126
219
 
127
220
  const urls = bsInstance.getOption("urls");
221
+ const localUrl = urls.get("local");
222
+ const externalUrl = urls.get("external");
223
+ const uiUrl = urls.get("ui");
224
+ const uiExtUrl = urls.get("ui-external");
225
+
226
+ console.log("");
227
+ console.log(chalk.green.bold("✔ Ports Configured:"));
228
+ console.log(
229
+ ` ${chalk.blue.bold("Frontend (BrowserSync):")} ${chalk.magenta(localUrl)}`,
230
+ );
231
+ console.log(
232
+ ` ${chalk.yellow.bold("Backend (PHP Target):")} ${chalk.magenta(
233
+ prismaPhpConfigJson.bsTarget || "http://localhost:80",
234
+ )}`,
235
+ );
236
+ console.log(chalk.gray(" ------------------------------------"));
237
+
238
+ if (externalUrl) {
239
+ console.log(
240
+ ` ${chalk.bold("External:")} ${chalk.magenta(externalUrl)}`,
241
+ );
242
+ }
243
+
244
+ if (uiUrl) {
245
+ console.log(` ${chalk.bold("UI:")} ${chalk.magenta(uiUrl)}`);
246
+ }
247
+
128
248
  const out = {
129
- local: urls.get("local"),
130
- external: urls.get("external"),
131
- ui: urls.get("ui"),
132
- uiExternal: urls.get("ui-external"),
249
+ local: localUrl,
250
+ external: externalUrl,
251
+ ui: uiUrl,
252
+ uiExternal: uiExtUrl,
133
253
  };
134
254
 
135
255
  writeFileSync(
136
256
  join(__dirname, "bs-config.json"),
137
- JSON.stringify(out, null, 2)
257
+ JSON.stringify(out, null, 2),
138
258
  );
139
- console.log("\n\x1b[90mPress Ctrl+C to stop.\x1b[0m\n");
140
- }
259
+ console.log(`\n${chalk.gray("Press Ctrl+C to stop.")}\n`);
260
+ },
141
261
  );
@@ -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
+ }