@saltcorn/plugins-loader 1.0.0-beta.9 → 1.0.0-rc.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/download_utils.js CHANGED
@@ -2,7 +2,7 @@ const fs = require("fs");
2
2
  const { rm } = require("fs").promises;
3
3
  const { mkdir, pathExists } = require("fs-extra");
4
4
  const { tmpName } = require("tmp-promise");
5
- const { execSync } = require("child_process");
5
+ const { execFileSync } = require("child_process");
6
6
  const { extract } = require("tar");
7
7
  const { join } = require("path");
8
8
  const { createWriteStream, unlink } = require("fs");
@@ -93,7 +93,10 @@ const loadTarball = (rootFolder, url, name) => {
93
93
  */
94
94
  const gitPullOrClone = async (plugin, pluginDir) => {
95
95
  let keyfnm,
96
- setKey = `-c core.sshCommand="ssh -oBatchMode=yes -o 'StrictHostKeyChecking no'" `;
96
+ setKey = [
97
+ "-c",
98
+ `core.sshCommand="ssh -oBatchMode=yes -o 'StrictHostKeyChecking no'"`,
99
+ ];
97
100
  if (plugin.deploy_private_key) {
98
101
  keyfnm = await tmpName();
99
102
  await fs.promises.writeFile(
@@ -104,12 +107,15 @@ const gitPullOrClone = async (plugin, pluginDir) => {
104
107
  encoding: "ascii",
105
108
  }
106
109
  );
107
- setKey = `-c core.sshCommand="ssh -oBatchMode=yes -o 'StrictHostKeyChecking no' -i ${keyfnm}" `;
110
+ setKey = [
111
+ "-c",
112
+ `core.sshCommand="ssh -oBatchMode=yes -o 'StrictHostKeyChecking no' -i ${keyfnm}"`,
113
+ ];
108
114
  }
109
115
  if (fs.existsSync(pluginDir)) {
110
- execSync(`git ${setKey} -C ${pluginDir} pull`);
116
+ execFileSync("git", [...setKey, "-C", pluginDir, "pull"]);
111
117
  } else {
112
- execSync(`git ${setKey} clone ${plugin.location} ${pluginDir}`);
118
+ execFileSync("git", [...setKey, "clone", plugin.location, pluginDir]);
113
119
  }
114
120
  if (plugin.deploy_private_key && keyfnm) await fs.promises.unlink(keyfnm);
115
121
  };
package/package.json CHANGED
@@ -1,20 +1,36 @@
1
1
  {
2
2
  "name": "@saltcorn/plugins-loader",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "Saltcorn plugin loader",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "scripts": {
7
- "test": "echo 'No tests'",
7
+ "test": "jest --runInBand",
8
8
  "tsc": "echo 'No TypeScript build'",
9
9
  "clean": "echo 'No TypeScript build'"
10
10
  },
11
11
  "dependencies": {
12
12
  "env-paths": "^2.2.0",
13
- "npm-registry-fetch": "16.0.0",
14
- "@saltcorn/data": "1.0.0-beta.9"
13
+ "npm-registry-fetch": "17.1.0",
14
+ "@saltcorn/data": "1.0.0-rc.2"
15
15
  },
16
16
  "author": "Christian Hugo",
17
17
  "license": "MIT",
18
+ "jest": {
19
+ "testEnvironment": "node",
20
+ "testPathIgnorePatterns": [
21
+ "/node_modules/",
22
+ "/plugin_packages/",
23
+ "/plugins_folder/"
24
+ ],
25
+ "coveragePathIgnorePatterns": [
26
+ "/node_modules/",
27
+ "/plugin_packages/",
28
+ "/plugins_folder/"
29
+ ],
30
+ "moduleNameMapper": {
31
+ "@saltcorn/data/(.*)": "<rootDir>/../saltcorn-data/dist/$1"
32
+ }
33
+ },
18
34
  "main": "index.js",
19
35
  "repository": "github:saltcorn/saltcorn",
20
36
  "publishConfig": {
@@ -11,6 +11,7 @@ const {
11
11
  const { getState } = require("@saltcorn/data/db/state");
12
12
  const { rm, rename, cp, readFile } = require("fs").promises;
13
13
  const envPaths = require("env-paths");
14
+ const semver = require("semver");
14
15
 
15
16
  const staticDeps = ["@saltcorn/markup", "@saltcorn/data", "jest"];
16
17
  const fixedPlugins = ["@saltcorn/base-plugin", "@saltcorn/sbadmin2"];
@@ -26,10 +27,10 @@ const readPackageJson = async (filePath) => {
26
27
  };
27
28
 
28
29
  const npmInstallNeeded = (oldPckJSON, newPckJSON) => {
29
- const oldDeps = oldPckJSON.dependencies || {};
30
- const oldDevDeps = oldPckJSON.devDependencies || {};
31
- const newDeps = newPckJSON.dependencies || {};
32
- const newDevDeps = newPckJSON.devDependencies || {};
30
+ const oldDeps = oldPckJSON.dependencies || Object.create(null);
31
+ const oldDevDeps = oldPckJSON.devDependencies || Object.create(null);
32
+ const newDeps = newPckJSON.dependencies || Object.create(null);
33
+ const newDevDeps = newPckJSON.devDependencies || Object.create(null);
33
34
  return (
34
35
  JSON.stringify(oldDeps) !== JSON.stringify(newDeps) ||
35
36
  JSON.stringify(oldDevDeps) !== JSON.stringify(newDevDeps)
@@ -37,7 +38,7 @@ const npmInstallNeeded = (oldPckJSON, newPckJSON) => {
37
38
  };
38
39
 
39
40
  class PluginInstaller {
40
- constructor(plugin, opts = {}) {
41
+ constructor(plugin, opts = Object.create(null)) {
41
42
  this.plugin = plugin;
42
43
  this.rootFolder =
43
44
  opts.rootFolder || envPaths("saltcorn", { suffix: "plugins" }).data;
@@ -56,13 +57,33 @@ class PluginInstaller {
56
57
  this.pckJsonPath = join(this.pluginDir, "package.json");
57
58
  this.tempDir = join(this.tempRootFolder, "temp_install", ...tokens);
58
59
  this.tempPckJsonPath = join(this.tempDir, "package.json");
60
+ this.scVersion = opts.scVersion;
61
+ }
62
+
63
+ /**
64
+ * check if the host supports the plugin and return a warning if not
65
+ * @param pckJSON
66
+ * @returns
67
+ */
68
+ checkEngineWarning(pckJSON) {
69
+ const scEngine = pckJSON.engines?.saltcorn;
70
+ if (
71
+ this.scVersion &&
72
+ scEngine &&
73
+ !semver.satisfies(this.scVersion, scEngine)
74
+ ) {
75
+ const warnMsg = `Plugin ${this.plugin.name} requires Saltcorn version ${scEngine} but running ${this.scVersion}`;
76
+ getState().log(4, warnMsg);
77
+ return warnMsg;
78
+ }
79
+ return null;
59
80
  }
60
81
 
61
82
  async install(force) {
62
83
  await this.ensurePluginsRootFolders();
63
84
  if (fixedPlugins.includes(this.plugin.location))
64
85
  return { plugin_module: require(this.plugin.location) };
65
-
86
+ const msgs = [];
66
87
  let pckJSON = await readPackageJson(this.pckJsonPath);
67
88
  const installer = async () => {
68
89
  if (await this.prepPluginsFolder(force, pckJSON)) {
@@ -80,6 +101,8 @@ class PluginInstaller {
80
101
  await removeTarball(this.rootFolder, this.plugin);
81
102
  }
82
103
  pckJSON = await readPackageJson(this.pckJsonPath);
104
+ const msg = this.checkEngineWarning(pckJSON);
105
+ if (msg && !msgs.includes(msg)) msgs.push(msg);
83
106
  };
84
107
  await installer();
85
108
  let module = null;
@@ -108,6 +131,7 @@ class PluginInstaller {
108
131
  location: this.pluginDir,
109
132
  name: this.plugin.name,
110
133
  loadedWithReload,
134
+ msgs,
111
135
  };
112
136
  }
113
137
 
@@ -0,0 +1,104 @@
1
+ const semver = require("semver");
2
+ const { getState } = require("@saltcorn/data/db/state");
3
+
4
+ /*
5
+ internal helper
6
+ */
7
+ const doCheck = (pluginVersion, versionInfos, scVersion) => {
8
+ if (!versionInfos[pluginVersion])
9
+ throw new Error(`Version ${pluginVersion} not found in available versions`);
10
+ const scEngine = versionInfos[pluginVersion].engines?.saltcorn;
11
+ if (!scEngine) return true;
12
+ if (semver.validRange(scEngine) === null) {
13
+ getState().log(4, `invalid engine property: ${scEngine}`);
14
+ return true;
15
+ }
16
+ if (semver.valid(scVersion) === null) {
17
+ getState().log(4, `invalid saltcorn version: ${scVersion}`);
18
+ return true;
19
+ }
20
+ return semver.satisfies(scVersion, scEngine);
21
+ };
22
+
23
+ /**
24
+ * check if 'pluginVersion' is supported or find the latest supported version
25
+ * @param pluginVersion - wanted version
26
+ * @param versionInfos - version infos from the npm registry (resembles the package.json version).
27
+ * Here you'll find the engines.saltcorn property.
28
+ * @param scVersion - optional saltcorn version (if not set it will be taken from the state)
29
+ * @returns
30
+ */
31
+ const supportedVersion = (
32
+ pluginVersion = "latest",
33
+ versionInfos,
34
+ scVersion
35
+ ) => {
36
+ const resolved =
37
+ pluginVersion === "latest" ? resolveLatest(versionInfos) : pluginVersion;
38
+ const safeScVersion = scVersion || getState().scVersion;
39
+ if (doCheck(resolved, versionInfos, safeScVersion)) return resolved;
40
+ else {
41
+ const keys = Object.keys(versionInfos);
42
+ keys.sort((a, b) => semver.rcompare(b, a));
43
+ // iterate in reverse order to get the latest version
44
+ for (let i = keys.length - 1; i >= 0; i--) {
45
+ const version = keys[i];
46
+ if (doCheck(version, versionInfos, safeScVersion)) return version;
47
+ }
48
+ return null;
49
+ }
50
+ };
51
+
52
+ /**
53
+ * check if 'pluginVersion' is supported
54
+ * @param pluginVersion - wanted version
55
+ * @param scEngine - version infos from the npm registry (resembles the package.json version)
56
+ * Here you'll find the engines.saltcorn property.
57
+ * If versionInfos is a string, it will be treated as the engines.saltcorn property.
58
+ * @param scVersion - optional saltcorn version (if not set it will be taken from the state)
59
+ */
60
+ const isVersionSupported = (pluginVersion, scEngine, scVersion) => {
61
+ const safeInfos =
62
+ typeof scEngine === "string"
63
+ ? { [pluginVersion]: { engines: scEngine } }
64
+ : scEngine;
65
+ const safeScVersion = scVersion || getState().scVersion;
66
+ return doCheck(pluginVersion, safeInfos, safeScVersion);
67
+ };
68
+
69
+ /**
70
+ * check if 'scVersion' fullfilles 'scEngine'
71
+ * @param scEngine fixed version or range of saltcorn versions (e.g. ">=1.0.0")
72
+ * @param scVersion optional saltcorn version (if not set it will be taken from the state)
73
+ * @returns true if the saltcorn version fullfilles scEngine
74
+ */
75
+ const isEngineSatisfied = (scEngine, scVersion) => {
76
+ if (!scEngine) return true;
77
+ if (semver.validRange(scEngine) === null) {
78
+ getState().log(4, `invalid engine property: ${scEngine}`);
79
+ return true;
80
+ }
81
+ const safeScVersion = scVersion || getState().scVersion;
82
+ if (semver.valid(safeScVersion) === null) {
83
+ getState().log(4, `invalid saltcorn version: ${scVersion}`);
84
+ return true;
85
+ }
86
+ return semver.satisfies(safeScVersion, scEngine);
87
+ };
88
+
89
+ /**
90
+ * change latest to the actual version
91
+ * @param versionInfos - version infos from the npm registry
92
+ */
93
+ const resolveLatest = (versionInfos) => {
94
+ const keys = Object.keys(versionInfos);
95
+ keys.sort((a, b) => semver.rcompare(a, b));
96
+ return keys[0];
97
+ };
98
+
99
+ module.exports = {
100
+ isVersionSupported,
101
+ isEngineSatisfied,
102
+ supportedVersion,
103
+ resolveLatest,
104
+ };
@@ -0,0 +1,263 @@
1
+ const npmFetch = require("npm-registry-fetch");
2
+ const semver = require("semver");
3
+
4
+ const {
5
+ supportedVersion,
6
+ isVersionSupported,
7
+ } = require("../stable_versioning");
8
+
9
+ jest.setTimeout(30000);
10
+
11
+ const getSortedKeys = (pkgInfo) => {
12
+ const keys = [...Object.keys(pkgInfo.versions)];
13
+ keys.sort((a, b) => semver.rcompare(a, b));
14
+ return keys;
15
+ };
16
+
17
+ describe("Stable versioning", () => {
18
+ it("stays compatible to version 1.0.0", async () => {
19
+ const wantedVersion = "latest";
20
+ const scVersion = "1.0.0";
21
+ const plugin = {
22
+ location: "@saltcorn/html",
23
+ version: wantedVersion,
24
+ };
25
+ const pkgInfo = await npmFetch.json(
26
+ `https://registry.npmjs.org/${plugin.location}`
27
+ );
28
+ const sortedKeys = getSortedKeys(pkgInfo);
29
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: ">=1.0.1" };
30
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: ">=1.0.1-beta.1" };
31
+ pkgInfo.versions[sortedKeys[2]].engines = { saltcorn: "<=1.0.0" };
32
+ pkgInfo.versions[sortedKeys[3]].engines = { saltcorn: ">=1.0.0-beta.1" };
33
+ const result = supportedVersion(
34
+ plugin.version,
35
+ pkgInfo.versions,
36
+ scVersion
37
+ );
38
+ expect(result).toBe(sortedKeys[2]);
39
+ expect(isVersionSupported(sortedKeys[0], pkgInfo.versions, scVersion)).toBe(
40
+ false
41
+ );
42
+ expect(isVersionSupported(sortedKeys[1], pkgInfo.versions, scVersion)).toBe(
43
+ false
44
+ );
45
+ expect(isVersionSupported(sortedKeys[2], pkgInfo.versions, scVersion)).toBe(
46
+ true
47
+ );
48
+ expect(isVersionSupported(sortedKeys[3], pkgInfo.versions, scVersion)).toBe(
49
+ true
50
+ );
51
+ });
52
+
53
+ it("picks one fixed version instead of a range", async () => {
54
+ const wantedVersion = "latest";
55
+ const scVersion = "1.0.0";
56
+ const plugin = {
57
+ location: "@saltcorn/html",
58
+ version: wantedVersion,
59
+ };
60
+ const pkgInfo = await npmFetch.json(
61
+ `https://registry.npmjs.org/${plugin.location}`
62
+ );
63
+ const sortedKeys = getSortedKeys(pkgInfo);
64
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: ">=1.0.1" };
65
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: ">=1.0.1-beta.1" };
66
+ pkgInfo.versions[sortedKeys[2]].engines = { saltcorn: "1.0.0" };
67
+ pkgInfo.versions[sortedKeys[3]].engines = { saltcorn: ">=1.0.0-beta.1" };
68
+ const result = supportedVersion(
69
+ plugin.version,
70
+ pkgInfo.versions,
71
+ scVersion
72
+ );
73
+ expect(result).toBe(sortedKeys[2]);
74
+ expect(isVersionSupported(sortedKeys[0], pkgInfo.versions, scVersion)).toBe(
75
+ false
76
+ );
77
+ expect(isVersionSupported(sortedKeys[1], pkgInfo.versions, scVersion)).toBe(
78
+ false
79
+ );
80
+ expect(isVersionSupported(sortedKeys[2], pkgInfo.versions, scVersion)).toBe(
81
+ true
82
+ );
83
+ expect(isVersionSupported(sortedKeys[3], pkgInfo.versions, scVersion)).toBe(
84
+ true
85
+ );
86
+ });
87
+
88
+ it("warns and takes invalid engine properties", async () => {
89
+ const wantedVersion = "latest";
90
+ const scVersion = "1.0.0";
91
+ const plugin = {
92
+ location: "@saltcorn/html",
93
+ version: wantedVersion,
94
+ };
95
+ const pkgInfo = await npmFetch.json(
96
+ `https://registry.npmjs.org/${plugin.location}`
97
+ );
98
+ const sortedKeys = getSortedKeys(pkgInfo);
99
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: ">=1.0.1" };
100
+ // invalid: >=1.0.1.beta.1 should be >=1.0.1-beta.1
101
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: ">=1.0.1.beta.1" };
102
+ pkgInfo.versions[sortedKeys[2]].engines = { saltcorn: "<=1.0.0" };
103
+ pkgInfo.versions[sortedKeys[3]].engines = { saltcorn: ">=1.0.0-beta.1" };
104
+ const result = supportedVersion(
105
+ plugin.version,
106
+ pkgInfo.versions,
107
+ scVersion
108
+ );
109
+ expect(result).toBe(sortedKeys[1]);
110
+ });
111
+
112
+ it("downgrades latest with greater equal", async () => {
113
+ const wantedVersion = "latest";
114
+ const scVersion = "1.0.0-beta.6";
115
+ const plugin = {
116
+ location: "@saltcorn/html",
117
+ version: wantedVersion,
118
+ };
119
+ const pkgInfo = await npmFetch.json(
120
+ `https://registry.npmjs.org/${plugin.location}`
121
+ );
122
+ const sortedKeys = getSortedKeys(pkgInfo);
123
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: ">=1.0.0-beta.7" };
124
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: ">=1.0.0-beta.6" };
125
+ const result = supportedVersion(
126
+ plugin.version,
127
+ pkgInfo.versions,
128
+ scVersion
129
+ );
130
+ expect(result).toBe(sortedKeys[1]);
131
+ expect(isVersionSupported(result, pkgInfo.versions, scVersion)).toBe(true);
132
+ expect(isVersionSupported(sortedKeys[0], pkgInfo.versions, scVersion)).toBe(
133
+ false
134
+ );
135
+ });
136
+
137
+ it("takes latest with smaller equal", async () => {
138
+ const wantedVersion = "latest";
139
+ const scVersion = "1.0.0-beta.6";
140
+ const plugin = {
141
+ location: "@saltcorn/html",
142
+ version: wantedVersion,
143
+ };
144
+ const pkgInfo = await npmFetch.json(
145
+ `https://registry.npmjs.org/${plugin.location}`
146
+ );
147
+ const sortedKeys = getSortedKeys(pkgInfo);
148
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: "<=1.0.0-beta.7" };
149
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: "<=1.0.0-beta.6" };
150
+ const result = supportedVersion(
151
+ plugin.version,
152
+ pkgInfo.versions,
153
+ scVersion
154
+ );
155
+ expect(result).toBe(sortedKeys[0]);
156
+ expect(isVersionSupported(result, pkgInfo.versions, scVersion)).toBe(true);
157
+ expect(isVersionSupported(sortedKeys[0], pkgInfo.versions, scVersion)).toBe(
158
+ true
159
+ );
160
+ });
161
+
162
+ it("resolves latest to the current version", async () => {
163
+ const wantedVersion = "latest";
164
+ const scVersion = "1.0.0-beta.6";
165
+ const plugin = {
166
+ location: "@saltcorn/html",
167
+ version: wantedVersion,
168
+ };
169
+ const pkgInfo = await npmFetch.json(
170
+ `https://registry.npmjs.org/${plugin.location}`
171
+ );
172
+ const sortedKeys = getSortedKeys(pkgInfo);
173
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: ">=1.0.0-beta.6" };
174
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: ">=1.0.0-beta.6" };
175
+ const result = supportedVersion(
176
+ plugin.version,
177
+ pkgInfo.versions,
178
+ scVersion
179
+ );
180
+ expect(result).toBe(sortedKeys[0]);
181
+ expect(isVersionSupported(result, pkgInfo.versions, scVersion)).toBe(true);
182
+ expect(isVersionSupported(sortedKeys[1], pkgInfo.versions, scVersion)).toBe(
183
+ true
184
+ );
185
+ });
186
+
187
+ it("it takes the latest version, all engine properties are missing", async () => {
188
+ const wantedVersion = "latest";
189
+ const scVersion = "1.0.0-beta.6";
190
+ const plugin = {
191
+ location: "@saltcorn/html",
192
+ version: wantedVersion,
193
+ };
194
+ const pkgInfo = await npmFetch.json(
195
+ `https://registry.npmjs.org/${plugin.location}`
196
+ );
197
+ const sortedKeys = getSortedKeys(pkgInfo);
198
+ for (const key of sortedKeys) pkgInfo.versions[key].engines = undefined;
199
+ const result = supportedVersion(
200
+ plugin.version,
201
+ pkgInfo.versions,
202
+ scVersion
203
+ );
204
+ expect(result).toBe(sortedKeys[0]);
205
+ expect(isVersionSupported(result, pkgInfo.versions, scVersion)).toBe(true);
206
+ expect(isVersionSupported(sortedKeys[1], pkgInfo.versions, scVersion)).toBe(
207
+ true
208
+ );
209
+ });
210
+
211
+ it("takes a version without the engines property", async () => {
212
+ const wantedVersion = "latest";
213
+ const scVersion = "1.0.0-beta.6";
214
+ const plugin = {
215
+ location: "@saltcorn/html",
216
+ version: wantedVersion,
217
+ };
218
+ const pkgInfo = await npmFetch.json(
219
+ `https://registry.npmjs.org/${plugin.location}`
220
+ );
221
+ const sortedKeys = getSortedKeys(pkgInfo);
222
+ pkgInfo.versions[sortedKeys[0]].engines = { saltcorn: ">=1.0.0-beta.7" };
223
+ pkgInfo.versions[sortedKeys[1]].engines = { saltcorn: ">=1.0.0-beta.7" };
224
+ pkgInfo.versions[sortedKeys[2]].engines = undefined;
225
+ const result = supportedVersion(
226
+ plugin.version,
227
+ pkgInfo.versions,
228
+ scVersion
229
+ );
230
+ expect(result).toBe(sortedKeys[2]);
231
+ expect(isVersionSupported(result, pkgInfo.versions, scVersion)).toBe(true);
232
+ expect(isVersionSupported(sortedKeys[0], pkgInfo.versions, scVersion)).toBe(
233
+ false
234
+ );
235
+ expect(isVersionSupported(sortedKeys[1], pkgInfo.versions, scVersion)).toBe(
236
+ false
237
+ );
238
+ expect(isVersionSupported(sortedKeys[2], pkgInfo.versions, scVersion)).toBe(
239
+ true
240
+ );
241
+ });
242
+
243
+ it("finds no supported version", async () => {
244
+ const wantedVersion = "latest";
245
+ const scVersion = "1.0.0-beta.6";
246
+ const plugin = {
247
+ location: "@saltcorn/html",
248
+ version: wantedVersion,
249
+ };
250
+ const pkgInfo = await npmFetch.json(
251
+ `https://registry.npmjs.org/${plugin.location}`
252
+ );
253
+ const sortedKeys = getSortedKeys(pkgInfo);
254
+ for (const key of sortedKeys)
255
+ pkgInfo.versions[key].engines = { saltcorn: ">=1.0.0-beta.7" };
256
+ const result = supportedVersion(
257
+ plugin.version,
258
+ pkgInfo.versions,
259
+ scVersion
260
+ );
261
+ expect(result).toBeNull();
262
+ });
263
+ });