@saltcorn/plugins-loader 1.5.0-beta.14 → 1.5.0-beta.16
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/package.json +2 -2
- package/plugin_installer.js +126 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/plugins-loader",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.16",
|
|
4
4
|
"description": "Saltcorn plugin loader",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"scripts": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"clean": "echo 'No TypeScript build'"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@saltcorn/data": "1.5.0-beta.
|
|
12
|
+
"@saltcorn/data": "1.5.0-beta.16",
|
|
13
13
|
"env-paths": "^2.2.1",
|
|
14
14
|
"npm-registry-fetch": "17.1.0",
|
|
15
15
|
"https-proxy-agent": "^7.0.6"
|
package/plugin_installer.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
removeTarball,
|
|
10
10
|
} = require("./download_utils");
|
|
11
11
|
const { getState } = require("@saltcorn/data/db/state");
|
|
12
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
12
13
|
const { rm, rename, cp, readFile, readdir } = require("fs").promises;
|
|
13
14
|
const envPaths = require("env-paths");
|
|
14
15
|
const semver = require("semver");
|
|
@@ -20,11 +21,12 @@ const staticDeps = [
|
|
|
20
21
|
"@saltcorn/postgres",
|
|
21
22
|
"jest",
|
|
22
23
|
];
|
|
23
|
-
const fixedPlugins = ["@saltcorn/base-plugin", "@saltcorn/sbadmin2"];
|
|
24
24
|
|
|
25
25
|
const isGitCheckout = async () => {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
return (
|
|
27
|
+
(await pathExists(join(__dirname, "..", "..", "Dockerfile.release"))) ||
|
|
28
|
+
(await pathExists(join(__dirname, "..", "..", ".git")))
|
|
29
|
+
);
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
const readPackageJson = async (filePath) => {
|
|
@@ -53,6 +55,9 @@ const npmInstallNeeded = (oldPckJSON, newPckJSON) => {
|
|
|
53
55
|
|
|
54
56
|
const defaultRootFolder = envPaths("saltcorn", { suffix: "plugins" }).data;
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* PluginInstaller class
|
|
60
|
+
*/
|
|
56
61
|
class PluginInstaller {
|
|
57
62
|
constructor(plugin, opts = Object.create(null)) {
|
|
58
63
|
this.plugin = plugin;
|
|
@@ -83,100 +88,52 @@ class PluginInstaller {
|
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @param
|
|
91
|
+
*
|
|
92
|
+
* @param {boolean} force
|
|
93
|
+
* @param {boolean} preInstall Only npm install without loading the module
|
|
88
94
|
* @returns
|
|
89
95
|
*/
|
|
90
|
-
|
|
91
|
-
const scEngine = pckJSON.engines?.saltcorn;
|
|
92
|
-
if (
|
|
93
|
-
this.scVersion &&
|
|
94
|
-
scEngine &&
|
|
95
|
-
!semver.satisfies(this.scVersion, scEngine)
|
|
96
|
-
) {
|
|
97
|
-
const warnMsg = `Plugin ${this.plugin.name} requires Saltcorn version ${scEngine} but running ${this.scVersion}`;
|
|
98
|
-
getState().log(4, warnMsg);
|
|
99
|
-
return warnMsg;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async install(force) {
|
|
96
|
+
async install(force = false, preInstall = false) {
|
|
105
97
|
getState().log(5, `loading plugin ${this.plugin.name}`);
|
|
106
|
-
await this.
|
|
107
|
-
if (
|
|
98
|
+
await this._ensurePluginsRootFolders();
|
|
99
|
+
if (Plugin.is_fixed_plugin(this.plugin.location))
|
|
108
100
|
return {
|
|
109
101
|
location: path.join(require.resolve(this.plugin.location), ".."),
|
|
110
102
|
plugin_module: require(this.plugin.location),
|
|
111
103
|
};
|
|
112
104
|
const msgs = [];
|
|
113
|
-
let pckJSON = await readPackageJson(this.pckJsonPath);
|
|
114
|
-
const installer = async () => {
|
|
115
|
-
if (await this.prepPluginsFolder(force, pckJSON)) {
|
|
116
|
-
const tmpPckJSON = await this.removeDependencies(
|
|
117
|
-
await readPackageJson(this.tempPckJsonPath),
|
|
118
|
-
true
|
|
119
|
-
);
|
|
120
|
-
let wasInstalled = false;
|
|
121
|
-
if (
|
|
122
|
-
!pckJSON ||
|
|
123
|
-
npmInstallNeeded(await this.removeDependencies(pckJSON), tmpPckJSON)
|
|
124
|
-
) {
|
|
125
|
-
wasInstalled = true;
|
|
126
|
-
await this.npmInstall(tmpPckJSON);
|
|
127
|
-
}
|
|
128
|
-
await this.movePlugin(wasInstalled);
|
|
129
|
-
if (await tarballExists(this.rootFolder, this.plugin))
|
|
130
|
-
await removeTarball(this.rootFolder, this.plugin);
|
|
131
|
-
pckJSON = await readPackageJson(this.pckJsonPath);
|
|
132
|
-
const msg = this.checkEngineWarning(pckJSON);
|
|
133
|
-
if (msg && !msgs.includes(msg)) msgs.push(msg);
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
await installer();
|
|
137
105
|
let module = null;
|
|
138
106
|
let loadedWithReload = false;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
107
|
+
let pckJSON = await this._installHelper(force, msgs);
|
|
108
|
+
if (!preInstall) {
|
|
109
|
+
try {
|
|
110
|
+
module = await this._loadMainFile(pckJSON);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (e.code === "MODULE_NOT_FOUND") await this._dumpNodeMoules();
|
|
113
|
+
if (force) {
|
|
114
|
+
// remove and try again
|
|
115
|
+
// could happen when there is a directory with a package.json
|
|
116
|
+
// but without a valid node modules folder
|
|
117
|
+
getState().log(
|
|
118
|
+
2,
|
|
119
|
+
`Error loading plugin ${this.plugin.name}. Removing and trying again.`
|
|
149
120
|
);
|
|
121
|
+
await this.remove();
|
|
122
|
+
pckJSON = await this._installHelper(force, msgs);
|
|
123
|
+
} else {
|
|
150
124
|
getState().log(
|
|
151
|
-
|
|
152
|
-
`
|
|
125
|
+
2,
|
|
126
|
+
`Error loading plugin ${this.plugin.name}. Trying again with reload flag. ` +
|
|
127
|
+
"A server restart may be required."
|
|
153
128
|
);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (force) {
|
|
157
|
-
// remove and try again
|
|
158
|
-
// could happen when there is a directory with a package.json
|
|
159
|
-
// but without a valid node modules folder
|
|
160
|
-
getState().log(
|
|
161
|
-
2,
|
|
162
|
-
`Error loading plugin ${this.plugin.name}. Removing and trying again.`
|
|
163
|
-
);
|
|
164
|
-
await this.remove();
|
|
165
|
-
pckJSON = null;
|
|
166
|
-
await installer();
|
|
167
|
-
} else {
|
|
168
|
-
getState().log(
|
|
169
|
-
2,
|
|
170
|
-
`Error loading plugin ${this.plugin.name}. Trying again with reload flag. ` +
|
|
171
|
-
"A server restart may be required."
|
|
172
|
-
);
|
|
173
|
-
}
|
|
129
|
+
}
|
|
174
130
|
|
|
175
|
-
|
|
176
|
-
|
|
131
|
+
module = await this._loadMainFile(pckJSON, true);
|
|
132
|
+
loadedWithReload = true;
|
|
133
|
+
}
|
|
177
134
|
}
|
|
178
135
|
return {
|
|
179
|
-
version: pckJSON.version,
|
|
136
|
+
version: this.plugin.version === "latest" ? "latest" : pckJSON.version,
|
|
180
137
|
plugin_module: module,
|
|
181
138
|
location: this.pluginDir,
|
|
182
139
|
name: this.plugin.name,
|
|
@@ -185,18 +142,81 @@ class PluginInstaller {
|
|
|
185
142
|
};
|
|
186
143
|
}
|
|
187
144
|
|
|
145
|
+
/**
|
|
146
|
+
* remove plugin directory
|
|
147
|
+
*/
|
|
188
148
|
async remove() {
|
|
189
149
|
if (await pathExists(this.pluginDir))
|
|
190
150
|
await rm(this.pluginDir, { recursive: true });
|
|
191
151
|
}
|
|
192
152
|
|
|
193
|
-
|
|
153
|
+
// -- private methods --
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* prepare the plugin folder and npm install if needed
|
|
157
|
+
* @param {*} force
|
|
158
|
+
* @param {*} pckJSON
|
|
159
|
+
* @param {*} msgs
|
|
160
|
+
*/
|
|
161
|
+
async _installHelper(force, msgs) {
|
|
162
|
+
const airgap = getState().getConfig("airgap", false);
|
|
163
|
+
let pckJSON = await readPackageJson(this.pckJsonPath);
|
|
164
|
+
if (airgap) return pckJSON;
|
|
165
|
+
else if (await this._prepPluginsFolder(force, pckJSON)) {
|
|
166
|
+
const tmpPckJSON = await this._removeDependencies(
|
|
167
|
+
await readPackageJson(this.tempPckJsonPath),
|
|
168
|
+
true
|
|
169
|
+
);
|
|
170
|
+
let wasInstalled = false;
|
|
171
|
+
if (
|
|
172
|
+
!pckJSON ||
|
|
173
|
+
npmInstallNeeded(await this._removeDependencies(pckJSON), tmpPckJSON)
|
|
174
|
+
) {
|
|
175
|
+
wasInstalled = true;
|
|
176
|
+
await this._npmInstall(tmpPckJSON);
|
|
177
|
+
}
|
|
178
|
+
await this._movePlugin(wasInstalled);
|
|
179
|
+
if (await tarballExists(this.rootFolder, this.plugin))
|
|
180
|
+
await removeTarball(this.rootFolder, this.plugin);
|
|
181
|
+
pckJSON = await readPackageJson(this.pckJsonPath);
|
|
182
|
+
const msg = this._checkEngineWarning(pckJSON);
|
|
183
|
+
if (msg && !msgs.includes(msg)) msgs.push(msg);
|
|
184
|
+
}
|
|
185
|
+
return pckJSON;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* check if the host supports the plugin and return a warning if not
|
|
190
|
+
* @param pckJSON
|
|
191
|
+
* @returns
|
|
192
|
+
*/
|
|
193
|
+
_checkEngineWarning(pckJSON) {
|
|
194
|
+
const scEngine = pckJSON.engines?.saltcorn;
|
|
195
|
+
if (
|
|
196
|
+
this.scVersion &&
|
|
197
|
+
scEngine &&
|
|
198
|
+
!semver.satisfies(this.scVersion, scEngine)
|
|
199
|
+
) {
|
|
200
|
+
const warnMsg = `Plugin ${this.plugin.name} requires Saltcorn version ${scEngine} but running ${this.scVersion}`;
|
|
201
|
+
getState().log(4, warnMsg);
|
|
202
|
+
return warnMsg;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* helper to prepare the plugin folder
|
|
209
|
+
* @param {*} force
|
|
210
|
+
* @param {*} pckJSON
|
|
211
|
+
* @returns
|
|
212
|
+
*/
|
|
213
|
+
async _prepPluginsFolder(force, pckJSON) {
|
|
194
214
|
let wasLoaded = false;
|
|
195
215
|
const folderExists = await pathExists(this.pluginDir);
|
|
196
216
|
switch (this.plugin.source) {
|
|
197
217
|
case "npm":
|
|
198
218
|
if (
|
|
199
|
-
(force && !(await this.
|
|
219
|
+
(force && !(await this._versionIsInstalled(pckJSON))) ||
|
|
200
220
|
!folderExists
|
|
201
221
|
) {
|
|
202
222
|
getState().log(6, "downloading from npm");
|
|
@@ -237,7 +257,7 @@ class PluginInstaller {
|
|
|
237
257
|
return wasLoaded;
|
|
238
258
|
}
|
|
239
259
|
|
|
240
|
-
async
|
|
260
|
+
async _ensurePluginsRootFolders() {
|
|
241
261
|
const isWindows = process.platform === "win32";
|
|
242
262
|
const ensureFn = async (folder) => {
|
|
243
263
|
const pluginsFolder = join(this.rootFolder, folder);
|
|
@@ -254,12 +274,12 @@ class PluginInstaller {
|
|
|
254
274
|
await ensureFn(folder);
|
|
255
275
|
}
|
|
256
276
|
|
|
257
|
-
|
|
277
|
+
_isFixedVersion() {
|
|
258
278
|
return !!this.plugin.version && this.plugin.version !== "latest";
|
|
259
279
|
}
|
|
260
280
|
|
|
261
|
-
async
|
|
262
|
-
if (!pckJSON || !this.
|
|
281
|
+
async _versionIsInstalled(pckJSON) {
|
|
282
|
+
if (!pckJSON || !this._isFixedVersion()) return false;
|
|
263
283
|
else {
|
|
264
284
|
const vInstalled = pckJSON.version;
|
|
265
285
|
if (vInstalled === this.plugin.version) return true;
|
|
@@ -267,7 +287,7 @@ class PluginInstaller {
|
|
|
267
287
|
}
|
|
268
288
|
}
|
|
269
289
|
|
|
270
|
-
async
|
|
290
|
+
async _loadMainFile(pckJSON, reload) {
|
|
271
291
|
const isWindows = process.platform === "win32";
|
|
272
292
|
if (process.env.NODE_ENV === "test") {
|
|
273
293
|
// in jest, downgrad to require
|
|
@@ -281,7 +301,7 @@ class PluginInstaller {
|
|
|
281
301
|
}
|
|
282
302
|
}
|
|
283
303
|
|
|
284
|
-
async
|
|
304
|
+
async _removeDependencies(tmpPckJSON, writeToDisk) {
|
|
285
305
|
const pckJSON = { ...tmpPckJSON };
|
|
286
306
|
const oldDepsLength = Object.keys(pckJSON.dependencies || {}).length;
|
|
287
307
|
const oldDevDepsLength = Object.keys(pckJSON.devDependencies || {}).length;
|
|
@@ -304,7 +324,7 @@ class PluginInstaller {
|
|
|
304
324
|
return pckJSON;
|
|
305
325
|
}
|
|
306
326
|
|
|
307
|
-
async
|
|
327
|
+
async _npmInstall(pckJSON) {
|
|
308
328
|
const isWindows = process.platform === "win32";
|
|
309
329
|
if (
|
|
310
330
|
Object.keys(pckJSON.dependencies || {}).length > 0 ||
|
|
@@ -343,7 +363,7 @@ class PluginInstaller {
|
|
|
343
363
|
}
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
async
|
|
366
|
+
async _movePlugin(wasInstalled) {
|
|
347
367
|
const isWindows = process.platform === "win32";
|
|
348
368
|
const copyMove = async () => {
|
|
349
369
|
await cp(this.tempDir, this.pluginDir, { recursive: true, force: true });
|
|
@@ -366,6 +386,21 @@ class PluginInstaller {
|
|
|
366
386
|
} else await copyMove();
|
|
367
387
|
} else await copyMove();
|
|
368
388
|
}
|
|
389
|
+
|
|
390
|
+
async _dumpNodeMoules() {
|
|
391
|
+
getState().log(5, `corrupt plugin dir: ${this.pluginDir}`);
|
|
392
|
+
const files = await readdir(this.pluginDir);
|
|
393
|
+
getState().log(5, `files in plugin dir: ${JSON.stringify(files)}`);
|
|
394
|
+
if (files.includes("node_modules")) {
|
|
395
|
+
const nodeModuleFiles = await readdir(
|
|
396
|
+
join(this.pluginDir, "node_modules")
|
|
397
|
+
);
|
|
398
|
+
getState().log(
|
|
399
|
+
5,
|
|
400
|
+
`node_modules files: ${JSON.stringify(nodeModuleFiles)}`
|
|
401
|
+
);
|
|
402
|
+
} else getState().log(5, `no node_modules in plugin dir`);
|
|
403
|
+
}
|
|
369
404
|
}
|
|
370
405
|
|
|
371
406
|
module.exports = PluginInstaller;
|