create-prisma-php-app 4.2.2-beta → 4.2.2

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.
@@ -3,7 +3,7 @@ import { writeFileSync, existsSync, mkdirSync } from "fs";
3
3
  import browserSync, { BrowserSyncInstance } from "browser-sync";
4
4
  import prismaPhpConfigJson from "../prisma-php.json";
5
5
  import { generateFileListJson } from "./files-list.js";
6
- import { join, dirname } from "path";
6
+ import { join, dirname, relative } from "path";
7
7
  import { getFileMeta, PUBLIC_DIR, SRC_DIR } from "./utils.js";
8
8
  import { updateAllClassLogs } from "./class-log.js";
9
9
  import {
@@ -15,9 +15,10 @@ import { checkComponentImports } from "./component-import-checker";
15
15
  import { DebouncedWorker, createSrcWatcher, DEFAULT_AWF } from "./utils.js";
16
16
 
17
17
  const { __dirname } = getFileMeta();
18
-
19
18
  const bs: BrowserSyncInstance = browserSync.create();
20
19
 
20
+ const PUBLIC_IGNORE_DIRS = [''];
21
+
21
22
  const pipeline = new DebouncedWorker(
22
23
  async () => {
23
24
  await generateFileListJson();
@@ -68,7 +69,19 @@ createSrcWatcher(join(SRC_DIR, "**", "*"), {
68
69
  });
69
70
 
70
71
  createSrcWatcher(join(PUBLIC_DIR, "**", "*"), {
71
- onEvent: (_ev, _abs, rel) => publicPipeline.schedule(rel),
72
+ onEvent: (_ev, abs, _) => {
73
+ const relFromPublic = relative(PUBLIC_DIR, abs);
74
+ const normalized = relFromPublic.replace(/\\/g, "/");
75
+
76
+ const segments = normalized.split("/").filter(Boolean);
77
+ const firstSegment = segments[0] || "";
78
+
79
+ if (PUBLIC_IGNORE_DIRS.includes(firstSegment)) {
80
+ return;
81
+ }
82
+
83
+ publicPipeline.schedule(relFromPublic);
84
+ },
72
85
  awaitWriteFinish: DEFAULT_AWF,
73
86
  logPrefix: "watch-public",
74
87
  usePolling: true,
@@ -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
+ }
@@ -29,21 +29,12 @@ class Auth
29
29
  private string $secretKey;
30
30
  private string $defaultTokenValidity = '1h'; // Default to 1 hour
31
31
 
32
- /**
33
- * Private constructor to prevent direct instantiation.
34
- * Use Auth::getInstance() to get the singleton instance.
35
- */
36
32
  private function __construct()
37
33
  {
38
34
  $this->secretKey = $_ENV['AUTH_SECRET'] ?? 'CD24eEv4qbsC5LOzqeaWbcr58mBMSvA4Mkii8GjRiHkt';
39
35
  self::$cookieName = self::getCookieName();
40
36
  }
41
37
 
42
- /**
43
- * Returns the singleton instance of the Auth class.
44
- *
45
- * @return Auth The singleton instance.
46
- */
47
38
  public static function getInstance(): Auth
48
39
  {
49
40
  if (self::$instance === null) {
@@ -53,33 +44,20 @@ class Auth
53
44
  }
54
45
 
55
46
  /**
56
- * Authenticates a user and generates a JWT (JSON Web Token) based on the specified user data
57
- * and token validity duration. The method first checks if the secret key is set, calculates
58
- * the token's expiration time, sets the necessary payload, and encodes it into a JWT.
59
- * If possible (HTTP headers not yet sent), it also sets cookies with the JWT for client-side storage.
47
+ * Authenticates a user and generates a JWT.
48
+ * Optionally redirects the user to a default or custom URL.
60
49
  *
61
- * @param mixed $data User data which can be a simple string or an instance of AuthRole.
62
- * If an instance of AuthRole is provided, its `value` property will be used as the role in the token.
63
- * @param string|null $tokenValidity Optional parameter specifying the duration the token is valid for (e.g., '10m', '1h').
64
- * If null, the default validity period set in the class property is used, which is 1 hour.
65
- * The format should be a number followed by a time unit ('s' for seconds, 'm' for minutes,
66
- * 'h' for hours, 'd' for days), and this is parsed to calculate the exact expiration time.
50
+ * @param mixed $data User data (string or AuthRole).
51
+ * @param string|null $tokenValidity Duration token is valid for (e.g., '1h'). Default is '1h'.
52
+ * @param bool|string $redirect
53
+ * - If `false` (default): No redirect occurs; returns the JWT.
54
+ * - If `true`: Redirects to `AuthConfig::DEFAULT_SIGNIN_REDIRECT`.
55
+ * - If `string`: Redirects to the specified URL (e.g., '/dashboard').
67
56
  *
68
57
  * @return string Returns the encoded JWT as a string.
69
- *
70
- * @throws InvalidArgumentException Thrown if the secret key is not set or if the duration format is invalid.
71
- *
72
- * Example:
73
- * $auth = Auth::getInstance();
74
- * $auth->setSecretKey('your_secret_key');
75
- * try {
76
- * $jwt = $auth->signIn('Admin', '1h');
77
- * echo "JWT: " . $jwt;
78
- * } catch (InvalidArgumentException $e) {
79
- * echo "Error: " . $e->getMessage();
80
- * }
58
+ * @throws InvalidArgumentException
81
59
  */
82
- public function signIn($data, ?string $tokenValidity = null): string
60
+ public function signIn($data, ?string $tokenValidity = null, bool|string $redirect = false): string
83
61
  {
84
62
  if (!$this->secretKey) {
85
63
  throw new InvalidArgumentException("Secret key is required for authentication.");
@@ -96,14 +74,20 @@ class Auth
96
74
  'exp' => $expirationTime,
97
75
  ];
98
76
 
99
- // Set the payload in the session
100
77
  $_SESSION[self::PAYLOAD_SESSION_KEY] = $payload;
101
78
 
102
- // Encode the JWT
103
79
  $jwt = JWT::encode($payload, $this->secretKey, 'HS256');
104
80
 
105
81
  if (!headers_sent()) {
106
82
  $this->setCookies($jwt, $expirationTime);
83
+
84
+ $this->rotateCsrfToken();
85
+ }
86
+
87
+ if ($redirect === true) {
88
+ Request::redirect(AuthConfig::DEFAULT_SIGNIN_REDIRECT);
89
+ } elseif (is_string($redirect) && !empty($redirect)) {
90
+ Request::redirect($redirect);
107
91
  }
108
92
 
109
93
  return $jwt;
@@ -184,19 +168,12 @@ class Auth
184
168
  public function verifyToken(?string $jwt): ?object
185
169
  {
186
170
  try {
187
- if (!$jwt) {
188
- return null;
189
- }
171
+ if (!$jwt) return null;
190
172
 
191
173
  $token = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));
192
174
 
193
- if (empty($token->{Auth::PAYLOAD_NAME})) {
194
- return null;
195
- }
196
-
197
- if (isset($token->exp) && time() >= $token->exp) {
198
- return null;
199
- }
175
+ if (empty($token->{Auth::PAYLOAD_NAME})) return null;
176
+ if (isset($token->exp) && time() >= $token->exp) return null;
200
177
 
201
178
  return $token;
202
179
  } catch (Exception) {
@@ -228,7 +205,6 @@ class Auth
228
205
  }
229
206
 
230
207
  $expirationTime = $this->calculateExpirationTime($tokenValidity ?? $this->defaultTokenValidity);
231
-
232
208
  $decodedToken->exp = $expirationTime;
233
209
  $newJwt = JWT::encode((array)$decodedToken, $this->secretKey, 'HS256');
234
210
 
@@ -244,13 +220,38 @@ class Auth
244
220
  if (!headers_sent()) {
245
221
  setcookie(self::$cookieName, $jwt, [
246
222
  'expires' => $expirationTime,
247
- 'path' => '/', // Set the path to '/' to make the cookie available site-wide
248
- 'domain' => '', // Specify your domain
249
- 'secure' => true, // Set to true if using HTTPS
250
- 'httponly' => true, // Prevent JavaScript access to the cookie
251
- 'samesite' => 'Lax', // or 'Strict' depending on your requirements
223
+ 'path' => '/',
224
+ 'domain' => '',
225
+ 'secure' => true,
226
+ 'httponly' => true,
227
+ 'samesite' => 'Lax',
228
+ ]);
229
+ }
230
+ }
231
+
232
+ public function rotateCsrfToken(): void
233
+ {
234
+ $secret = $_ENV['FUNCTION_CALL_SECRET'] ?? '';
235
+
236
+ if (empty($secret)) {
237
+ return;
238
+ }
239
+
240
+ $nonce = bin2hex(random_bytes(16));
241
+ $signature = hash_hmac('sha256', $nonce, $secret);
242
+ $token = $nonce . '.' . $signature;
243
+
244
+ if (!headers_sent()) {
245
+ setcookie('prisma_php_csrf', $token, [
246
+ 'expires' => time() + 3600, // 1 hour validity
247
+ 'path' => '/',
248
+ 'secure' => true,
249
+ 'httponly' => false, // Must be FALSE so client JS can read it
250
+ 'samesite' => 'Lax',
252
251
  ]);
253
252
  }
253
+
254
+ $_COOKIE['prisma_php_csrf'] = $token;
254
255
  }
255
256
 
256
257
  /**
@@ -276,6 +277,8 @@ class Auth
276
277
  unset($_SESSION[self::PAYLOAD_SESSION_KEY]);
277
278
  }
278
279
 
280
+ $this->rotateCsrfToken();
281
+
279
282
  if ($redirect) {
280
283
  Request::redirect($redirect);
281
284
  }
@@ -8,7 +8,7 @@
8
8
  <ol class="list-inside list-decimal text-sm/6 text-center sm:text-left">
9
9
  <li class="mb-2 tracking-[-.01em]">
10
10
  Get started by editing
11
- <code class="bg-text-foreground/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
11
+ <code class="bg-text-foreground/[.05] dark:bg-white/6 px-1 py-0.5 rounded font-semibold">
12
12
  src/app/index.php
13
13
  </code>
14
14
  .
@@ -46,7 +46,7 @@
46
46
  </div>
47
47
  </main>
48
48
 
49
- <footer class="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
49
+ <footer class="row-start-3 flex gap-6 flex-wrap items-center justify-center">
50
50
  <a
51
51
  class="flex items-center gap-2 hover:underline hover:underline-offset-4"
52
52
  href="https://prismaphp.tsnc.tech/docs?doc=learning-path"
@@ -1,7 +1,6 @@
1
1
  <?php
2
2
 
3
3
  use PP\MainLayout;
4
- use PP\Request;
5
4
 
6
5
  MainLayout::$title = !empty(MainLayout::$title) ? MainLayout::$title : 'Create Prisma PHP App';
7
6
  MainLayout::$description = !empty(MainLayout::$description) ? MainLayout::$description : 'Generated by create Prisma PHP App';
@@ -16,7 +15,7 @@ MainLayout::$description = !empty(MainLayout::$description) ? MainLayout::$descr
16
15
  <!-- Dynamic Header Scripts -->
17
16
  </head>
18
17
 
19
- <body>
18
+ <body pp-spa="true" style="opacity:0;pointer-events:none;user-select:none;transition:opacity .18s ease-out;">
20
19
  <?= MainLayout::$children; ?>
21
20
  <!-- Dynamic Footer Scripts -->
22
21
  </body>
@@ -1,7 +1,7 @@
1
- <div class="flex flex-col min-h-[100vh] items-center justify-center space-y-4 text-center">
1
+ <div class="flex flex-col min-h-screen items-center justify-center space-y-4 text-center">
2
2
  <div class="space-y-2">
3
3
  <h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">404 Not Found</h1>
4
- <p class="max-w-[600px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
4
+ <p class="max-w-150 text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
5
5
  Sorry, we couldn't find the page you're looking for.
6
6
  </p>
7
7
  </div>
@@ -106,6 +106,6 @@
106
106
  // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107
107
  "skipLibCheck": true /* Skip type checking all .d.ts files. */
108
108
  },
109
- "include": ["**/*.ts"],
109
+ "include": ["**/*.ts", ".pp/**/*.d.ts"],
110
110
  "exclude": ["node_modules", "vendor"]
111
111
  }
@@ -2,6 +2,7 @@ import { defineConfig, Plugin } from "vite";
2
2
  import path from "path";
3
3
  import fg from "fast-glob";
4
4
  import { writeFileSync } from "fs";
5
+ import { generateGlobalTypes } from "./settings/vite-plugins/generate-global-types.js";
5
6
 
6
7
  const entries = Object.fromEntries(
7
8
  fg.sync("ts/**/*.ts", { ignore: ["**/*.test.ts"] }).map((f) => {
@@ -10,6 +11,13 @@ const entries = Object.fromEntries(
10
11
  })
11
12
  );
12
13
 
14
+ const VITE_WATCH_EXCLUDE = [
15
+ "public/js/**",
16
+ "node_modules/**",
17
+ "vendor/**",
18
+ ".pp/**",
19
+ ];
20
+
13
21
  function browserSyncNotify(): Plugin {
14
22
  const flagFile = path.resolve(__dirname, ".pp", ".vite-build-complete");
15
23
 
@@ -21,16 +29,17 @@ function browserSyncNotify(): Plugin {
21
29
  };
22
30
  }
23
31
 
24
- export default defineConfig({
32
+ export default defineConfig(({ command, mode }) => ({
25
33
  publicDir: false,
26
34
  build: {
27
35
  outDir: "public/js",
28
36
  emptyOutDir: false,
29
37
  minify: "esbuild",
30
38
  sourcemap: false,
31
- watch: {
32
- exclude: ["public/**", "node_modules/**"],
33
- },
39
+ watch:
40
+ command === "build" && mode === "development"
41
+ ? { exclude: VITE_WATCH_EXCLUDE }
42
+ : undefined,
34
43
  rollupOptions: {
35
44
  input: entries,
36
45
  external: [/^\/js\/.*/],
@@ -41,7 +50,12 @@ export default defineConfig({
41
50
  },
42
51
  },
43
52
  },
44
- plugins: [browserSyncNotify()],
53
+ plugins: [
54
+ generateGlobalTypes(),
55
+ ...(command === "build" && mode === "development"
56
+ ? [browserSyncNotify()]
57
+ : []),
58
+ ],
45
59
  esbuild: { legalComments: "none" },
46
60
  define: { "process.env.NODE_ENV": '"production"' },
47
- });
61
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-prisma-php-app",
3
- "version": "4.2.2-beta",
3
+ "version": "4.2.2",
4
4
  "description": "Prisma-PHP: A Revolutionary Library Bridging PHP with Prisma ORM",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -32,15 +32,15 @@
32
32
  "author": "Jefferson Abraham Omier <thesteelninjacode@gmail.com>",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "chalk": "^5.3.0",
35
+ "chalk": "^5.6.2",
36
36
  "crypto-js": "^4.2.0",
37
37
  "prompts": "^2.4.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/crypto-js": "^4.2.2",
41
- "@types/node": "^20.11.7",
41
+ "@types/node": "^25.0.3",
42
42
  "@types/prompts": "^2.4.9",
43
43
  "ts-node": "^10.9.2",
44
- "typescript": "^5.3.3"
44
+ "typescript": "^5.9.3"
45
45
  }
46
46
  }