create-prisma-php-app 5.0.0-alpha.7 → 5.0.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.
Files changed (61) hide show
  1. package/README.md +23 -2
  2. package/dist/.github/copilot-instructions.md +191 -0
  3. package/dist/AGENTS.md +588 -193
  4. package/dist/bootstrap.php +426 -213
  5. package/dist/index.js +2 -2
  6. package/dist/phpunit.xml +25 -0
  7. package/dist/postcss.config.js +4 -2
  8. package/dist/prisma-php.js +2 -2
  9. package/dist/public/.htaccess +1 -1
  10. package/dist/public/js/main.js +13 -1
  11. package/dist/public/js/pp-reactive-v2.min.js +1 -0
  12. package/dist/settings/auto-swagger-docs.ts +10 -10
  13. package/dist/settings/bs-config.ts +47 -27
  14. package/dist/settings/build.ts +2 -32
  15. package/dist/settings/component-map.ts +476 -0
  16. package/dist/settings/files-list.ts +2 -2
  17. package/dist/settings/project-name.ts +3 -6
  18. package/dist/settings/restart-mcp.ts +2 -2
  19. package/dist/settings/restart-websocket.ts +2 -2
  20. package/dist/settings/run-postcss.ts +205 -0
  21. package/dist/settings/run-tests.ts +35 -0
  22. package/dist/settings/swagger-config.ts +3 -3
  23. package/dist/settings/utils.ts +3 -3
  24. package/dist/src/Lib/Auth/Auth.php +63 -35
  25. package/dist/src/Lib/MCP/mcp-server.php +2 -3
  26. package/dist/src/Lib/Middleware/AuthMiddleware.php +2 -2
  27. package/dist/src/Lib/Middleware/CorsMiddleware.php +72 -0
  28. package/dist/src/Lib/Websocket/ConnectionManager.php +460 -7
  29. package/dist/src/Lib/Websocket/Socket.php +170 -0
  30. package/dist/src/Lib/Websocket/SocketPool.php +50 -0
  31. package/dist/src/Lib/Websocket/SocketRegistry.php +88 -0
  32. package/dist/src/Lib/Websocket/sockets.php +50 -0
  33. package/dist/src/Lib/Websocket/websocket-server.php +10 -3
  34. package/dist/src/app/globals.css +3 -1
  35. package/dist/src/app/layout.php +1 -1
  36. package/dist/tests/AuthTest.php +59 -0
  37. package/dist/tests/ConnectionManagerTest.php +277 -0
  38. package/dist/tests/CsrfTest.php +119 -0
  39. package/dist/tests/DeferComponentRootsTest.php +147 -0
  40. package/dist/tests/FeaturesTest.php +41 -0
  41. package/dist/tests/README.md +119 -0
  42. package/dist/tests/RpcWireContractTest.php +101 -0
  43. package/dist/tests/SocketPoolTest.php +69 -0
  44. package/dist/tests/SocketRegistryTest.php +77 -0
  45. package/dist/tests/SocketTest.php +124 -0
  46. package/dist/tests/SocketsRegistrationTest.php +40 -0
  47. package/dist/tests/Support/FakeConnection.php +62 -0
  48. package/dist/tests/Support/Features.php +38 -0
  49. package/dist/tests/Support/RequiresFeature.php +26 -0
  50. package/dist/tests/bootstrap.php +44 -0
  51. package/dist/ts/main.ts +21 -3
  52. package/dist/ts/tailwind-merge.ts +13 -0
  53. package/package.json +4 -4
  54. package/dist/README.md +0 -213
  55. package/dist/public/js/pp-reactive-v2.js +0 -1
  56. package/dist/settings/bs-config.json +0 -6
  57. package/dist/settings/class-imports.ts +0 -165
  58. package/dist/settings/class-log.ts +0 -244
  59. package/dist/settings/component-import-checker.ts +0 -90
  60. package/dist/settings/files-list.json +0 -1
  61. package/dist/settings/prisma-schema-config.json +0 -16
