evophp 1.3.0 → 1.3.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.
- package/README.md +87 -2
- package/bin/evophp.js +1 -1
- package/commands/dependencies.js +188 -10
- package/commands/init.js +1 -0
- package/commands/install.js +100 -19
- package/commands/uninstall.js +157 -17
- package/package.json +1 -1
- package/utils/dependencies.js +40 -12
- package/utils/helpers.js +9 -7
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ EvoPHP CLI is a command-line interface designed to simplify the development and
|
|
|
16
16
|
* [Plugin/Theme Uninstallation](#plugintheme-uninstallation)
|
|
17
17
|
* [Dependency Management](#dependency-management)
|
|
18
18
|
* [Entry Management](#entry-management)
|
|
19
|
+
* [Plugin & Theme Dependencies](#plugin--theme-dependencies)
|
|
19
20
|
* [Authentication & PAT Storage](#authentication--pat-storage)
|
|
20
21
|
* [Further Documentation](#further-documentation)
|
|
21
22
|
|
|
@@ -104,6 +105,8 @@ evophp i theme <name>
|
|
|
104
105
|
evophp i theme johndoe/EvoTheme
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
* **Automatic dependency installation**: If the plugin or theme includes a `dependencies.json` file, EvoPHP will automatically install all listed JS/PHP packages and any dependent EvoPHP plugins or themes. See [Plugin & Theme Dependencies](#plugin--theme-dependencies).
|
|
109
|
+
|
|
107
110
|
---
|
|
108
111
|
|
|
109
112
|
### Plugin/Theme Uninstallation
|
|
@@ -116,6 +119,7 @@ evophp u theme <name>
|
|
|
116
119
|
```
|
|
117
120
|
|
|
118
121
|
* Removes the installed plugin or theme from the project.
|
|
122
|
+
* Also removes any EvoPHP plugin/theme dependencies that were installed alongside it — **unless** they are still required by another installed plugin or theme, in which case they are retained.
|
|
119
123
|
|
|
120
124
|
* Examples:
|
|
121
125
|
|
|
@@ -133,7 +137,7 @@ evophp install-dependencies <plugin-name>
|
|
|
133
137
|
evophp d <plugin-name>
|
|
134
138
|
```
|
|
135
139
|
|
|
136
|
-
* Adds or installs plugin dependencies.
|
|
140
|
+
* Adds or installs plugin dependencies — npm packages, Composer packages, or other EvoPHP plugins/themes.
|
|
137
141
|
|
|
138
142
|
#### Production (npm):
|
|
139
143
|
|
|
@@ -153,6 +157,25 @@ evophp d MyPlugin --dev
|
|
|
153
157
|
evophp d MyPlugin --composer
|
|
154
158
|
```
|
|
155
159
|
|
|
160
|
+
#### EvoPHP Plugin dependency:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
evophp d MyPlugin --evoplugin
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
* Prompts for one or more `author/name` (or plain `name`) EvoPHP plugins to add as dependencies.
|
|
167
|
+
* Clones and installs each listed plugin into the project.
|
|
168
|
+
* Records them under the `"plugin"` key in `MyPlugin`'s `dependencies.json`.
|
|
169
|
+
* A GitHub token is only required when a dependency is provided without an `author/` prefix.
|
|
170
|
+
|
|
171
|
+
#### EvoPHP Theme dependency:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
evophp d MyPlugin --evotheme
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
* Same as `--evoplugin` but for themes — clones, installs, and records under the `"theme"` key in `dependencies.json`.
|
|
178
|
+
|
|
156
179
|
---
|
|
157
180
|
|
|
158
181
|
### Entry Management
|
|
@@ -179,6 +202,68 @@ evophp e <plugin> <entry>
|
|
|
179
202
|
evophp entry MyPlugin Dashboard
|
|
180
203
|
```
|
|
181
204
|
|
|
205
|
+
### Patch PHP & JS Dependencies
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
evophp patch --upgrade
|
|
209
|
+
evophp p
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This is used to update core libraries of EvoPhp.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Plugin & Theme Dependencies
|
|
217
|
+
|
|
218
|
+
EvoPHP supports a `dependencies.json` file at the root of any plugin or theme. This file declares all dependencies that should be automatically resolved during installation.
|
|
219
|
+
|
|
220
|
+
### Structure
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"prod": "qrcode.vue vue3-qrcode-reader jspdf @fullcalendar/core @fullcalendar/vue3",
|
|
225
|
+
"dev": "",
|
|
226
|
+
"plugin": "kinsokafor/Invoice kinsokafor/GooglePack",
|
|
227
|
+
"theme": "kinsokafor/BSTTheme"
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Key | Type | Description |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `prod` | string | Space-separated npm packages to install as production dependencies |
|
|
234
|
+
| `dev` | string | Space-separated npm packages to install as dev dependencies |
|
|
235
|
+
| `plugin` | string | Space-separated `author/name` EvoPHP plugins this plugin/theme depends on |
|
|
236
|
+
| `theme` | string | Space-separated `author/name` EvoPHP themes this plugin/theme depends on |
|
|
237
|
+
|
|
238
|
+
### Automatic Installation
|
|
239
|
+
|
|
240
|
+
When you run `evophp install`, EvoPHP will:
|
|
241
|
+
|
|
242
|
+
1. Clone the plugin or theme from GitHub.
|
|
243
|
+
2. Read its `dependencies.json` (if present).
|
|
244
|
+
3. Install all listed npm (`prod`/`dev`) and Composer (`prod`) packages.
|
|
245
|
+
4. Recursively install any EvoPHP plugins or themes listed under `"plugin"` and `"theme"` — each of which may declare their own `dependencies.json`, resolved the same way.
|
|
246
|
+
|
|
247
|
+
### Automatic Uninstallation
|
|
248
|
+
|
|
249
|
+
When you run `evophp uninstall`, EvoPHP will:
|
|
250
|
+
|
|
251
|
+
1. Remove the target plugin or theme.
|
|
252
|
+
2. Check each of its EvoPHP plugin/theme dependencies.
|
|
253
|
+
3. Remove a dependency only if **no other installed plugin or theme** still requires it. If another plugin depends on it, it is retained and a notice is shown.
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
PluginA depends on → Invoice, GooglePack
|
|
259
|
+
PluginB depends on → Invoice
|
|
260
|
+
|
|
261
|
+
evophp uninstall plugin PluginA
|
|
262
|
+
✔ PluginA removed
|
|
263
|
+
⚠ Invoice retained (still required by PluginB)
|
|
264
|
+
✔ GooglePack removed
|
|
265
|
+
```
|
|
266
|
+
|
|
182
267
|
---
|
|
183
268
|
|
|
184
269
|
## Authentication & PAT Storage
|
|
@@ -198,4 +283,4 @@ When using a command with just `<name>` (e.g. `evophp new plugin MyPlugin`), Evo
|
|
|
198
283
|
## Further Documentation
|
|
199
284
|
|
|
200
285
|
More advanced use cases, templates, and configuration details coming soon at:
|
|
201
|
-
📘 [Official EvoPHP Documentation](#)
|
|
286
|
+
📘 [Official EvoPHP Documentation](#)
|
package/bin/evophp.js
CHANGED
package/commands/dependencies.js
CHANGED
|
@@ -2,38 +2,217 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
4
|
const { readLineAsync } = require("../utils/helpers");
|
|
5
|
+
const { getUserData } = require("../utils/github");
|
|
6
|
+
const { cloneRepo } = require("../utils/git");
|
|
7
|
+
const { installDependencies, extractDependencies } = require("../utils/dependencies");
|
|
5
8
|
const log = require("../utils/logger");
|
|
9
|
+
require("dotenv").config({ path: "./.env.development" });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Installs a single evophp plugin or theme and records it in the
|
|
13
|
+
* parent plugin's dependencies.json under the appropriate key.
|
|
14
|
+
*
|
|
15
|
+
* @param {'plugin'|'theme'} depType
|
|
16
|
+
* @param {string} dep - "author/name" or plain "name"
|
|
17
|
+
* @param {string} gitHubToken
|
|
18
|
+
* @param {object} dependencies - the in-memory dependencies object to mutate
|
|
19
|
+
*/
|
|
20
|
+
async function installEvoDep(depType, dep, gitHubToken, dependencies) {
|
|
21
|
+
let author, depName;
|
|
22
|
+
|
|
23
|
+
if (dep.includes("/")) {
|
|
24
|
+
[author, depName] = dep.split("/");
|
|
25
|
+
} else {
|
|
26
|
+
if (!gitHubToken) {
|
|
27
|
+
log.error(`A GitHub token is required to install "${dep}" without an author prefix.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const { getUserData } = require("../utils/github");
|
|
31
|
+
const userData = await getUserData(gitHubToken);
|
|
32
|
+
author = userData.login;
|
|
33
|
+
depName = dep;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const paths = depType === "plugin"
|
|
37
|
+
? [path.join("Public", "Modules", depName), path.join("EvoJsDev", "Modules", depName)]
|
|
38
|
+
: [path.join("Public", "Themes", depName)];
|
|
39
|
+
|
|
40
|
+
if (paths.every(p => fs.existsSync(p))) {
|
|
41
|
+
log.warn(`${depType} "${depName}" is already installed, skipping clone.`);
|
|
42
|
+
} else {
|
|
43
|
+
log.info(`Installing ${depType} "${depName}" from ${author}...`);
|
|
44
|
+
|
|
45
|
+
if (depType === "plugin") {
|
|
46
|
+
await cloneRepo(`${author}/${depName}`, null, paths[0]);
|
|
47
|
+
await cloneRepo(`${author}/${depName}.js`, null, paths[1]);
|
|
48
|
+
await installDependencies(extractDependencies(paths[1], "js"), "js");
|
|
49
|
+
await installDependencies(extractDependencies(paths[0], "php"), "php");
|
|
50
|
+
await execSync(`evophp d ${author}/${depName} --sync`, { stdio: "inherit" });
|
|
51
|
+
} else {
|
|
52
|
+
await cloneRepo(`${author}/${depName}`, null, paths[0]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
log.success(`${depType} "${depName}" installed.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Record as "author/name" in dependencies.json, deduplicated
|
|
59
|
+
const key = depType; // "plugin" or "theme"
|
|
60
|
+
const existing = (dependencies[key] || "")
|
|
61
|
+
.split(" ")
|
|
62
|
+
.map(d => d.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
|
|
65
|
+
const fullRef = `${author}/${depName}`;
|
|
66
|
+
if (!existing.some(d => d === fullRef || d.split("/").pop() === depName)) {
|
|
67
|
+
existing.push(fullRef);
|
|
68
|
+
}
|
|
69
|
+
dependencies[key] = existing.join(" ");
|
|
70
|
+
}
|
|
6
71
|
|
|
7
72
|
module.exports = {
|
|
8
|
-
command: "install-dependencies <plugin> [--dev|--composer]",
|
|
73
|
+
command: "install-dependencies <plugin> [--dev|--composer|--evoplugin|--evotheme|--sync]",
|
|
9
74
|
aliases: ["d"],
|
|
10
75
|
describe: "Add and install plugin or theme dependencies",
|
|
11
76
|
|
|
12
77
|
builder: yargs =>
|
|
13
78
|
yargs
|
|
14
79
|
.positional("plugin", { describe: "Plugin or theme name", type: "string" })
|
|
15
|
-
.option("dev",
|
|
16
|
-
.option("composer",
|
|
80
|
+
.option("dev", { describe: "Install as dev dependency (JS)", type: "boolean" })
|
|
81
|
+
.option("composer", { describe: "Use composer for PHP", type: "boolean" })
|
|
82
|
+
.option("evoplugin", { describe: "Install an evophp plugin dependency", type: "boolean" })
|
|
83
|
+
.option("evotheme", { describe: "Install an evophp theme dependency", type: "boolean" })
|
|
84
|
+
.option("sync", { describe: "Install any missing evophp plugins/themes in dependencies.json", type: "boolean" }),
|
|
17
85
|
|
|
18
|
-
handler: async ({ plugin, dev, composer }) => {
|
|
86
|
+
handler: async ({ plugin, dev, composer, evoplugin, evotheme, sync }) => {
|
|
19
87
|
try {
|
|
88
|
+
const isEvo = !!(evoplugin || evotheme);
|
|
89
|
+
const isPHP = !!composer;
|
|
20
90
|
const depType = dev ? "dev" : "prod";
|
|
21
91
|
|
|
22
|
-
|
|
92
|
+
// ── sync: install any missing evophp plugins/themes from dependencies.json ──
|
|
93
|
+
if (sync) {
|
|
94
|
+
const evoDepsDir = path.join("EvoJsDev", "Modules", plugin);
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(evoDepsDir)) {
|
|
97
|
+
log.error(`Plugin not found at ${evoDepsDir}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dependenciesFile = path.join(evoDepsDir, "dependencies.json");
|
|
102
|
+
if (!fs.existsSync(dependenciesFile)) {
|
|
103
|
+
log.warn(`No dependencies.json found for "${plugin}".`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dependencies = JSON.parse(fs.readFileSync(dependenciesFile, "utf-8"));
|
|
108
|
+
|
|
109
|
+
const pluginDeps = (dependencies.plugin || "").split(" ").map(d => d.trim()).filter(Boolean);
|
|
110
|
+
const themeDeps = (dependencies.theme || "").split(" ").map(d => d.trim()).filter(Boolean);
|
|
111
|
+
|
|
112
|
+
if (pluginDeps.length === 0 && themeDeps.length === 0) {
|
|
113
|
+
log.info("No evophp plugin or theme dependencies listed.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Collect only the missing ones
|
|
118
|
+
const missingPlugins = pluginDeps.filter(dep => {
|
|
119
|
+
const depName = dep.includes("/") ? dep.split("/").pop() : dep;
|
|
120
|
+
return !fs.existsSync(path.join("Public", "Modules", depName)) ||
|
|
121
|
+
!fs.existsSync(path.join("EvoJsDev", "Modules", depName));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const missingThemes = themeDeps.filter(dep => {
|
|
125
|
+
const depName = dep.includes("/") ? dep.split("/").pop() : dep;
|
|
126
|
+
return !fs.existsSync(path.join("Public", "Themes", depName));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (missingPlugins.length === 0 && missingThemes.length === 0) {
|
|
130
|
+
log.success("All evophp dependencies are already installed.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
log.info(`Missing plugins : ${missingPlugins.join(", ") || "none"}`);
|
|
135
|
+
log.info(`Missing themes : ${missingThemes.join(", ") || "none"}`);
|
|
136
|
+
|
|
137
|
+
// Resolve a token only if needed
|
|
138
|
+
let gitHubToken = process.env.GITHUB_TOKEN;
|
|
139
|
+
const needsToken = [...missingPlugins, ...missingThemes].some(d => !d.includes("/"));
|
|
140
|
+
if (needsToken && !gitHubToken) {
|
|
141
|
+
gitHubToken = await readLineAsync("Enter your GitHub Personal Access Token: ");
|
|
142
|
+
const { saveToEnv } = require("../utils/helpers");
|
|
143
|
+
saveToEnv("GITHUB_TOKEN", gitHubToken);
|
|
144
|
+
log.success("GitHub token saved to .env.development");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const dep of missingPlugins) {
|
|
148
|
+
await installEvoDep("plugin", dep, gitHubToken, dependencies);
|
|
149
|
+
}
|
|
150
|
+
for (const dep of missingThemes) {
|
|
151
|
+
await installEvoDep("theme", dep, gitHubToken, dependencies);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log.success(`Sync complete for "${plugin}".`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For JS/PHP deps the source of truth lives in EvoJsDev; for PHP-only in Public
|
|
23
159
|
const baseDir = isPHP
|
|
24
160
|
? path.join("Public", "Modules", plugin)
|
|
25
161
|
: path.join("EvoJsDev", "Modules", plugin);
|
|
26
162
|
|
|
27
|
-
|
|
28
|
-
|
|
163
|
+
// evophp deps are recorded in the JS side of the plugin
|
|
164
|
+
const evoDepsDir = path.join("EvoJsDev", "Modules", plugin);
|
|
165
|
+
|
|
166
|
+
const targetDir = isEvo ? evoDepsDir : baseDir;
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(targetDir)) {
|
|
169
|
+
log.error(`Plugin not found at ${targetDir}`);
|
|
29
170
|
process.exit(1);
|
|
30
171
|
}
|
|
31
172
|
|
|
32
|
-
const dependenciesFile = path.join(
|
|
173
|
+
const dependenciesFile = path.join(targetDir, "dependencies.json");
|
|
33
174
|
let dependencies = fs.existsSync(dependenciesFile)
|
|
34
175
|
? JSON.parse(fs.readFileSync(dependenciesFile, "utf-8"))
|
|
35
176
|
: {};
|
|
36
177
|
|
|
178
|
+
// ── evophp plugin / theme dependencies ────────────────────────────────
|
|
179
|
+
if (isEvo) {
|
|
180
|
+
const evoDepType = evoplugin ? "plugin" : "theme";
|
|
181
|
+
|
|
182
|
+
log.info(`Existing evophp ${evoDepType} dependencies:`);
|
|
183
|
+
log.info(dependencies[evoDepType] || "None");
|
|
184
|
+
|
|
185
|
+
const input = await readLineAsync(
|
|
186
|
+
`Enter evophp ${evoDepType}(s) to add as dependencies (space-separated, author/name or name): `
|
|
187
|
+
);
|
|
188
|
+
const newDeps = input.split(" ").map(d => d.trim()).filter(Boolean);
|
|
189
|
+
|
|
190
|
+
if (newDeps.length === 0) {
|
|
191
|
+
log.warn("No dependencies provided.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let gitHubToken = process.env.GITHUB_TOKEN;
|
|
196
|
+
// Only fetch a token if any dep is missing the author prefix
|
|
197
|
+
if (newDeps.some(d => !d.includes("/"))) {
|
|
198
|
+
if (!gitHubToken) {
|
|
199
|
+
gitHubToken = await readLineAsync("Enter your GitHub Personal Access Token: ");
|
|
200
|
+
const { saveToEnv } = require("../utils/helpers");
|
|
201
|
+
saveToEnv("GITHUB_TOKEN", gitHubToken);
|
|
202
|
+
log.success("GitHub token saved to .env.development");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const dep of newDeps) {
|
|
207
|
+
await installEvoDep(evoDepType, dep, gitHubToken, dependencies);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fs.writeFileSync(dependenciesFile, JSON.stringify(dependencies, null, 2));
|
|
211
|
+
log.success("Updated dependencies.json with new evophp dependencies.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── JS / PHP (npm / composer) dependencies ────────────────────────────
|
|
37
216
|
log.info(`Existing ${isPHP ? "PHP" : "JS"} dependencies:`);
|
|
38
217
|
log.info("Production:", dependencies.prod || "None");
|
|
39
218
|
if (!isPHP) log.info("Development:", dependencies.dev || "None");
|
|
@@ -64,7 +243,6 @@ module.exports = {
|
|
|
64
243
|
fs.writeFileSync(dependenciesFile, JSON.stringify(dependencies, null, 2));
|
|
65
244
|
log.success("Updated dependencies.json");
|
|
66
245
|
|
|
67
|
-
// Install
|
|
68
246
|
const installCmd = isPHP
|
|
69
247
|
? `composer require ${newDeps.join(" ")}`
|
|
70
248
|
: `npm install ${newDeps.join(" ")} ${dev ? "--save-dev" : "--save"}`;
|
|
@@ -77,4 +255,4 @@ module.exports = {
|
|
|
77
255
|
process.exit(1);
|
|
78
256
|
}
|
|
79
257
|
}
|
|
80
|
-
};
|
|
258
|
+
};
|
package/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require("path");
|
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
4
|
const { readLineAsync } = require("../utils/helpers");
|
|
5
5
|
const log = require("../utils/logger");
|
|
6
|
+
// const { editFile } = require("../utils/edit");
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
command: "init [name]",
|
package/commands/install.js
CHANGED
|
@@ -4,10 +4,102 @@ const { execSync } = require('child_process');
|
|
|
4
4
|
const { readLineAsync, mergeConfig, saveToEnv } = require('../utils/helpers');
|
|
5
5
|
const { getUserData } = require('../utils/github');
|
|
6
6
|
const { cloneRepo } = require('../utils/git');
|
|
7
|
-
const { installDependencies } = require('../utils/dependencies');
|
|
7
|
+
const { installDependencies, extractDependencies, mergeDependencies } = require('../utils/dependencies');
|
|
8
8
|
const log = require('../utils/logger');
|
|
9
9
|
require("dotenv").config({ path: "./.env.development" });
|
|
10
10
|
|
|
11
|
+
let jsDep = {};
|
|
12
|
+
let phpDep = {};
|
|
13
|
+
let configFiles = [];
|
|
14
|
+
/**
|
|
15
|
+
* Core install logic for a single plugin or theme.
|
|
16
|
+
* Extracted so it can be called recursively for evophp dependencies.
|
|
17
|
+
*
|
|
18
|
+
* @param {'plugin'|'theme'} type
|
|
19
|
+
* @param {string} author
|
|
20
|
+
* @param {string} pluginName
|
|
21
|
+
* @param {string} gitHubToken
|
|
22
|
+
* @param {Set<string>} installed - tracks already-installed entries to avoid infinite loops
|
|
23
|
+
*/
|
|
24
|
+
async function installOne(type, author, pluginName, gitHubToken, installed = new Set()) {
|
|
25
|
+
const key = `${type}:${author}/${pluginName}`;
|
|
26
|
+
if (installed.has(key)) {
|
|
27
|
+
log.info(`Skipping already-installed ${type} "${author}/${pluginName}"`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
installed.add(key);
|
|
31
|
+
|
|
32
|
+
const paths = type === 'plugin'
|
|
33
|
+
? [path.join('Public', 'Modules', pluginName), path.join('EvoJsDev', 'Modules', pluginName)]
|
|
34
|
+
: [path.join('Public', 'Themes', pluginName)];
|
|
35
|
+
|
|
36
|
+
// Skip clone if already present on disk
|
|
37
|
+
if (paths.every(p => fs.existsSync(p))) {
|
|
38
|
+
log.warn(`${type} "${pluginName}" already exists on disk, skipping clone.`);
|
|
39
|
+
} else {
|
|
40
|
+
log.info(`Installing ${type} "${pluginName}" from ${author}...`);
|
|
41
|
+
|
|
42
|
+
if (type === 'plugin') {
|
|
43
|
+
await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
|
|
44
|
+
await cloneRepo(`${author}/${pluginName}.js`, null, paths[1]);
|
|
45
|
+
|
|
46
|
+
const configFile = path.resolve(`./EvoJsDev/Modules/${pluginName}/config.json`);
|
|
47
|
+
if (fs.existsSync(configFile)) {
|
|
48
|
+
const pluginConfig = require(configFile);
|
|
49
|
+
mergeConfig(pluginConfig, "./config.json", false);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let newJsDep = await extractDependencies(paths[1], 'js');
|
|
53
|
+
jsDep = await mergeDependencies(newJsDep, jsDep);
|
|
54
|
+
|
|
55
|
+
let newPhpDep = await extractDependencies(paths[0], 'php');
|
|
56
|
+
phpDep = await mergeDependencies(newPhpDep, phpDep);
|
|
57
|
+
|
|
58
|
+
} else if (type === 'theme') {
|
|
59
|
+
await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Read dependencies.json to find dependent evophp plugins/themes
|
|
64
|
+
const depFile = type === 'plugin'
|
|
65
|
+
? path.join('EvoJsDev', 'Modules', pluginName, 'dependencies.json')
|
|
66
|
+
: path.join('Public', 'Themes', pluginName, 'dependencies.json');
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(depFile)) return;
|
|
69
|
+
|
|
70
|
+
let deps;
|
|
71
|
+
try {
|
|
72
|
+
deps = JSON.parse(fs.readFileSync(depFile, 'utf-8'));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
log.warn(`Could not parse dependencies.json for ${type} "${pluginName}": ${err.message}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Recursively install dependent evophp plugins
|
|
79
|
+
const pluginDeps = (deps.plugin || '').split(' ').map(d => d.trim()).filter(Boolean);
|
|
80
|
+
for (const dep of pluginDeps) {
|
|
81
|
+
if (!dep.includes('/')) {
|
|
82
|
+
log.warn(`Skipping malformed plugin dependency "${dep}" (expected author/name)`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const [depAuthor, depName] = dep.split('/');
|
|
86
|
+
log.info(`Installing dependent plugin "${dep}" required by ${type} "${pluginName}"...`);
|
|
87
|
+
await installOne('plugin', depAuthor, depName, gitHubToken, installed);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Recursively install dependent evophp themes
|
|
91
|
+
const themeDeps = (deps.theme || '').split(' ').map(d => d.trim()).filter(Boolean);
|
|
92
|
+
for (const dep of themeDeps) {
|
|
93
|
+
if (!dep.includes('/')) {
|
|
94
|
+
log.warn(`Skipping malformed theme dependency "${dep}" (expected author/name)`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const [depAuthor, depName] = dep.split('/');
|
|
98
|
+
log.info(`Installing dependent theme "${dep}" required by ${type} "${pluginName}"...`);
|
|
99
|
+
await installOne('theme', depAuthor, depName, gitHubToken, installed);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
11
103
|
module.exports = {
|
|
12
104
|
command: 'install [type] [name]',
|
|
13
105
|
aliases: ['i'],
|
|
@@ -36,10 +128,8 @@ module.exports = {
|
|
|
36
128
|
let gitHubToken = process.env.GITHUB_TOKEN;
|
|
37
129
|
|
|
38
130
|
if (name.includes("/")) {
|
|
39
|
-
// Full author/name provided — no auth required
|
|
40
131
|
[author, pluginName] = name.split("/");
|
|
41
132
|
} else {
|
|
42
|
-
// Token required to determine user
|
|
43
133
|
if (!gitHubToken) {
|
|
44
134
|
gitHubToken = await readLineAsync("Enter your GitHub Personal Access Token: ");
|
|
45
135
|
saveToEnv("GITHUB_TOKEN", gitHubToken);
|
|
@@ -51,6 +141,7 @@ module.exports = {
|
|
|
51
141
|
log.info(`Authenticated as ${author}`);
|
|
52
142
|
}
|
|
53
143
|
|
|
144
|
+
// Guard: exit early if the top-level target already exists
|
|
54
145
|
const paths = type === 'plugin'
|
|
55
146
|
? [path.join('Public', 'Modules', pluginName), path.join('EvoJsDev', 'Modules', pluginName)]
|
|
56
147
|
: [path.join('Public', 'Themes', pluginName)];
|
|
@@ -62,24 +153,14 @@ module.exports = {
|
|
|
62
153
|
}
|
|
63
154
|
}
|
|
64
155
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
|
|
69
|
-
await cloneRepo(`${author}/${pluginName}.js`, null, paths[1]);
|
|
156
|
+
// Shared Set across the entire install tree to prevent duplicate/circular installs
|
|
157
|
+
const installed = new Set();
|
|
158
|
+
await installOne(type, author, pluginName, gitHubToken, installed);
|
|
70
159
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const pluginConfig = require(configFile);
|
|
74
|
-
mergeConfig(pluginConfig);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
await installDependencies(paths[1], 'js');
|
|
78
|
-
await installDependencies(paths[0], 'php');
|
|
160
|
+
await installDependencies(jsDep, 'js');
|
|
161
|
+
await installDependencies(phpDep, 'php');
|
|
79
162
|
|
|
80
|
-
}
|
|
81
|
-
await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
|
|
82
|
-
}
|
|
163
|
+
mergeConfig({});
|
|
83
164
|
|
|
84
165
|
log.success(`The ${type} "${pluginName}" has been installed successfully.`);
|
|
85
166
|
|
package/commands/uninstall.js
CHANGED
|
@@ -5,6 +5,139 @@ const editJsonFile = require("edit-json-file");
|
|
|
5
5
|
const { readLineAsync } = require("../utils/helpers");
|
|
6
6
|
const log = require("../utils/logger");
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Returns all installed plugin and theme names as { type, name } objects.
|
|
10
|
+
*/
|
|
11
|
+
function getInstalledItems() {
|
|
12
|
+
const items = [];
|
|
13
|
+
|
|
14
|
+
const pluginDir = path.join("Public", "Modules");
|
|
15
|
+
if (fs.existsSync(pluginDir)) {
|
|
16
|
+
fs.readdirSync(pluginDir).forEach(name => {
|
|
17
|
+
if (fs.statSync(path.join(pluginDir, name)).isDirectory()) {
|
|
18
|
+
items.push({ type: "plugin", name });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const themeDir = path.join("Public", "Themes");
|
|
24
|
+
if (fs.existsSync(themeDir)) {
|
|
25
|
+
fs.readdirSync(themeDir).forEach(name => {
|
|
26
|
+
if (fs.statSync(path.join(themeDir, name)).isDirectory()) {
|
|
27
|
+
items.push({ type: "theme", name });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return items;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads the dependencies.json for a given installed item.
|
|
37
|
+
* Returns parsed object or null if absent/unreadable.
|
|
38
|
+
*
|
|
39
|
+
* @param {'plugin'|'theme'} type
|
|
40
|
+
* @param {string} name
|
|
41
|
+
*/
|
|
42
|
+
function readDepsFile(type, name) {
|
|
43
|
+
const depFile = type === "plugin"
|
|
44
|
+
? path.join("EvoJsDev", "Modules", name, "dependencies.json")
|
|
45
|
+
: path.join("Public", "Themes", name, "dependencies.json");
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(depFile)) return null;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(depFile, "utf-8"));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Finds all installed plugins/themes that declare a dependency on the target.
|
|
58
|
+
* Returns array of { type, name } of the dependents.
|
|
59
|
+
*
|
|
60
|
+
* @param {'plugin'|'theme'} targetType
|
|
61
|
+
* @param {string} targetName - just the name portion (e.g. "Invoice"), matched
|
|
62
|
+
* against both "name" and "author/name" entries
|
|
63
|
+
*/
|
|
64
|
+
function findDependents(targetType, targetName) {
|
|
65
|
+
const dependents = [];
|
|
66
|
+
const depKey = targetType === "plugin" ? "plugin" : "theme";
|
|
67
|
+
|
|
68
|
+
for (const item of getInstalledItems()) {
|
|
69
|
+
// Don't check the item against itself
|
|
70
|
+
if (item.type === targetType && item.name === targetName) continue;
|
|
71
|
+
|
|
72
|
+
const deps = readDepsFile(item.type, item.name);
|
|
73
|
+
if (!deps || !deps[depKey]) continue;
|
|
74
|
+
|
|
75
|
+
const listed = deps[depKey]
|
|
76
|
+
.split(" ")
|
|
77
|
+
.map(d => d.trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
|
|
80
|
+
const isDependent = listed.some(d => {
|
|
81
|
+
// Match "Invoice" or "kinsokafor/Invoice"
|
|
82
|
+
const parts = d.split("/");
|
|
83
|
+
return parts[parts.length - 1] === targetName;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (isDependent) {
|
|
87
|
+
dependents.push(item);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return dependents;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Uninstalls a single plugin or theme after checking for dependents.
|
|
96
|
+
* Returns true if removed, false if retained due to dependents.
|
|
97
|
+
*
|
|
98
|
+
* @param {'plugin'|'theme'} type
|
|
99
|
+
* @param {string} name
|
|
100
|
+
* @param {boolean} force - skip dependent check (used when caller already confirmed)
|
|
101
|
+
*/
|
|
102
|
+
async function uninstallOne(type, name, force = false) {
|
|
103
|
+
const paths = type === "plugin"
|
|
104
|
+
? [path.join("Public", "Modules", name), path.join("EvoJsDev", "Modules", name)]
|
|
105
|
+
: [path.join("Public", "Themes", name)];
|
|
106
|
+
|
|
107
|
+
const existing = paths.filter(fs.existsSync);
|
|
108
|
+
if (existing.length === 0) {
|
|
109
|
+
log.warn(`${type} "${name}" not found on disk, skipping.`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!force) {
|
|
114
|
+
const dependents = findDependents(type, name);
|
|
115
|
+
if (dependents.length > 0) {
|
|
116
|
+
const depList = dependents.map(d => `${d.type} "${d.name}"`).join(", ");
|
|
117
|
+
log.warn(`Skipping ${type} "${name}" — still required by: ${depList}`);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const dir of existing) {
|
|
123
|
+
rimraf.sync(dir);
|
|
124
|
+
log.success(`Deleted: ${dir}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (type === "plugin") {
|
|
128
|
+
const configPath = path.resolve("config.json");
|
|
129
|
+
if (fs.existsSync(configPath)) {
|
|
130
|
+
let file = editJsonFile(configPath);
|
|
131
|
+
file.unset(`modules.${name}`);
|
|
132
|
+
file.save();
|
|
133
|
+
log.success(`Removed '${name}' from config.json`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
log.success(`${type} "${name}" uninstalled.`);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
8
141
|
module.exports = {
|
|
9
142
|
command: "uninstall [type] [name]",
|
|
10
143
|
aliases: ["u"],
|
|
@@ -36,13 +169,11 @@ module.exports = {
|
|
|
36
169
|
}
|
|
37
170
|
}
|
|
38
171
|
|
|
39
|
-
const paths =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: [path.join("Public", "Themes", name)];
|
|
172
|
+
const paths = type === "plugin"
|
|
173
|
+
? [path.join("Public", "Modules", name), path.join("EvoJsDev", "Modules", name)]
|
|
174
|
+
: [path.join("Public", "Themes", name)];
|
|
43
175
|
|
|
44
176
|
const existing = paths.filter(fs.existsSync);
|
|
45
|
-
|
|
46
177
|
if (existing.length === 0) {
|
|
47
178
|
log.warn(`No matching ${type} found for "${name}".`);
|
|
48
179
|
process.exit(0);
|
|
@@ -57,18 +188,27 @@ module.exports = {
|
|
|
57
188
|
return;
|
|
58
189
|
}
|
|
59
190
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
191
|
+
// Remove the main target
|
|
192
|
+
await uninstallOne(type, name, true);
|
|
193
|
+
|
|
194
|
+
// Now attempt to remove its evophp dependencies, retaining any still used elsewhere
|
|
195
|
+
const deps = readDepsFile(type, name);
|
|
196
|
+
if (deps) {
|
|
197
|
+
const pluginDeps = (deps.plugin || "").split(" ").map(d => d.trim()).filter(Boolean);
|
|
198
|
+
const themeDeps = (deps.theme || "").split(" ").map(d => d.trim()).filter(Boolean);
|
|
199
|
+
|
|
200
|
+
if (pluginDeps.length > 0 || themeDeps.length > 0) {
|
|
201
|
+
log.info(`Checking evophp dependencies of "${name}" for removal...`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const dep of pluginDeps) {
|
|
205
|
+
const depName = dep.includes("/") ? dep.split("/").pop() : dep;
|
|
206
|
+
await uninstallOne("plugin", depName);
|
|
207
|
+
}
|
|
64
208
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let file = editJsonFile(configPath);
|
|
69
|
-
file.unset(`modules.${name}`);
|
|
70
|
-
file.save();
|
|
71
|
-
log.success(`Removed '${name}' from config.json`);
|
|
209
|
+
for (const dep of themeDeps) {
|
|
210
|
+
const depName = dep.includes("/") ? dep.split("/").pop() : dep;
|
|
211
|
+
await uninstallOne("theme", depName);
|
|
72
212
|
}
|
|
73
213
|
}
|
|
74
214
|
|
|
@@ -78,4 +218,4 @@ module.exports = {
|
|
|
78
218
|
process.exit(1);
|
|
79
219
|
}
|
|
80
220
|
}
|
|
81
|
-
};
|
|
221
|
+
};
|
package/package.json
CHANGED
package/utils/dependencies.js
CHANGED
|
@@ -3,31 +3,59 @@ const path = require('path');
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const log = require('./logger');
|
|
5
5
|
|
|
6
|
-
async function installDependencies(
|
|
7
|
-
const dependenciesFile = path.join(dir, 'dependencies.json');
|
|
8
|
-
if (!fs.existsSync(dependenciesFile)) {
|
|
9
|
-
log.info(`No ${type} dependencies.json found in ${dir}`);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const deps = JSON.parse(fs.readFileSync(dependenciesFile, 'utf-8'));
|
|
6
|
+
async function installDependencies(deps, type) {
|
|
14
7
|
const cwd = process.cwd();
|
|
15
8
|
|
|
16
9
|
if (type === 'js') {
|
|
17
|
-
if (deps.prod) {
|
|
10
|
+
if (deps.prod && (deps.prod || "").trim() != "") {
|
|
18
11
|
log.info("Installing JS production dependencies...");
|
|
19
12
|
execSync(`npm install ${deps.prod}`, { cwd, stdio: 'inherit' });
|
|
20
13
|
}
|
|
21
|
-
if (deps.dev) {
|
|
14
|
+
if (deps.dev && (deps.dev || "").trim() != "") {
|
|
22
15
|
log.info("Installing JS development dependencies...");
|
|
23
16
|
execSync(`npm install ${deps.dev} --save-dev`, { cwd, stdio: 'inherit' });
|
|
24
17
|
}
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
if (type === 'php' && deps.prod) {
|
|
20
|
+
if (type === 'php' && deps.prod && (deps.prod || "").trim() != "") {
|
|
28
21
|
log.info("Installing PHP dependencies...");
|
|
29
22
|
execSync(`composer require ${deps.prod}`, { cwd, stdio: 'inherit' });
|
|
30
23
|
}
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
async function extractDependencies(dir, type)
|
|
27
|
+
{
|
|
28
|
+
const dependenciesFile = path.join(dir, 'dependencies.json');
|
|
29
|
+
if (!fs.existsSync(dependenciesFile)) {
|
|
30
|
+
log.info(`No ${type} dependencies.json found in ${dir}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const deps = JSON.parse(fs.readFileSync(dependenciesFile, 'utf-8'));
|
|
35
|
+
|
|
36
|
+
return deps;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function mergeDependencies(newDep, oldDep) {
|
|
40
|
+
const res = await new Promise((resolve) => {
|
|
41
|
+
for (const key in newDep) {
|
|
42
|
+
if (oldDep[key]) {
|
|
43
|
+
oldDep[key] = [
|
|
44
|
+
...new Set(
|
|
45
|
+
`${oldDep[key]} ${newDep[key]}`
|
|
46
|
+
.trim()
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
)
|
|
50
|
+
].join(" ");
|
|
51
|
+
} else {
|
|
52
|
+
oldDep[key] = String(newDep[key]).trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
resolve(oldDep);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { installDependencies, extractDependencies, mergeDependencies };
|
package/utils/helpers.js
CHANGED
|
@@ -67,7 +67,7 @@ const mergeDeep = async (target, source) => {
|
|
|
67
67
|
return target;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
const mergeConfig = async (config, filePath = "./config.json") => {
|
|
70
|
+
const mergeConfig = async (config, filePath = "./config.json", runConfig = true) => {
|
|
71
71
|
const file = editJsonFile(filePath);
|
|
72
72
|
for (const key in config) {
|
|
73
73
|
const existing = file.get(key);
|
|
@@ -83,12 +83,14 @@ const mergeConfig = async (config, filePath = "./config.json") => {
|
|
|
83
83
|
}
|
|
84
84
|
file.save();
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
if(runConfig) {
|
|
87
|
+
// Run build process if entry config changes
|
|
88
|
+
try {
|
|
89
|
+
console.log("\n[INFO] Running npm run dev to recompile JS bundle...");
|
|
90
|
+
execSync("npm run dev", { stdio: "inherit" });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("[ERROR] Failed to run 'npm run dev':", error.message);
|
|
93
|
+
}
|
|
92
94
|
}
|
|
93
95
|
};
|
|
94
96
|
|