@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.
Files changed (2) hide show
  1. package/package.json +2 -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.14",
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.14",
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"
@@ -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
- const gitPath = join(__dirname, "..", "..", "Dockerfile.release");
27
- return await pathExists(gitPath);
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
- * check if the host supports the plugin and return a warning if not
87
- * @param pckJSON
91
+ *
92
+ * @param {boolean} force
93
+ * @param {boolean} preInstall Only npm install without loading the module
88
94
  * @returns
89
95
  */
90
- checkEngineWarning(pckJSON) {
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.ensurePluginsRootFolders();
107
- if (fixedPlugins.includes(this.plugin.location))
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
- try {
140
- module = await this.loadMainFile(pckJSON);
141
- } catch (e) {
142
- if (e.code === "MODULE_NOT_FOUND") {
143
- getState().log(5, `corrupt plugin dir: ${this.pluginDir}`);
144
- const files = await readdir(this.pluginDir);
145
- getState().log(5, `files in plugin dir: ${JSON.stringify(files)}`);
146
- if (files.includes("node_modules")) {
147
- const nodeModuleFiles = await readdir(
148
- join(this.pluginDir, "node_modules")
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
- 5,
152
- `node_modules files: ${JSON.stringify(nodeModuleFiles)}`
125
+ 2,
126
+ `Error loading plugin ${this.plugin.name}. Trying again with reload flag. ` +
127
+ "A server restart may be required."
153
128
  );
154
- } else getState().log(5, `no node_modules in plugin dir`);
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
- module = await this.loadMainFile(pckJSON, true);
176
- loadedWithReload = true;
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
- async prepPluginsFolder(force, pckJSON) {
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.versionIsInstalled(pckJSON))) ||
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 ensurePluginsRootFolders() {
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
- isFixedVersion() {
277
+ _isFixedVersion() {
258
278
  return !!this.plugin.version && this.plugin.version !== "latest";
259
279
  }
260
280
 
261
- async versionIsInstalled(pckJSON) {
262
- if (!pckJSON || !this.isFixedVersion()) return false;
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 loadMainFile(pckJSON, reload) {
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 removeDependencies(tmpPckJSON, writeToDisk) {
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 npmInstall(pckJSON) {
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 movePlugin(wasInstalled) {
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;