@@ -0,0 +1,476 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import { Engine } from "php-parser";
4
+ import { getFileMeta } from "./utils.js";
5
+
6
+ const { __dirname } = getFileMeta();
7
+
8
+ const parser = new Engine({
9
+ parser: {
10
+ php8: true,
11
+ suppressErrors: true,
12
+ },
13
+ ast: {
14
+ withPositions: false,
15
+ },
16
+ });
17
+
18
+ const PROJECT_ROOT = path.join(__dirname, "..");
19
+ const CONFIG_FILE = path.join(PROJECT_ROOT, "prisma-php.json");
20
+ const COMPONENT_MAP_FILE = path.join(
21
+ PROJECT_ROOT,
22
+ "settings/component-map.json",
23
+ );
24
+
25
+ export const SRC_DIR = path.join(PROJECT_ROOT, "src");
26
+
27
+ const PHPX_BASE_CLASS = "PHPX";
28
+
29
+ type JsonValue =
30
+ | string
31
+ | number
32
+ | boolean
33
+ | null
34
+ | JsonValue[]
35
+ | { [key: string]: JsonValue };
36
+
37
+ export interface ComponentMapProp {
38
+ name: string;
39
+ type: string;
40
+ hasDefault: boolean;
41
+ defaultValue: JsonValue | string;
42
+ }
43
+
44
+ export interface ComponentMapEntry {
45
+ componentName: string;
46
+ tagName: string;
47
+ filePath: string;
48
+ relativePath: string;
49
+ importRoute: string;
50
+ extendsPHPX: boolean;
51
+ acceptsArbitraryProps: boolean;
52
+ props: ComponentMapProp[];
53
+ }
54
+
55
+ interface PrismaPhpConfig {
56
+ projectRootPath?: string;
57
+ excludeFiles?: string[];
58
+ componentScanDirs?: string[];
59
+ }
60
+
61
+ type DiscoveredComponent = ComponentMapEntry;
62
+
63
+ async function loadConfig(): Promise<PrismaPhpConfig> {
64
+ try {
65
+ const raw = await fs.readFile(CONFIG_FILE, "utf-8");
66
+ return JSON.parse(raw) as PrismaPhpConfig;
67
+ } catch {
68
+ return {};
69
+ }
70
+ }
71
+
72
+ function resolveProjectRoot(config: PrismaPhpConfig): string {
73
+ if (config.projectRootPath && path.isAbsolute(config.projectRootPath)) {
74
+ return config.projectRootPath;
75
+ }
76
+
77
+ return PROJECT_ROOT;
78
+ }
79
+
80
+ function resolveScanRoots(
81
+ config: PrismaPhpConfig,
82
+ projectRoot: string,
83
+ ): string[] {
84
+ const scanDirs =
85
+ Array.isArray(config.componentScanDirs) &&
86
+ config.componentScanDirs.length > 0
87
+ ? config.componentScanDirs
88
+ : ["src"];
89
+
90
+ return scanDirs.map((dirPath) =>
91
+ path.isAbsolute(dirPath) ? dirPath : path.join(projectRoot, dirPath),
92
+ );
93
+ }
94
+
95
+ function normalizePathKey(filePath: string): string {
96
+ return path.normalize(filePath).toLowerCase();
97
+ }
98
+
99
+ function toAbsoluteComponentPath(filePath: string): string {
100
+ return path.normalize(filePath);
101
+ }
102
+
103
+ function toProjectRelativePath(filePath: string, projectRoot: string): string {
104
+ return path.relative(projectRoot, filePath).replace(/\\/g, "/");
105
+ }
106
+
107
+ async function saveJsonFile(filePath: string, value: unknown): Promise<void> {
108
+ await fs.writeFile(filePath, JSON.stringify(value, null, 2), "utf-8");
109
+ }
110
+
111
+ export function componentNameToTagName(componentName: string): string {
112
+ const kebabName = componentName
113
+ .replace(/([a-z\d])([A-Z])/g, "$1-$2")
114
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
115
+ .replace(/[_.\s]+/g, "-")
116
+ .toLowerCase();
117
+
118
+ return `x-${kebabName}`;
119
+ }
120
+
121
+ export async function getAllPhpFiles(dir: string): Promise<string[]> {
122
+ const files: string[] = [];
123
+
124
+ try {
125
+ const entries = await fs.readdir(dir, { withFileTypes: true });
126
+
127
+ for (const entry of entries) {
128
+ const fullPath = path.join(dir, entry.name);
129
+
130
+ if (entry.isDirectory()) {
131
+ files.push(...(await getAllPhpFiles(fullPath)));
132
+ } else if (entry.isFile() && fullPath.toLowerCase().endsWith(".php")) {
133
+ files.push(fullPath);
134
+ }
135
+ }
136
+ } catch {
137
+ return files;
138
+ }
139
+
140
+ return files;
141
+ }
142
+
143
+ function nameToString(node: any): string {
144
+ if (!node) {
145
+ return "";
146
+ }
147
+
148
+ if (typeof node === "string") {
149
+ return node;
150
+ }
151
+
152
+ if (typeof node.raw === "string" && node.raw.trim() !== "") {
153
+ return node.raw;
154
+ }
155
+
156
+ if (typeof node.name === "string") {
157
+ return node.name;
158
+ }
159
+
160
+ if (Array.isArray(node.name)) {
161
+ return node.name
162
+ .map((part: any) =>
163
+ typeof part === "string" ? part : (part?.name ?? ""),
164
+ )
165
+ .filter(Boolean)
166
+ .join("\\");
167
+ }
168
+
169
+ if (Array.isArray(node.items)) {
170
+ return node.items.map(nameToString).filter(Boolean).join("|");
171
+ }
172
+
173
+ return String(node.name ?? "");
174
+ }
175
+
176
+ function typeToString(typeNode: any, nullable = false): string {
177
+ if (!typeNode) {
178
+ return "mixed";
179
+ }
180
+
181
+ let rawType = "";
182
+
183
+ if (typeof typeNode.raw === "string" && typeNode.raw.trim() !== "") {
184
+ rawType = typeNode.raw.replace(/\s+/g, "");
185
+ } else if (Array.isArray(typeNode.types)) {
186
+ rawType = typeNode.types.map((item: any) => typeToString(item)).join("|");
187
+ } else {
188
+ rawType = nameToString(typeNode).replace(/\s+/g, "");
189
+ }
190
+
191
+ if (
192
+ nullable &&
193
+ rawType !== "mixed" &&
194
+ !rawType.startsWith("?") &&
195
+ !rawType.split("|").includes("null")
196
+ ) {
197
+ return `?${rawType}`;
198
+ }
199
+
200
+ return rawType || "mixed";
201
+ }
202
+
203
+ function astValueToJson(valueNode: any): JsonValue | string {
204
+ if (!valueNode || typeof valueNode !== "object") {
205
+ return null;
206
+ }
207
+
208
+ switch (valueNode.kind) {
209
+ case "string":
210
+ return valueNode.value ?? "";
211
+ case "number":
212
+ return Number(valueNode.value ?? valueNode.raw ?? 0);
213
+ case "boolean":
214
+ return Boolean(valueNode.value);
215
+ case "nullkeyword":
216
+ return null;
217
+ case "array": {
218
+ const items = Array.isArray(valueNode.items) ? valueNode.items : [];
219
+ const hasNamedKeys = items.some((item: any) => item?.key);
220
+
221
+ if (!hasNamedKeys) {
222
+ return items.map((item: any) => astValueToJson(item?.value ?? item));
223
+ }
224
+
225
+ const mapped: Record<string, JsonValue | string> = {};
226
+
227
+ for (const item of items) {
228
+ const key = item?.key ? String(astValueToJson(item.key)) : "";
229
+ mapped[key] = astValueToJson(item?.value);
230
+ }
231
+
232
+ return mapped;
233
+ }
234
+ case "unary": {
235
+ const inner = astValueToJson(valueNode.what);
236
+
237
+ if (typeof inner === "number") {
238
+ return valueNode.type === "-" ? -inner : inner;
239
+ }
240
+
241
+ return typeof valueNode.raw === "string" ? valueNode.raw : String(inner);
242
+ }
243
+ case "constref": {
244
+ const constantName = nameToString(valueNode.name).toLowerCase();
245
+
246
+ if (constantName === "true") {
247
+ return true;
248
+ }
249
+
250
+ if (constantName === "false") {
251
+ return false;
252
+ }
253
+
254
+ if (constantName === "null") {
255
+ return null;
256
+ }
257
+
258
+ return nameToString(valueNode.name);
259
+ }
260
+ default:
261
+ if (typeof valueNode.raw === "string" && valueNode.raw.trim() !== "") {
262
+ return valueNode.raw;
263
+ }
264
+
265
+ return nameToString(valueNode);
266
+ }
267
+ }
268
+
269
+ function extractComponentProps(classNode: any): ComponentMapProp[] {
270
+ const props: ComponentMapProp[] = [];
271
+
272
+ for (const statement of classNode.body ?? []) {
273
+ if (
274
+ !statement ||
275
+ statement.kind !== "propertystatement" ||
276
+ statement.visibility !== "public" ||
277
+ statement.isStatic
278
+ ) {
279
+ continue;
280
+ }
281
+
282
+ for (const property of statement.properties ?? []) {
283
+ props.push({
284
+ name: nameToString(property.name),
285
+ type: typeToString(property.type, Boolean(property.nullable)),
286
+ hasDefault: property.value !== undefined && property.value !== null,
287
+ defaultValue:
288
+ property.value !== undefined && property.value !== null
289
+ ? astValueToJson(property.value)
290
+ : null,
291
+ });
292
+ }
293
+ }
294
+
295
+ return props;
296
+ }
297
+
298
+ function classExtendsPHPX(classNode: any): boolean {
299
+ if (!classNode?.extends) {
300
+ return false;
301
+ }
302
+
303
+ const name = nameToString(classNode.extends);
304
+ return name.split("\\").pop() === PHPX_BASE_CLASS;
305
+ }
306
+
307
+ export async function analyzeComponentsInFile(
308
+ filePath: string,
309
+ projectRoot: string,
310
+ ): Promise<DiscoveredComponent[]> {
311
+ const code = await fs.readFile(filePath, "utf-8");
312
+
313
+ try {
314
+ const ast = parser.parseCode(code, filePath);
315
+ const components: DiscoveredComponent[] = [];
316
+
317
+ function traverse(node: any, currentNamespace = "") {
318
+ if (Array.isArray(node)) {
319
+ node.forEach((childNode) => traverse(childNode, currentNamespace));
320
+ return;
321
+ }
322
+
323
+ if (!node || typeof node !== "object") {
324
+ return;
325
+ }
326
+
327
+ if (node.kind === "namespace") {
328
+ const nextNamespace = nameToString(node.name).replace(/^\\+/, "");
329
+
330
+ for (const [key, value] of Object.entries(node)) {
331
+ if (key === "kind" || key === "name") {
332
+ continue;
333
+ }
334
+
335
+ traverse(value, nextNamespace || currentNamespace);
336
+ }
337
+
338
+ return;
339
+ }
340
+
341
+ if (node.kind === "class" && node.name?.name) {
342
+ const extendsPHPX = classExtendsPHPX(node);
343
+
344
+ if (extendsPHPX) {
345
+ const componentName = node.name.name as string;
346
+ const importRoute =
347
+ (currentNamespace ? `${currentNamespace}\\` : "") + componentName;
348
+
349
+ components.push({
350
+ componentName,
351
+ tagName: componentNameToTagName(componentName),
352
+ filePath: toAbsoluteComponentPath(filePath),
353
+ relativePath: toProjectRelativePath(filePath, projectRoot),
354
+ importRoute,
355
+ extendsPHPX,
356
+ acceptsArbitraryProps: extendsPHPX,
357
+ props: extractComponentProps(node),
358
+ });
359
+ }
360
+ }
361
+
362
+ for (const [key, value] of Object.entries(node)) {
363
+ if (key === "kind" || key === "name") {
364
+ continue;
365
+ }
366
+
367
+ traverse(value, currentNamespace);
368
+ }
369
+ }
370
+
371
+ traverse(ast);
372
+ return components;
373
+ } catch (error) {
374
+ console.error(`Error parsing component file: ${filePath}`, error);
375
+ return [];
376
+ }
377
+ }
378
+
379
+ function sortComponentMap(
380
+ componentMap: DiscoveredComponent[],
381
+ ): DiscoveredComponent[] {
382
+ return [...componentMap].sort((left, right) => {
383
+ const nameComparison = left.componentName.localeCompare(
384
+ right.componentName,
385
+ );
386
+
387
+ if (nameComparison !== 0) {
388
+ return nameComparison;
389
+ }
390
+
391
+ return left.relativePath.localeCompare(right.relativePath);
392
+ });
393
+ }
394
+
395
+ export function buildRuntimeComponentLookup(
396
+ componentMap: ComponentMapEntry[],
397
+ ): Record<
398
+ string,
399
+ Array<{
400
+ tagName: string;
401
+ componentName: string;
402
+ className: string;
403
+ filePath: string;
404
+ }>
405
+ > {
406
+ const lookup: Record<
407
+ string,
408
+ Array<{
409
+ tagName: string;
410
+ componentName: string;
411
+ className: string;
412
+ filePath: string;
413
+ }>
414
+ > = {};
415
+
416
+ for (const entry of componentMap) {
417
+ const tagName = entry.tagName.toLowerCase();
418
+
419
+ if (!lookup[tagName]) {
420
+ lookup[tagName] = [];
421
+ }
422
+
423
+ lookup[tagName].push({
424
+ tagName,
425
+ componentName: entry.componentName,
426
+ className: entry.importRoute,
427
+ filePath: entry.filePath,
428
+ });
429
+ }
430
+
431
+ return Object.fromEntries(
432
+ Object.entries(lookup).sort(([left], [right]) => left.localeCompare(right)),
433
+ );
434
+ }
435
+
436
+ export async function updateComponentMap(): Promise<ComponentMapEntry[]> {
437
+ const config = await loadConfig();
438
+ const projectRoot = resolveProjectRoot(config);
439
+ const scanRoots = resolveScanRoots(config, projectRoot);
440
+ const excludedFiles = new Set(
441
+ (config.excludeFiles ?? []).map((filePath) =>
442
+ normalizePathKey(
443
+ path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath),
444
+ ),
445
+ ),
446
+ );
447
+
448
+ const phpFiles: string[] = [];
449
+
450
+ for (const scanRoot of scanRoots) {
451
+ const discoveredFiles = await getAllPhpFiles(scanRoot);
452
+
453
+ for (const filePath of discoveredFiles) {
454
+ if (!excludedFiles.has(normalizePathKey(filePath))) {
455
+ phpFiles.push(filePath);
456
+ }
457
+ }
458
+ }
459
+
460
+ const uniquePhpFiles = [
461
+ ...new Set(phpFiles.map((filePath) => path.normalize(filePath))),
462
+ ];
463
+ const discoveredComponents: DiscoveredComponent[] = [];
464
+
465
+ for (const filePath of uniquePhpFiles) {
466
+ discoveredComponents.push(
467
+ ...(await analyzeComponentsInFile(filePath, projectRoot)),
468
+ );
469
+ }
470
+
471
+ const sortedComponents = sortComponentMap(discoveredComponents);
472
+ const componentMap: ComponentMapEntry[] = [...sortedComponents];
473
+ await saveJsonFile(COMPONENT_MAP_FILE, componentMap);
474
+
475
+ return componentMap;
476
+ }
@@ -23,7 +23,7 @@ const getAllFiles = (dirPath: string): string[] => {
23
23
  } else {
24
24
  const relativePath = `.${sep}${relative(
25
25
  join(__dirname, ".."),
26
- fullPath
26
+ fullPath,
27
27
  )}`;
28
28
  files.push(relativePath.replace(/\\/g, "/").replace(/^\.\.\//, ""));
29
29
  }
@@ -41,7 +41,7 @@ export const generateFileListJson = async (): Promise<void> => {
41
41
  if (allFiles.length > 0) {
42
42
  writeFileSync(jsonFilePath, JSON.stringify(allFiles, null, 2));
43
43
  console.log(
44
- `File list generated: ${appFiles.length} app files, ${publicFiles.length} public files`
44
+ `File list generated: ${appFiles.length} app files, ${publicFiles.length} public files`,
45
45
  );
46
46
  } else {
47
47
  console.error("No files found to save in the JSON file.");
@@ -3,8 +3,7 @@ import { join, basename, dirname, normalize, sep } from "path";
3
3
  import prismaPhpConfigJson from "../prisma-php.json";
4
4
  import { getFileMeta } from "./utils.js";
5
5
  import { promises as fsPromises } from "fs";
6
- import { updateAllClassLogs } from "./class-log";
7
- import { updateComponentImports } from "./class-imports";
6
+ import { updateComponentMap } from "./component-map";
8
7
  import { generateFileListJson } from "./files-list";
9
8
 
10
9
  const { __dirname } = getFileMeta();
@@ -152,8 +151,7 @@ export async function deleteDirectoriesIfExist(
152
151
 
153
152
  export const filesToDelete = [
154
153
  join(__dirname, "request-data.json"),
155
- join(__dirname, "class-log.json"),
156
- join(__dirname, "class-imports.json"),
154
+ join(__dirname, "component-map.json"),
157
155
  ];
158
156
 
159
157
  export const dirsToDelete = [
@@ -164,5 +162,4 @@ export const dirsToDelete = [
164
162
  await deleteFilesIfExist(filesToDelete);
165
163
  await deleteDirectoriesIfExist(dirsToDelete);
166
164
  await generateFileListJson();
167
- await updateAllClassLogs();
168
- await updateComponentImports();
165
+ await updateComponentMap();
@@ -19,7 +19,7 @@ const serverScriptPath = join(
19
19
  "src",
20
20
  "Lib",
21
21
  "MCP",
22
- "mcp-server.php"
22
+ "mcp-server.php",
23
23
  );
24
24
  const watchRoot = join(__dirname, "..", "src");
25
25
 
@@ -37,7 +37,7 @@ const restarter = new DebouncedWorker(
37
37
  await mcp.restart("file change");
38
38
  },
39
39
  250,
40
- "mcp-restart"
40
+ "mcp-restart",
41
41
  );
42
42
 
43
43
  createSrcWatcher(watchRoot, {
@@ -13,7 +13,7 @@ const serverScriptPath = join(
13
13
  SRC_DIR,
14
14
  "Lib",
15
15
  "Websocket",
16
- "websocket-server.php"
16
+ "websocket-server.php",
17
17
  );
18
18
 
19
19
  const ws = createRestartableProcess({
@@ -30,7 +30,7 @@ const restarter = new DebouncedWorker(
30
30
  await ws.restart("file change");
31
31
  },
32
32
  400,
33
- "ws-restart"
33
+ "ws-restart",
34
34
  );
35
35
 
36
36
  createSrcWatcher(SRC_DIR, {