create-prisma-php-app 3.3.2 → 3.3.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.
Files changed (2) hide show
  1. package/dist/index.js +826 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,715 @@
1
1
  #!/usr/bin/env node
2
- import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],dockerFiles=[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"];function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let c=`http://localhost/${n}`;c=c.endsWith("/")?c.slice(0,-1):c;const o=c.replace(/(?<!:)(\/\/+)/g,"/"),i=n.replace(/\/\/+/g,"/");return{bsTarget:`${o}/`,bsPathRewrite:{"^/":`/${i.startsWith("/")?i.substring(1):i}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let c=[];if(s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch","tailwind:build":"postcss src/app/css/tailwind.css -o src/app/css/styles.css"},c.push("tailwind")),s.websocket&&(n.scripts={...n.scripts,websocket:"tsx settings/restart-websocket.ts"},c.push("websocket")),s.docker&&(n.scripts={...n.scripts,docker:"docker-compose up"},c.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";n.scripts={...n.scripts,"create-swagger-docs":e}}let o={...n.scripts};o.browserSync="tsx settings/bs-config.ts",o["browserSync:build"]="tsx settings/build.ts",o.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`,o.build=`npm-run-all${s.tailwindcss?" tailwind:build":""} browserSync:build`,n.scripts=o,n.type="module",fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,n,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),c=n&&fs.statSync(e);if(n&&c&&c.isDirectory()){const n=s.toLowerCase();if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(t.backendOnly&&n.includes("src\\app\\js")||t.backendOnly&&n.includes("src\\app\\css")||t.backendOnly&&n.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&n.includes("src\\app\\swagger-docs"))return;const c=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&(s.includes("restart-websocket.ts")||s.includes("restart-websocket.bat")))return;if(!t.docker&&dockerFiles.some((e=>s.includes(e))))return;if(t.backendOnly&&nonBackendFiles.some((e=>s.includes(e))))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({src:s,dest:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)}))}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),n+='\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let c="";s.backendOnly||(c=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${n}`:n),e=e.replace("</head>",`${c}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){return!!updateAnswer?.isUpdate&&(updateAnswer?.excludeFilePath?.includes(e.replace(/\\/g,"/"))??!1)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&n.push({src:"/.dockerignore",dest:"/.dockerignore"},{src:"/docker-compose.yml",dest:"/docker-compose.yml"},{src:"/Dockerfile",dest:"/Dockerfile"},{src:"/apache.conf",dest:"/apache.conf"}),t.forEach((({src:s,dest:t})=>{const n=path.join(__dirname,s),c=path.join(e,t);if(checkExcludeFiles(c))return;const o=fs.readFileSync(n,"utf8");fs.writeFileSync(c,o,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const c=generateAuthSecret(),o=generateHexEncodedKey(),i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${c}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# PHPMailer SMTP configuration (uncomment and set as needed)\n# SMTP_HOST="smtp.gmail.com" # Your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username\n# SMTP_PASSWORD="123456" # Your SMTP password\n# SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port\n# SMTP_ENCRYPTION="ssl" # ssl or tls\n# MAIL_FROM="john.doe@gmail.com" # Sender email address\n# MAIL_FROM_NAME="John Doe" # Sender name\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Local storage key for browser storage (auto-generated if not set).\n# Spaces will be replaced with underscores and converted to lowercase.\nLOCALSTORE_KEY="${o}"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={}){const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||s.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{process.exit(0)},n=await prompts(s,{onCancel:t}),c=[];n.backendOnly??e.backendOnly??!1?(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!0,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!0,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const o=await prompts(c,{onCancel:t});return{projectName:n.projectName?String(n.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:n.backendOnly??e.backendOnly??!1,swaggerDocs:o.swaggerDocs??e.swaggerDocs??!1,tailwindcss:o.tailwindcss??e.tailwindcss??!1,websocket:o.websocket??e.websocket??!1,prisma:o.prisma??e.prisma??!1,docker:o.docker??e.docker??!1}}async function uninstallNpmDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){s.forEach((e=>{}));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let n="";e.on("data",(e=>n+=e)),e.on("end",(()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:null}catch(e){return null}}
2
+ import { execSync, spawnSync } from "child_process";
3
+ import fs from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import path from "path";
6
+ import chalk from "chalk";
7
+ import prompts from "prompts";
8
+ import https from "https";
9
+ import { randomBytes } from "crypto";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ let updateAnswer = null;
13
+ const nonBackendFiles = [
14
+ "favicon.ico",
15
+ "\\src\\app\\index.php",
16
+ "metadata.php",
17
+ "not-found.php",
18
+ "error.php",
19
+ ];
20
+ const dockerFiles = [
21
+ ".dockerignore",
22
+ "docker-compose.yml",
23
+ "Dockerfile",
24
+ "apache.conf",
25
+ ];
26
+ function bsConfigUrls(projectRootPath) {
27
+ // Identify the base path dynamically up to and including 'htdocs'
28
+ const htdocsIndex = projectRootPath.indexOf("\\htdocs\\");
29
+ if (htdocsIndex === -1) {
30
+ console.error(
31
+ "Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"
32
+ );
33
+ return {
34
+ bsTarget: "",
35
+ bsPathRewrite: {},
36
+ };
37
+ }
38
+ // Extract the path up to and including 'htdocs\\'
39
+ const basePathToRemove = projectRootPath.substring(
40
+ 0,
41
+ htdocsIndex + "\\htdocs\\".length
42
+ );
43
+ // Escape backslashes for the regex pattern
44
+ const escapedBasePathToRemove = basePathToRemove.replace(/\\/g, "\\\\");
45
+ // Remove the base path and replace backslashes with forward slashes for URL compatibility
46
+ const relativeWebPath = projectRootPath
47
+ .replace(new RegExp(`^${escapedBasePathToRemove}`), "")
48
+ .replace(/\\/g, "/");
49
+ // Construct the Browser Sync command with the correct proxy URL, being careful not to affect the protocol part
50
+ let proxyUrl = `http://localhost/${relativeWebPath}`;
51
+ // Ensure the proxy URL does not end with a slash before appending '/public'
52
+ proxyUrl = proxyUrl.endsWith("/") ? proxyUrl.slice(0, -1) : proxyUrl;
53
+ // Clean the URL by replacing "//" with "/" but not affecting "http://"
54
+ // We replace instances of "//" that are not preceded by ":"
55
+ const cleanUrl = proxyUrl.replace(/(?<!:)(\/\/+)/g, "/");
56
+ const cleanRelativeWebPath = relativeWebPath.replace(/\/\/+/g, "/");
57
+ // Correct the relativeWebPath to ensure it does not start with a "/"
58
+ const adjustedRelativeWebPath = cleanRelativeWebPath.startsWith("/")
59
+ ? cleanRelativeWebPath.substring(1)
60
+ : cleanRelativeWebPath;
61
+ return {
62
+ bsTarget: `${cleanUrl}/`,
63
+ bsPathRewrite: {
64
+ "^/": `/${adjustedRelativeWebPath}/`,
65
+ },
66
+ };
67
+ }
68
+ async function updatePackageJson(baseDir, answer) {
69
+ const packageJsonPath = path.join(baseDir, "package.json");
70
+ if (checkExcludeFiles(packageJsonPath)) return;
71
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
72
+ packageJson.scripts = {
73
+ ...packageJson.scripts,
74
+ projectName: "tsx settings/project-name.ts",
75
+ };
76
+ let answersToInclude = [];
77
+ if (answer.tailwindcss) {
78
+ packageJson.scripts = {
79
+ ...packageJson.scripts,
80
+ tailwind:
81
+ "postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch",
82
+ "tailwind:build":
83
+ "postcss src/app/css/tailwind.css -o src/app/css/styles.css",
84
+ };
85
+ answersToInclude.push("tailwind");
86
+ }
87
+ if (answer.websocket) {
88
+ packageJson.scripts = {
89
+ ...packageJson.scripts,
90
+ websocket: "tsx settings/restart-websocket.ts",
91
+ };
92
+ answersToInclude.push("websocket");
93
+ }
94
+ if (answer.docker) {
95
+ packageJson.scripts = {
96
+ ...packageJson.scripts,
97
+ docker: "docker-compose up",
98
+ };
99
+ answersToInclude.push("docker");
100
+ }
101
+ if (answer.swaggerDocs) {
102
+ const swaggerDocsExecuteScript = answer.prisma
103
+ ? "tsx settings/auto-swagger-docs.ts"
104
+ : "tsx settings/swagger-config.ts";
105
+ packageJson.scripts = {
106
+ ...packageJson.scripts,
107
+ "create-swagger-docs": swaggerDocsExecuteScript,
108
+ };
109
+ }
110
+ // Initialize with existing scripts
111
+ let updatedScripts = {
112
+ ...packageJson.scripts,
113
+ };
114
+ updatedScripts.browserSync = "tsx settings/bs-config.ts";
115
+ updatedScripts["browserSync:build"] = "tsx settings/build.ts";
116
+ updatedScripts.dev = `npm-run-all projectName -p browserSync ${answersToInclude.join(
117
+ " "
118
+ )}`;
119
+ updatedScripts.build = `npm-run-all${
120
+ answer.tailwindcss ? " tailwind:build" : ""
121
+ } browserSync:build`;
122
+ // Finally, assign the updated scripts back to packageJson
123
+ packageJson.scripts = updatedScripts;
124
+ packageJson.type = "module";
125
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
126
+ }
127
+ async function updateComposerJson(baseDir) {
128
+ const composerJsonPath = path.join(baseDir, "composer.json");
129
+ if (checkExcludeFiles(composerJsonPath)) return;
130
+ }
131
+ async function updateIndexJsForWebSocket(baseDir, answer) {
132
+ if (!answer.websocket) {
133
+ return;
134
+ }
135
+ const indexPath = path.join(baseDir, "src", "app", "js", "index.js");
136
+ if (checkExcludeFiles(indexPath)) return;
137
+ let indexContent = fs.readFileSync(indexPath, "utf8");
138
+ // WebSocket initialization code to be appended
139
+ const webSocketCode = `
140
+ // WebSocket initialization
141
+ var ws = new WebSocket("ws://localhost:8080");
142
+ `;
143
+ // Append WebSocket code if user chose to use WebSocket
144
+ indexContent += webSocketCode;
145
+ fs.writeFileSync(indexPath, indexContent, "utf8");
146
+ console.log("WebSocket code added to index.js successfully.");
147
+ }
148
+ function generateAuthSecret() {
149
+ // Generate 33 random bytes and encode them as a base64 string
150
+ return randomBytes(33).toString("base64");
151
+ }
152
+ function generateHexEncodedKey(size = 16) {
153
+ return randomBytes(size).toString("hex"); // Hex encoding ensures safe session keys
154
+ }
155
+ // Recursive copy function
156
+ function copyRecursiveSync(src, dest, answer) {
157
+ const exists = fs.existsSync(src);
158
+ const stats = exists && fs.statSync(src);
159
+ const isDirectory = exists && stats && stats.isDirectory();
160
+ if (isDirectory) {
161
+ const destLower = dest.toLowerCase();
162
+ if (!answer.websocket && destLower.includes("src\\lib\\websocket")) return;
163
+ if (
164
+ (answer.backendOnly && destLower.includes("src\\app\\js")) ||
165
+ (answer.backendOnly && destLower.includes("src\\app\\css")) ||
166
+ (answer.backendOnly && destLower.includes("src\\app\\assets"))
167
+ )
168
+ return;
169
+ if (!answer.swaggerDocs && destLower.includes("src\\app\\swagger-docs"))
170
+ return;
171
+ const destModified = dest.replace(/\\/g, "/");
172
+ if (updateAnswer?.excludeFilePath?.includes(destModified)) return;
173
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
174
+ fs.readdirSync(src).forEach((childItemName) => {
175
+ copyRecursiveSync(
176
+ path.join(src, childItemName),
177
+ path.join(dest, childItemName),
178
+ answer
179
+ );
180
+ });
181
+ } else {
182
+ if (checkExcludeFiles(dest)) return;
183
+ if (
184
+ !answer.tailwindcss &&
185
+ (dest.includes("tailwind.css") || dest.includes("styles.css"))
186
+ )
187
+ return;
188
+ if (
189
+ !answer.websocket &&
190
+ (dest.includes("restart-websocket.ts") ||
191
+ dest.includes("restart-websocket.bat"))
192
+ )
193
+ return;
194
+ if (!answer.docker && dockerFiles.some((file) => dest.includes(file)))
195
+ return;
196
+ if (
197
+ answer.backendOnly &&
198
+ nonBackendFiles.some((file) => dest.includes(file))
199
+ )
200
+ return;
201
+ if (!answer.backendOnly && dest.includes("route.php")) return;
202
+ if (
203
+ answer.backendOnly &&
204
+ !answer.swaggerDocs &&
205
+ dest.includes("layout.php")
206
+ )
207
+ return;
208
+ if (!answer.swaggerDocs && dest.includes("swagger-config.ts")) return;
209
+ if (answer.tailwindcss && dest.includes("index.css")) return;
210
+ if (
211
+ (!answer.swaggerDocs || !answer.prisma) &&
212
+ (dest.includes("auto-swagger-docs.ts") ||
213
+ dest.includes("prisma-schema-config.json"))
214
+ )
215
+ return;
216
+ fs.copyFileSync(src, dest, 0);
217
+ }
218
+ }
219
+ // Function to execute the recursive copy for entire directories
220
+ async function executeCopy(baseDir, directoriesToCopy, answer) {
221
+ directoriesToCopy.forEach(({ src: srcDir, dest: destDir }) => {
222
+ const sourcePath = path.join(__dirname, srcDir);
223
+ const destPath = path.join(baseDir, destDir);
224
+ copyRecursiveSync(sourcePath, destPath, answer);
225
+ });
226
+ }
227
+ function modifyPostcssConfig(baseDir) {
228
+ const filePath = path.join(baseDir, "postcss.config.js");
229
+ if (checkExcludeFiles(filePath)) return;
230
+ const newContent = `export default {
231
+ plugins: {
232
+ "@tailwindcss/postcss": {},
233
+ cssnano: {},
234
+ },
235
+ };`;
236
+ fs.writeFileSync(filePath, newContent, { flag: "w" });
237
+ console.log(chalk.green("postcss.config.js updated successfully."));
238
+ }
239
+ function modifyLayoutPHP(baseDir, answer) {
240
+ const layoutPath = path.join(baseDir, "src", "app", "layout.php");
241
+ if (checkExcludeFiles(layoutPath)) return;
242
+ try {
243
+ let indexContent = fs.readFileSync(layoutPath, "utf8");
244
+ let stylesAndLinks = "";
245
+ if (!answer.backendOnly) {
246
+ if (!answer.tailwindcss) {
247
+ stylesAndLinks = `\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />`;
248
+ }
249
+ stylesAndLinks += `\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"></script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"></script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"></script>`;
250
+ }
251
+ // Tailwind CSS link or CDN script
252
+ let tailwindLink = "";
253
+ if (!answer.backendOnly) {
254
+ tailwindLink = answer.tailwindcss
255
+ ? ` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${stylesAndLinks}`
256
+ : stylesAndLinks;
257
+ }
258
+ // Insert before the closing </head> tag
259
+ indexContent = indexContent.replace(
260
+ "</head>",
261
+ `${tailwindLink}
262
+ </head>`
263
+ );
264
+ fs.writeFileSync(layoutPath, indexContent, { flag: "w" });
265
+ console.log(
266
+ chalk.green(
267
+ `layout.php modified successfully for ${
268
+ answer.tailwindcss ? "local Tailwind CSS" : "Tailwind CSS CDN"
269
+ }.`
270
+ )
271
+ );
272
+ } catch (error) {
273
+ console.error(chalk.red("Error modifying layout.php:"), error);
274
+ }
275
+ }
276
+ // This function updates or creates the .env file
277
+ async function createOrUpdateEnvFile(baseDir, content) {
278
+ const envPath = path.join(baseDir, ".env");
279
+ if (checkExcludeFiles(envPath)) return;
280
+ console.log("🚀 ~ content:", content);
281
+ fs.writeFileSync(envPath, content, { flag: "w" });
282
+ }
283
+ function checkExcludeFiles(destPath) {
284
+ if (!updateAnswer?.isUpdate) return false;
285
+ return (
286
+ updateAnswer?.excludeFilePath?.includes(destPath.replace(/\\/g, "/")) ??
287
+ false
288
+ );
289
+ }
290
+ async function createDirectoryStructure(baseDir, answer) {
291
+ console.log("🚀 ~ baseDir:", baseDir);
292
+ console.log("🚀 ~ answer:", answer);
293
+ const filesToCopy = [
294
+ { src: "/bootstrap.php", dest: "/bootstrap.php" },
295
+ { src: "/.htaccess", dest: "/.htaccess" },
296
+ { src: "/tsconfig.json", dest: "/tsconfig.json" },
297
+ { src: "/app-gitignore", dest: "/.gitignore" },
298
+ ];
299
+ if (answer.tailwindcss) {
300
+ filesToCopy.push({ src: "/postcss.config.js", dest: "/postcss.config.js" });
301
+ }
302
+ const directoriesToCopy = [
303
+ {
304
+ src: "/settings",
305
+ dest: "/settings",
306
+ },
307
+ {
308
+ src: "/src",
309
+ dest: "/src",
310
+ },
311
+ ];
312
+ if (answer.docker) {
313
+ directoriesToCopy.push(
314
+ { src: "/.dockerignore", dest: "/.dockerignore" },
315
+ { src: "/docker-compose.yml", dest: "/docker-compose.yml" },
316
+ { src: "/Dockerfile", dest: "/Dockerfile" },
317
+ { src: "/apache.conf", dest: "/apache.conf" }
318
+ );
319
+ }
320
+ console.log("🚀 ~ directoriesToCopy:", directoriesToCopy);
321
+ filesToCopy.forEach(({ src, dest }) => {
322
+ const sourcePath = path.join(__dirname, src);
323
+ const destPath = path.join(baseDir, dest);
324
+ if (checkExcludeFiles(destPath)) return;
325
+ const code = fs.readFileSync(sourcePath, "utf8");
326
+ fs.writeFileSync(destPath, code, { flag: "w" });
327
+ });
328
+ await executeCopy(baseDir, directoriesToCopy, answer);
329
+ await updatePackageJson(baseDir, answer);
330
+ await updateComposerJson(baseDir);
331
+ if (!answer.backendOnly) {
332
+ await updateIndexJsForWebSocket(baseDir, answer);
333
+ }
334
+ if (answer.tailwindcss) {
335
+ modifyPostcssConfig(baseDir);
336
+ }
337
+ if (answer.tailwindcss || !answer.backendOnly || answer.swaggerDocs) {
338
+ modifyLayoutPHP(baseDir, answer);
339
+ }
340
+ const authSecret = generateAuthSecret();
341
+ const localStoreKey = generateHexEncodedKey();
342
+ const authCookieName = generateHexEncodedKey(8);
343
+ const functionCallSecret = generateHexEncodedKey(32);
344
+ const prismaPHPEnvContent = `# Authentication secret key for JWT or session encryption.
345
+ AUTH_SECRET="${authSecret}"
346
+ # Name of the authentication cookie.
347
+ AUTH_COOKIE_NAME="${authCookieName}"
348
+
349
+ # PHPMailer SMTP configuration (uncomment and set as needed)
350
+ # SMTP_HOST="smtp.gmail.com" # Your SMTP host
351
+ # SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username
352
+ # SMTP_PASSWORD="123456" # Your SMTP password
353
+ # SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port
354
+ # SMTP_ENCRYPTION="ssl" # ssl or tls
355
+ # MAIL_FROM="john.doe@gmail.com" # Sender email address
356
+ # MAIL_FROM_NAME="John Doe" # Sender name
357
+
358
+ # Show errors in the browser (development only). Set to false in production.
359
+ SHOW_ERRORS="true"
360
+
361
+ # Application timezone (default: UTC)
362
+ APP_TIMEZONE="UTC"
363
+
364
+ # Application environment (development or production)
365
+ APP_ENV="development"
366
+
367
+ # Enable or disable application cache (default: false)
368
+ CACHE_ENABLED="false"
369
+ # Cache time-to-live in seconds (default: 600)
370
+ CACHE_TTL="600"
371
+
372
+ # Local storage key for browser storage (auto-generated if not set).
373
+ # Spaces will be replaced with underscores and converted to lowercase.
374
+ LOCALSTORE_KEY="${localStoreKey}"
375
+
376
+ # Secret key for encrypting function calls.
377
+ FUNCTION_CALL_SECRET="${functionCallSecret}"`;
378
+ if (answer.prisma) {
379
+ const prismaEnvContent = `# Environment variables declared in this file are automatically made available to Prisma.
380
+ # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
381
+
382
+ # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
383
+ # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
384
+
385
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"`;
386
+ const envContent = `${prismaEnvContent}\n\n${prismaPHPEnvContent}`;
387
+ await createOrUpdateEnvFile(baseDir, envContent);
388
+ } else {
389
+ await createOrUpdateEnvFile(baseDir, prismaPHPEnvContent);
390
+ }
391
+ }
392
+ async function getAnswer(predefinedAnswers = {}) {
393
+ console.log("🚀 ~ predefinedAnswers:", predefinedAnswers);
394
+ const questionsArray = [];
395
+ // Ask for project name if not provided
396
+ if (!predefinedAnswers.projectName) {
397
+ questionsArray.push({
398
+ type: "text",
399
+ name: "projectName",
400
+ message: "What is your project named?",
401
+ initial: "my-app",
402
+ });
403
+ }
404
+ // IMPORTANT: Only ask backendOnly for NEW projects (not updates)
405
+ if (!predefinedAnswers.backendOnly && !updateAnswer?.isUpdate) {
406
+ questionsArray.push({
407
+ type: "toggle",
408
+ name: "backendOnly",
409
+ message: `Would you like to create a ${chalk.blue(
410
+ "backend-only project"
411
+ )}?`,
412
+ initial: false,
413
+ active: "Yes",
414
+ inactive: "No",
415
+ });
416
+ }
417
+ const onCancel = () => {
418
+ console.log(chalk.red("Operation cancelled by the user."));
419
+ process.exit(0);
420
+ };
421
+ const initialResponse = await prompts(questionsArray, { onCancel });
422
+ console.log("🚀 ~ initialResponse:", initialResponse);
423
+ const nonBackendOnlyQuestionsArray = [];
424
+ const isBackendOnly =
425
+ initialResponse.backendOnly ?? predefinedAnswers.backendOnly ?? false;
426
+ if (isBackendOnly) {
427
+ // For backend-only project (skip Tailwind), but still ask other features
428
+ // During updates, ALWAYS ask these questions regardless of predefinedAnswers
429
+ if (!predefinedAnswers.swaggerDocs && !updateAnswer?.isUpdate) {
430
+ nonBackendOnlyQuestionsArray.push({
431
+ type: "toggle",
432
+ name: "swaggerDocs",
433
+ message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
434
+ initial: false,
435
+ active: "Yes",
436
+ inactive: "No",
437
+ });
438
+ } else if (updateAnswer?.isUpdate) {
439
+ // Always ask during updates
440
+ nonBackendOnlyQuestionsArray.push({
441
+ type: "toggle",
442
+ name: "swaggerDocs",
443
+ message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
444
+ initial: predefinedAnswers.swaggerDocs ?? false,
445
+ active: "Yes",
446
+ inactive: "No",
447
+ });
448
+ }
449
+ if (!predefinedAnswers.websocket && !updateAnswer?.isUpdate) {
450
+ nonBackendOnlyQuestionsArray.push({
451
+ type: "toggle",
452
+ name: "websocket",
453
+ message: `Would you like to use ${chalk.blue("Websocket")}?`,
454
+ initial: true,
455
+ active: "Yes",
456
+ inactive: "No",
457
+ });
458
+ } else if (updateAnswer?.isUpdate) {
459
+ // Always ask during updates
460
+ nonBackendOnlyQuestionsArray.push({
461
+ type: "toggle",
462
+ name: "websocket",
463
+ message: `Would you like to use ${chalk.blue("Websocket")}?`,
464
+ initial: predefinedAnswers.websocket ?? true,
465
+ active: "Yes",
466
+ inactive: "No",
467
+ });
468
+ }
469
+ if (!predefinedAnswers.prisma && !updateAnswer?.isUpdate) {
470
+ nonBackendOnlyQuestionsArray.push({
471
+ type: "toggle",
472
+ name: "prisma",
473
+ message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
474
+ initial: true,
475
+ active: "Yes",
476
+ inactive: "No",
477
+ });
478
+ } else if (updateAnswer?.isUpdate) {
479
+ // Always ask during updates
480
+ nonBackendOnlyQuestionsArray.push({
481
+ type: "toggle",
482
+ name: "prisma",
483
+ message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
484
+ initial: predefinedAnswers.prisma ?? true,
485
+ active: "Yes",
486
+ inactive: "No",
487
+ });
488
+ }
489
+ if (!predefinedAnswers.docker && !updateAnswer?.isUpdate) {
490
+ nonBackendOnlyQuestionsArray.push({
491
+ type: "toggle",
492
+ name: "docker",
493
+ message: `Would you like to use ${chalk.blue("Docker")}?`,
494
+ initial: false,
495
+ active: "Yes",
496
+ inactive: "No",
497
+ });
498
+ } else if (updateAnswer?.isUpdate) {
499
+ // Always ask during updates
500
+ nonBackendOnlyQuestionsArray.push({
501
+ type: "toggle",
502
+ name: "docker",
503
+ message: `Would you like to use ${chalk.blue("Docker")}?`,
504
+ initial: predefinedAnswers.docker ?? false,
505
+ active: "Yes",
506
+ inactive: "No",
507
+ });
508
+ }
509
+ } else {
510
+ // For full-stack project, include Tailwind
511
+ if (!predefinedAnswers.swaggerDocs && !updateAnswer?.isUpdate) {
512
+ nonBackendOnlyQuestionsArray.push({
513
+ type: "toggle",
514
+ name: "swaggerDocs",
515
+ message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
516
+ initial: false,
517
+ active: "Yes",
518
+ inactive: "No",
519
+ });
520
+ } else if (updateAnswer?.isUpdate) {
521
+ // Always ask during updates
522
+ nonBackendOnlyQuestionsArray.push({
523
+ type: "toggle",
524
+ name: "swaggerDocs",
525
+ message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
526
+ initial: predefinedAnswers.swaggerDocs ?? false,
527
+ active: "Yes",
528
+ inactive: "No",
529
+ });
530
+ }
531
+ if (!predefinedAnswers.tailwindcss && !updateAnswer?.isUpdate) {
532
+ nonBackendOnlyQuestionsArray.push({
533
+ type: "toggle",
534
+ name: "tailwindcss",
535
+ message: `Would you like to use ${chalk.blue("Tailwind CSS")}?`,
536
+ initial: false,
537
+ active: "Yes",
538
+ inactive: "No",
539
+ });
540
+ } else if (updateAnswer?.isUpdate) {
541
+ // Always ask during updates
542
+ nonBackendOnlyQuestionsArray.push({
543
+ type: "toggle",
544
+ name: "tailwindcss",
545
+ message: `Would you like to use ${chalk.blue("Tailwind CSS")}?`,
546
+ initial: predefinedAnswers.tailwindcss ?? false,
547
+ active: "Yes",
548
+ inactive: "No",
549
+ });
550
+ }
551
+ if (!predefinedAnswers.websocket && !updateAnswer?.isUpdate) {
552
+ nonBackendOnlyQuestionsArray.push({
553
+ type: "toggle",
554
+ name: "websocket",
555
+ message: `Would you like to use ${chalk.blue("Websocket")}?`,
556
+ initial: false,
557
+ active: "Yes",
558
+ inactive: "No",
559
+ });
560
+ } else if (updateAnswer?.isUpdate) {
561
+ // Always ask during updates
562
+ nonBackendOnlyQuestionsArray.push({
563
+ type: "toggle",
564
+ name: "websocket",
565
+ message: `Would you like to use ${chalk.blue("Websocket")}?`,
566
+ initial: predefinedAnswers.websocket ?? false,
567
+ active: "Yes",
568
+ inactive: "No",
569
+ });
570
+ }
571
+ if (!predefinedAnswers.prisma && !updateAnswer?.isUpdate) {
572
+ nonBackendOnlyQuestionsArray.push({
573
+ type: "toggle",
574
+ name: "prisma",
575
+ message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
576
+ initial: false,
577
+ active: "Yes",
578
+ inactive: "No",
579
+ });
580
+ } else if (updateAnswer?.isUpdate) {
581
+ // Always ask during updates
582
+ nonBackendOnlyQuestionsArray.push({
583
+ type: "toggle",
584
+ name: "prisma",
585
+ message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
586
+ initial: predefinedAnswers.prisma ?? false,
587
+ active: "Yes",
588
+ inactive: "No",
589
+ });
590
+ }
591
+ if (!predefinedAnswers.docker && !updateAnswer?.isUpdate) {
592
+ nonBackendOnlyQuestionsArray.push({
593
+ type: "toggle",
594
+ name: "docker",
595
+ message: `Would you like to use ${chalk.blue("Docker")}?`,
596
+ initial: false,
597
+ active: "Yes",
598
+ inactive: "No",
599
+ });
600
+ } else if (updateAnswer?.isUpdate) {
601
+ // Always ask during updates
602
+ nonBackendOnlyQuestionsArray.push({
603
+ type: "toggle",
604
+ name: "docker",
605
+ message: `Would you like to use ${chalk.blue("Docker")}?`,
606
+ initial: predefinedAnswers.docker ?? false,
607
+ active: "Yes",
608
+ inactive: "No",
609
+ });
610
+ }
611
+ }
612
+ const nonBackendOnlyResponse = await prompts(nonBackendOnlyQuestionsArray, {
613
+ onCancel,
614
+ });
615
+ console.log("🚀 ~ nonBackendOnlyResponse:", nonBackendOnlyResponse);
616
+ return {
617
+ projectName: initialResponse.projectName
618
+ ? String(initialResponse.projectName).trim().replace(/ /g, "-")
619
+ : predefinedAnswers.projectName ?? "my-app",
620
+ backendOnly:
621
+ initialResponse.backendOnly ?? predefinedAnswers.backendOnly ?? false,
622
+ swaggerDocs:
623
+ nonBackendOnlyResponse.swaggerDocs ??
624
+ predefinedAnswers.swaggerDocs ??
625
+ false,
626
+ tailwindcss:
627
+ nonBackendOnlyResponse.tailwindcss ??
628
+ predefinedAnswers.tailwindcss ??
629
+ false,
630
+ websocket:
631
+ nonBackendOnlyResponse.websocket ?? predefinedAnswers.websocket ?? false,
632
+ prisma: nonBackendOnlyResponse.prisma ?? predefinedAnswers.prisma ?? false,
633
+ docker: nonBackendOnlyResponse.docker ?? predefinedAnswers.docker ?? false,
634
+ };
635
+ }
636
+ async function uninstallNpmDependencies(baseDir, dependencies, isDev = false) {
637
+ console.log("Uninstalling dependencies:");
638
+ dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
639
+ // Prepare the npm uninstall command with the appropriate flag for dev dependencies
640
+ const npmUninstallCommand = `npm uninstall ${
641
+ isDev ? "--save-dev" : "--save"
642
+ } ${dependencies.join(" ")}`;
643
+ // Execute the npm uninstall command
644
+ execSync(npmUninstallCommand, {
645
+ stdio: "inherit",
646
+ cwd: baseDir,
647
+ });
648
+ }
649
+ async function uninstallComposerDependencies(baseDir, dependencies) {
650
+ console.log("Uninstalling Composer dependencies:");
651
+ dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
652
+ // Prepare the composer remove command
653
+ const composerRemoveCommand = `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${dependencies.join(
654
+ " "
655
+ )}`;
656
+ // Execute the composer remove command
657
+ execSync(composerRemoveCommand, {
658
+ stdio: "inherit",
659
+ cwd: baseDir,
660
+ });
661
+ }
662
+ function fetchPackageVersion(packageName) {
663
+ return new Promise((resolve, reject) => {
664
+ https
665
+ .get(`https://registry.npmjs.org/${packageName}`, (res) => {
666
+ let data = "";
667
+ res.on("data", (chunk) => (data += chunk));
668
+ res.on("end", () => {
669
+ try {
670
+ const parsed = JSON.parse(data);
671
+ resolve(parsed["dist-tags"].latest);
672
+ } catch (error) {
673
+ reject(new Error("Failed to parse JSON response"));
674
+ }
675
+ });
676
+ })
677
+ .on("error", (err) => reject(err));
678
+ });
679
+ }
680
+ const readJsonFile = (filePath) => {
681
+ const jsonData = fs.readFileSync(filePath, "utf8");
682
+ return JSON.parse(jsonData);
683
+ };
684
+ function compareVersions(installedVersion, currentVersion) {
685
+ const installedVersionArray = installedVersion.split(".").map(Number);
686
+ const currentVersionArray = currentVersion.split(".").map(Number);
687
+ for (let i = 0; i < installedVersionArray.length; i++) {
688
+ if (installedVersionArray[i] > currentVersionArray[i]) {
689
+ return 1;
690
+ } else if (installedVersionArray[i] < currentVersionArray[i]) {
691
+ return -1;
692
+ }
693
+ }
694
+ return 0;
695
+ }
696
+ function getInstalledPackageVersion(packageName) {
697
+ try {
698
+ const output = execSync(`npm list -g ${packageName} --depth=0`).toString();
699
+ const versionMatch = output.match(
700
+ new RegExp(`${packageName}@(\\d+\\.\\d+\\.\\d+)`)
701
+ );
702
+ if (versionMatch) {
703
+ return versionMatch[1];
704
+ } else {
705
+ console.error(`Package ${packageName} is not installed`);
706
+ return null;
707
+ }
708
+ } catch (error) {
709
+ console.error(error instanceof Error ? error.message : String(error));
710
+ return null;
711
+ }
712
+ }
3
713
  /**
4
714
  * Install dependencies in the specified directory.
5
715
  * @param {string} baseDir - The base directory where to install the dependencies.
@@ -7,7 +717,11 @@ import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLT
7
717
  * @param {boolean} [isDev=false] - Whether to install the dependencies as devDependencies.
8
718
  */
9
719
  async function installNpmDependencies(baseDir, dependencies, isDev = false) {
10
- console.log("Initializing new Node.js project...");
720
+ if (!fs.existsSync(path.join(baseDir, "package.json"))) {
721
+ console.log("Initializing new Node.js project...");
722
+ } else {
723
+ console.log("Updating existing Node.js project...");
724
+ }
11
725
  // Initialize a package.json if it doesn't exist
12
726
  if (!fs.existsSync(path.join(baseDir, "package.json"))) {
13
727
  execSync("npm init -y", {
@@ -185,6 +899,7 @@ async function main() {
185
899
  if (fs.existsSync(filePath))
186
900
  excludeFiles.push(filePath.replace(/\\/g, "/"));
187
901
  });
902
+ // Set updateAnswer with OLD settings initially (for checkExcludeFiles function)
188
903
  updateAnswer = {
189
904
  projectName,
190
905
  backendOnly: localSettings.backendOnly,
@@ -212,6 +927,22 @@ async function main() {
212
927
  docker: args.includes("--docker") || localSettings.docker,
213
928
  };
214
929
  answer = await getAnswer(predefinedAnswers);
930
+ // IMPORTANT: Update updateAnswer with the NEW answer after getting user input
931
+ if (answer !== null) {
932
+ updateAnswer = {
933
+ projectName,
934
+ backendOnly: answer.backendOnly,
935
+ swaggerDocs: answer.swaggerDocs,
936
+ tailwindcss: answer.tailwindcss,
937
+ websocket: answer.websocket,
938
+ prisma: answer.prisma,
939
+ docker: answer.docker,
940
+ isUpdate: true,
941
+ excludeFiles: localSettings.excludeFiles ?? [],
942
+ excludeFilePath: excludeFiles ?? [],
943
+ filePath: currentDir,
944
+ };
945
+ }
215
946
  } else {
216
947
  // It's a new project - use CLI arguments
217
948
  let useBackendOnly = args.includes("--backend-only");
@@ -357,24 +1088,57 @@ async function main() {
357
1088
  if (updateAnswer?.isUpdate) {
358
1089
  const updateUninstallNpmDependencies = [];
359
1090
  const updateUninstallComposerDependencies = [];
1091
+ // Helper function to check if a composer package is installed
1092
+ const isComposerPackageInstalled = (packageName) => {
1093
+ try {
1094
+ const composerJsonPath = path.join(projectPath, "composer.json");
1095
+ if (fs.existsSync(composerJsonPath)) {
1096
+ const composerJson = JSON.parse(
1097
+ fs.readFileSync(composerJsonPath, "utf8")
1098
+ );
1099
+ return !!(
1100
+ composerJson.require && composerJson.require[packageName]
1101
+ );
1102
+ }
1103
+ return false;
1104
+ } catch {
1105
+ return false;
1106
+ }
1107
+ };
1108
+ // Helper function to check if an npm package is installed
1109
+ const isNpmPackageInstalled = (packageName) => {
1110
+ try {
1111
+ const packageJsonPath = path.join(projectPath, "package.json");
1112
+ if (fs.existsSync(packageJsonPath)) {
1113
+ const packageJson = JSON.parse(
1114
+ fs.readFileSync(packageJsonPath, "utf8")
1115
+ );
1116
+ return !!(
1117
+ (packageJson.dependencies &&
1118
+ packageJson.dependencies[packageName]) ||
1119
+ (packageJson.devDependencies &&
1120
+ packageJson.devDependencies[packageName])
1121
+ );
1122
+ }
1123
+ return false;
1124
+ } catch {
1125
+ return false;
1126
+ }
1127
+ };
360
1128
  if (updateAnswer.backendOnly) {
361
1129
  nonBackendFiles.forEach((file) => {
362
1130
  const filePath = path.join(projectPath, "src", "app", file);
363
1131
  if (fs.existsSync(filePath)) {
364
- fs.unlinkSync(filePath); // Delete each file if it exists
1132
+ fs.unlinkSync(filePath);
365
1133
  console.log(`${file} was deleted successfully.`);
366
- } else {
367
- console.log(`${file} does not exist.`);
368
1134
  }
369
1135
  });
370
1136
  const backendOnlyFolders = ["js", "css"];
371
1137
  backendOnlyFolders.forEach((folder) => {
372
1138
  const folderPath = path.join(projectPath, "src", "app", folder);
373
1139
  if (fs.existsSync(folderPath)) {
374
- fs.rmSync(folderPath, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
1140
+ fs.rmSync(folderPath, { recursive: true, force: true });
375
1141
  console.log(`${folder} was deleted successfully.`);
376
- } else {
377
- console.log(`${folder} does not exist.`);
378
1142
  }
379
1143
  });
380
1144
  }
@@ -386,44 +1150,53 @@ async function main() {
386
1150
  "swagger-docs"
387
1151
  );
388
1152
  if (fs.existsSync(swaggerDocsFolder)) {
389
- fs.rmSync(swaggerDocsFolder, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
1153
+ fs.rmSync(swaggerDocsFolder, { recursive: true, force: true });
390
1154
  console.log(`swagger-docs was deleted successfully.`);
391
1155
  }
392
1156
  const swaggerFiles = ["swagger-config.ts"];
393
1157
  swaggerFiles.forEach((file) => {
394
1158
  const filePath = path.join(projectPath, "settings", file);
395
1159
  if (fs.existsSync(filePath)) {
396
- fs.unlinkSync(filePath); // Delete each file if it exists
1160
+ fs.unlinkSync(filePath);
397
1161
  console.log(`${file} was deleted successfully.`);
398
- } else {
399
- console.log(`${file} does not exist.`);
400
1162
  }
401
1163
  });
402
- updateUninstallNpmDependencies.push(
403
- "swagger-jsdoc",
404
- "@types/swagger-jsdoc",
405
- "prompts",
406
- "@types/prompts"
407
- );
1164
+ // Only add to uninstall list if packages are actually installed
1165
+ if (isNpmPackageInstalled("swagger-jsdoc")) {
1166
+ updateUninstallNpmDependencies.push("swagger-jsdoc");
1167
+ }
1168
+ if (isNpmPackageInstalled("@types/swagger-jsdoc")) {
1169
+ updateUninstallNpmDependencies.push("@types/swagger-jsdoc");
1170
+ }
1171
+ if (isNpmPackageInstalled("prompts")) {
1172
+ updateUninstallNpmDependencies.push("prompts");
1173
+ }
1174
+ if (isNpmPackageInstalled("@types/prompts")) {
1175
+ updateUninstallNpmDependencies.push("@types/prompts");
1176
+ }
408
1177
  }
409
1178
  if (!updateAnswer.tailwindcss) {
410
1179
  const tailwindFiles = ["postcss.config.js"];
411
1180
  tailwindFiles.forEach((file) => {
412
1181
  const filePath = path.join(projectPath, file);
413
1182
  if (fs.existsSync(filePath)) {
414
- fs.unlinkSync(filePath); // Delete each file if it exists
1183
+ fs.unlinkSync(filePath);
415
1184
  console.log(`${file} was deleted successfully.`);
416
- } else {
417
- console.log(`${file} does not exist.`);
418
1185
  }
419
1186
  });
420
- updateUninstallNpmDependencies.push(
1187
+ // Only add to uninstall list if packages are actually installed
1188
+ const tailwindPackages = [
421
1189
  "tailwindcss",
422
1190
  "postcss",
423
1191
  "postcss-cli",
424
1192
  "@tailwindcss/postcss",
425
- "cssnano"
426
- );
1193
+ "cssnano",
1194
+ ];
1195
+ tailwindPackages.forEach((pkg) => {
1196
+ if (isNpmPackageInstalled(pkg)) {
1197
+ updateUninstallNpmDependencies.push(pkg);
1198
+ }
1199
+ });
427
1200
  }
428
1201
  if (!updateAnswer.websocket) {
429
1202
  const websocketFiles = [
@@ -433,10 +1206,8 @@ async function main() {
433
1206
  websocketFiles.forEach((file) => {
434
1207
  const filePath = path.join(projectPath, "settings", file);
435
1208
  if (fs.existsSync(filePath)) {
436
- fs.unlinkSync(filePath); // Delete each file if it exists
1209
+ fs.unlinkSync(filePath);
437
1210
  console.log(`${file} was deleted successfully.`);
438
- } else {
439
- console.log(`${file} does not exist.`);
440
1211
  }
441
1212
  });
442
1213
  const websocketFolder = path.join(
@@ -446,18 +1217,28 @@ async function main() {
446
1217
  "Websocket"
447
1218
  );
448
1219
  if (fs.existsSync(websocketFolder)) {
449
- fs.rmSync(websocketFolder, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
1220
+ fs.rmSync(websocketFolder, { recursive: true, force: true });
450
1221
  console.log(`Websocket folder was deleted successfully.`);
451
1222
  }
452
- updateUninstallNpmDependencies.push("chokidar-cli");
453
- updateUninstallComposerDependencies.push("cboden/ratchet");
1223
+ // Only add to uninstall list if packages are actually installed
1224
+ if (isNpmPackageInstalled("chokidar-cli")) {
1225
+ updateUninstallNpmDependencies.push("chokidar-cli");
1226
+ }
1227
+ if (isComposerPackageInstalled("cboden/ratchet")) {
1228
+ updateUninstallComposerDependencies.push("cboden/ratchet");
1229
+ }
454
1230
  }
455
1231
  if (!updateAnswer.prisma) {
456
- updateUninstallNpmDependencies.push(
1232
+ const prismaPackages = [
457
1233
  "prisma",
458
1234
  "@prisma/client",
459
- "@prisma/internals"
460
- );
1235
+ "@prisma/internals",
1236
+ ];
1237
+ prismaPackages.forEach((pkg) => {
1238
+ if (isNpmPackageInstalled(pkg)) {
1239
+ updateUninstallNpmDependencies.push(pkg);
1240
+ }
1241
+ });
461
1242
  }
462
1243
  if (!updateAnswer.docker) {
463
1244
  const dockerFiles = [
@@ -469,14 +1250,18 @@ async function main() {
469
1250
  dockerFiles.forEach((file) => {
470
1251
  const filePath = path.join(projectPath, file);
471
1252
  if (fs.existsSync(filePath)) {
472
- fs.unlinkSync(filePath); // Delete each file if it exists
1253
+ fs.unlinkSync(filePath);
473
1254
  console.log(`${file} was deleted successfully.`);
474
- } else {
475
- console.log(`${file} does not exist.`);
476
1255
  }
477
1256
  });
478
1257
  }
1258
+ // Only uninstall if there are packages to uninstall
479
1259
  if (updateUninstallNpmDependencies.length > 0) {
1260
+ console.log(
1261
+ `Uninstalling npm packages: ${updateUninstallNpmDependencies.join(
1262
+ ", "
1263
+ )}`
1264
+ );
480
1265
  await uninstallNpmDependencies(
481
1266
  projectPath,
482
1267
  updateUninstallNpmDependencies,
@@ -484,6 +1269,11 @@ async function main() {
484
1269
  );
485
1270
  }
486
1271
  if (updateUninstallComposerDependencies.length > 0) {
1272
+ console.log(
1273
+ `Uninstalling composer packages: ${updateUninstallComposerDependencies.join(
1274
+ ", "
1275
+ )}`
1276
+ );
487
1277
  await uninstallComposerDependencies(
488
1278
  projectPath,
489
1279
  updateUninstallComposerDependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-prisma-php-app",
3
- "version": "3.3.2",
3
+ "version": "3.3.4",
4
4
  "description": "Prisma-PHP: A Revolutionary Library Bridging PHP with Prisma ORM",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",