@saltcorn/plugins-loader 1.0.0-beta.9 → 1.0.0-rc.10

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.10",
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.10"
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"];
@@ -25,19 +26,34 @@ const readPackageJson = async (filePath) => {
25
26
  else return null;
26
27
  };
27
28
 
28
- const npmInstallNeeded = (oldPckJSON, newPckJSON) => {
29
- const oldDeps = oldPckJSON.dependencies || {};
30
- const oldDevDeps = oldPckJSON.devDependencies || {};
31
- const newDeps = newPckJSON.dependencies || {};
32
- const newDevDeps = newPckJSON.devDependencies || {};
33
- return (
34
- JSON.stringify(oldDeps) !== JSON.stringify(newDeps) ||
35
- JSON.stringify(oldDevDeps) !== JSON.stringify(newDevDeps)
36
- );
29
+ /**
30
+ * install when the new package.json has different dependencies
31
+ * or when the source is local and there are any dependencies
32
+ * @param source
33
+ * @param oldPckJSON
34
+ * @param newPckJSON
35
+ * @returns
36
+ */
37
+ const npmInstallNeeded = (source, oldPckJSON, newPckJSON) => {
38
+ if (source === "local") {
39
+ return (
40
+ Object.keys(newPckJSON.dependencies || {}).length > 0 ||
41
+ Object.keys(newPckJSON.devDependencies || {}).length > 0
42
+ );
43
+ } else {
44
+ const oldDeps = oldPckJSON.dependencies || Object.create(null);
45
+ const oldDevDeps = oldPckJSON.devDependencies || Object.create(null);
46
+ const newDeps = newPckJSON.dependencies || Object.create(null);
47
+ const newDevDeps = newPckJSON.devDependencies || Object.create(null);
48
+ return (
49
+ JSON.stringify(oldDeps) !== JSON.stringify(newDeps) ||
50
+ JSON.stringify(oldDevDeps) !== JSON.stringify(newDevDeps)
51
+ );
52
+ }
37
53
  };
38
54
 
39
55
  class PluginInstaller {
40
- constructor(plugin, opts = {}) {
56
+ constructor(plugin, opts = Object.create(null)) {
41
57
  this.plugin = plugin;
42
58
  this.rootFolder =
43
59
  opts.rootFolder || envPaths("saltcorn", { suffix: "plugins" }).data;
@@ -56,13 +72,34 @@ class PluginInstaller {
56
72
  this.pckJsonPath = join(this.pluginDir, "package.json");
57
73
  this.tempDir = join(this.tempRootFolder, "temp_install", ...tokens);
58
74
  this.tempPckJsonPath = join(this.tempDir, "package.json");
75
+ this.scVersion = opts.scVersion;
76
+ this.envVars = opts.envVars || {};
77
+ }
78
+
79
+ /**
80
+ * check if the host supports the plugin and return a warning if not
81
+ * @param pckJSON
82
+ * @returns
83
+ */
84
+ checkEngineWarning(pckJSON) {
85
+ const scEngine = pckJSON.engines?.saltcorn;
86
+ if (
87
+ this.scVersion &&
88
+ scEngine &&
89
+ !semver.satisfies(this.scVersion, scEngine)
90
+ ) {
91
+ const warnMsg = `Plugin ${this.plugin.name} requires Saltcorn version ${scEngine} but running ${this.scVersion}`;
92
+ getState().log(4, warnMsg);
93
+ return warnMsg;
94
+ }
95
+ return null;
59
96
  }
60
97
 
61
98
  async install(force) {
62
99
  await this.ensurePluginsRootFolders();
63
100
  if (fixedPlugins.includes(this.plugin.location))
64
101
  return { plugin_module: require(this.plugin.location) };
65
-
102
+ const msgs = [];
66
103
  let pckJSON = await readPackageJson(this.pckJsonPath);
67
104
  const installer = async () => {
68
105
  if (await this.prepPluginsFolder(force, pckJSON)) {
@@ -72,7 +109,11 @@ class PluginInstaller {
72
109
  );
73
110
  if (
74
111
  !pckJSON ||
75
- npmInstallNeeded(await this.removeDependencies(pckJSON), tmpPckJSON)
112
+ npmInstallNeeded(
113
+ this.plugin.source,
114
+ await this.removeDependencies(pckJSON),
115
+ tmpPckJSON
116
+ )
76
117
  )
77
118
  await this.npmInstall(tmpPckJSON);
78
119
  await this.movePlugin();
@@ -80,6 +121,8 @@ class PluginInstaller {
80
121
  await removeTarball(this.rootFolder, this.plugin);
81
122
  }
82
123
  pckJSON = await readPackageJson(this.pckJsonPath);
124
+ const msg = this.checkEngineWarning(pckJSON);
125
+ if (msg && !msgs.includes(msg)) msgs.push(msg);
83
126
  };
84
127
  await installer();
85
128
  let module = null;
@@ -108,6 +151,7 @@ class PluginInstaller {
108
151
  location: this.pluginDir,
109
152
  name: this.plugin.name,
110
153
  loadedWithReload,
154
+ msgs,
111
155
  };
112
156
  }
113
157
 
@@ -142,6 +186,9 @@ class PluginInstaller {
142
186
  case "local":
143
187
  if (force || !folderExists) {
144
188
  await copy(this.plugin.location, this.tempDir);
189
+ // if tempdir has a node_modules folder, remove it
190
+ if (await pathExists(join(this.tempDir, "node_modules")))
191
+ await rm(join(this.tempDir, "node_modules"), { recursive: true });
145
192
  wasLoaded = true;
146
193
  }
147
194
  break;
@@ -233,6 +280,7 @@ class PluginInstaller {
233
280
  ) {
234
281
  const child = spawn("npm", ["install"], {
235
282
  cwd: this.tempDir,
283
+ env: { ...process.env, ...this.envVars },
236
284
  ...(isWindows ? { shell: true } : {}),
237
285
  });
238
286
  return new Promise((resolve, reject) => {
@@ -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
+ });