@saltcorn/plugins-loader 0.9.5-beta.3
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 +144 -0
- package/index.js +0 -0
- package/package.json +22 -0
- package/plugin_installer.js +213 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { mkdir, rm, pathExists } = require("fs-extra");
|
|
3
|
+
const { tmpName } = require("tmp-promise");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const { extract } = require("tar");
|
|
6
|
+
const { join } = require("path");
|
|
7
|
+
const { createWriteStream, unlink } = require("fs");
|
|
8
|
+
const { get } = require("https");
|
|
9
|
+
const npmFetch = require("npm-registry-fetch");
|
|
10
|
+
|
|
11
|
+
const rootFolder = process.cwd();
|
|
12
|
+
|
|
13
|
+
const downloadFromGithub = async (plugin, pluginDir) => {
|
|
14
|
+
const tarballUrl = `https://api.github.com/repos/${plugin.location}/tarball`;
|
|
15
|
+
const fileName = plugin.name.split("/").pop();
|
|
16
|
+
const filePath = await loadTarball(tarballUrl, fileName);
|
|
17
|
+
await mkdir(pluginDir, { recursive: true });
|
|
18
|
+
await extractTarball(filePath, pluginDir);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const downloadFromNpm = async (plugin, pluginDir, pckJson) => {
|
|
22
|
+
const pkgInfo = await npmFetch.json(
|
|
23
|
+
`https://registry.npmjs.org/${plugin.location}`
|
|
24
|
+
);
|
|
25
|
+
const keys = Object.keys(pkgInfo.versions);
|
|
26
|
+
const latest = keys[keys.length - 1];
|
|
27
|
+
const vToInstall =
|
|
28
|
+
plugin.version && plugin.version !== "latest" ? plugin.version : latest;
|
|
29
|
+
|
|
30
|
+
if (pckJson && pckJson.version === vToInstall) return null;
|
|
31
|
+
else {
|
|
32
|
+
const tarballUrl = pkgInfo.versions[vToInstall].dist.tarball;
|
|
33
|
+
const fileName = plugin.name.split("/").pop();
|
|
34
|
+
const filePath = await loadTarball(tarballUrl, fileName);
|
|
35
|
+
await mkdir(pluginDir, { recursive: true });
|
|
36
|
+
await extractTarball(filePath, pluginDir);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const loadTarball = (url, name) => {
|
|
41
|
+
const options = {
|
|
42
|
+
headers: {
|
|
43
|
+
"User-Agent": "request",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const writeTarball = async (res) => {
|
|
47
|
+
const filePath = join(rootFolder, "plugins_folder", `${name}.tar.gz`);
|
|
48
|
+
const stream = createWriteStream(filePath);
|
|
49
|
+
res.pipe(stream);
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
stream.on("finish", () => {
|
|
52
|
+
stream.close();
|
|
53
|
+
resolve(filePath);
|
|
54
|
+
});
|
|
55
|
+
stream.on("error", (err) => {
|
|
56
|
+
stream.close();
|
|
57
|
+
reject(err);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
get(url, options, async (res) => {
|
|
64
|
+
if (res.statusCode === 302) {
|
|
65
|
+
get(res.headers.location, options, async (redirect) => {
|
|
66
|
+
if (redirect.statusCode === 200) {
|
|
67
|
+
const filePath = await writeTarball(redirect);
|
|
68
|
+
resolve(filePath);
|
|
69
|
+
} else
|
|
70
|
+
reject(
|
|
71
|
+
new Error(
|
|
72
|
+
`Error downloading tarball: http code ${redirect.statusCode}`
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
} else if (res.statusCode !== 200)
|
|
77
|
+
reject(
|
|
78
|
+
new Error(`Error downloading tarball: http code ${res.statusCode}`)
|
|
79
|
+
);
|
|
80
|
+
else {
|
|
81
|
+
const filePath = await writeTarball(res);
|
|
82
|
+
resolve(filePath);
|
|
83
|
+
}
|
|
84
|
+
}).on("error", (err) => {
|
|
85
|
+
reject(err);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Git pull or clone
|
|
92
|
+
* @param plugin
|
|
93
|
+
*/
|
|
94
|
+
const gitPullOrClone = async (plugin, pluginDir) => {
|
|
95
|
+
await fs.promises.mkdir("git_plugins", { recursive: true });
|
|
96
|
+
let keyfnm,
|
|
97
|
+
setKey = `-c core.sshCommand="ssh -oBatchMode=yes -o 'StrictHostKeyChecking no'" `;
|
|
98
|
+
if (plugin.deploy_private_key) {
|
|
99
|
+
keyfnm = await tmpName();
|
|
100
|
+
await fs.promises.writeFile(
|
|
101
|
+
keyfnm,
|
|
102
|
+
plugin.deploy_private_key.replace(/[\r]+/g, "") + "\n",
|
|
103
|
+
{
|
|
104
|
+
mode: 0o600,
|
|
105
|
+
encoding: "ascii",
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
setKey = `-c core.sshCommand="ssh -oBatchMode=yes -o 'StrictHostKeyChecking no' -i ${keyfnm}" `;
|
|
109
|
+
}
|
|
110
|
+
if (fs.existsSync(pluginDir)) {
|
|
111
|
+
execSync(`git ${setKey} -C ${pluginDir} pull`);
|
|
112
|
+
} else {
|
|
113
|
+
execSync(`git ${setKey} clone ${plugin.location} ${pluginDir}`);
|
|
114
|
+
}
|
|
115
|
+
if (plugin.deploy_private_key && keyfnm) await fs.promises.unlink(keyfnm);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const extractTarball = async (tarFile, destination) => {
|
|
119
|
+
await extract({
|
|
120
|
+
file: tarFile,
|
|
121
|
+
cwd: destination,
|
|
122
|
+
strip: 1,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const tarballExists = async (plugin) => {
|
|
127
|
+
const fileName = `${plugin.name.split("/").pop()}.tar.gz`;
|
|
128
|
+
return await pathExists(join(rootFolder, "plugins_folder", fileName));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const removeTarball = async (plugin) => {
|
|
132
|
+
const fileName = `${plugin.name.split("/").pop()}.tar.gz`;
|
|
133
|
+
await rm(join(rootFolder, "plugins_folder", fileName));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
downloadFromGithub,
|
|
138
|
+
downloadFromNpm,
|
|
139
|
+
gitPullOrClone,
|
|
140
|
+
loadTarball,
|
|
141
|
+
extractTarball,
|
|
142
|
+
tarballExists,
|
|
143
|
+
removeTarball,
|
|
144
|
+
};
|
package/index.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saltcorn/plugins-loader",
|
|
3
|
+
"version": "0.9.5-beta.3",
|
|
4
|
+
"description": "Saltcorn plugin loader",
|
|
5
|
+
"homepage": "https://saltcorn.com",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo 'No tests'",
|
|
8
|
+
"tsc": "echo 'No TypeScript build'",
|
|
9
|
+
"clean": "echo 'No TypeScript build'"
|
|
10
|
+
},
|
|
11
|
+
"author": "Christian Hugo",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"main": "index.js",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"semver": "^7.6.0",
|
|
16
|
+
"resolve-global": "^1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": "github:saltcorn/saltcorn",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const { join, normalize, dirname } = require("path");
|
|
2
|
+
const { writeFile, mkdir, rm, pathExists, copy, symlink } = require("fs-extra");
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const {
|
|
5
|
+
downloadFromNpm,
|
|
6
|
+
downloadFromGithub,
|
|
7
|
+
gitPullOrClone,
|
|
8
|
+
tarballExists,
|
|
9
|
+
removeTarball,
|
|
10
|
+
} = require("./download_utils");
|
|
11
|
+
const semver = require("semver");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const resolveGlobal = require("resolve-global");
|
|
14
|
+
|
|
15
|
+
const rootFolder = process.cwd();
|
|
16
|
+
const staticDeps = ["@saltcorn/markup", "@saltcorn/data", "jest"];
|
|
17
|
+
const fixedPlugins = ["@saltcorn/base-plugin", "@saltcorn/sbadmin2"];
|
|
18
|
+
|
|
19
|
+
const isGitCheckout = async () => {
|
|
20
|
+
const gitPath = join(__dirname, "..", "..", "Dockerfile.release");
|
|
21
|
+
return await pathExists(gitPath);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class PluginInstaller {
|
|
25
|
+
constructor(plugin) {
|
|
26
|
+
this.plugin = plugin;
|
|
27
|
+
this.pckJson = null;
|
|
28
|
+
this.tarFile = null;
|
|
29
|
+
const tokens =
|
|
30
|
+
plugin.source === "npm"
|
|
31
|
+
? plugin.location.split("/")
|
|
32
|
+
: plugin.name.split("/");
|
|
33
|
+
this.pluginDir = join(
|
|
34
|
+
rootFolder,
|
|
35
|
+
plugin.source === "git" ? "git_plugins" : "plugins_folder",
|
|
36
|
+
...tokens
|
|
37
|
+
);
|
|
38
|
+
this.pckJsonPath = join(this.pluginDir, "package.json");
|
|
39
|
+
this.name = tokens[tokens.length - 1];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async install(force) {
|
|
43
|
+
await this.ensurePluginsRootFolders();
|
|
44
|
+
if (fixedPlugins.includes(this.plugin.location))
|
|
45
|
+
return { plugin_module: require(this.plugin.location) };
|
|
46
|
+
this.pckJson = await this.readPackageJson();
|
|
47
|
+
if (await this.prepPluginsFolder(force)) {
|
|
48
|
+
await this.removeDependencies();
|
|
49
|
+
this.pckJson = await this.readPackageJson();
|
|
50
|
+
await this.npmInstall();
|
|
51
|
+
if (await tarballExists(this.plugin)) await removeTarball(this.plugin);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
version: this.pckJson.version,
|
|
55
|
+
plugin_module: await this.loadMainFile(),
|
|
56
|
+
location: this.pluginDir,
|
|
57
|
+
name: this.name,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async prepPluginsFolder(force) {
|
|
62
|
+
let wasLoaded = false;
|
|
63
|
+
switch (this.plugin.source) {
|
|
64
|
+
case "npm":
|
|
65
|
+
if (
|
|
66
|
+
(force && !(await this.versionIsInstalled())) ||
|
|
67
|
+
!(await pathExists(this.pluginDir))
|
|
68
|
+
) {
|
|
69
|
+
this.tarFile = await downloadFromNpm(
|
|
70
|
+
this.plugin,
|
|
71
|
+
this.pluginDir,
|
|
72
|
+
this.pckJson
|
|
73
|
+
);
|
|
74
|
+
wasLoaded = true;
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
case "github":
|
|
78
|
+
if (force || !(await pathExists(this.pluginDir))) {
|
|
79
|
+
this.tarFile = await downloadFromGithub(this.plugin, this.pluginDir);
|
|
80
|
+
wasLoaded = true;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case "local":
|
|
84
|
+
if (force || !(await pathExists(this.pluginDir))) {
|
|
85
|
+
await copy(this.plugin.location, this.pluginDir);
|
|
86
|
+
wasLoaded = true;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "git":
|
|
90
|
+
if (force || !(await pathExists(this.pluginDir))) {
|
|
91
|
+
await gitPullOrClone(this.plugin, this.pluginDir);
|
|
92
|
+
this.pckJsonPath = join(this.pluginDir, "package.json");
|
|
93
|
+
wasLoaded = true;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
return wasLoaded;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async ensurePluginsRootFolders() {
|
|
101
|
+
const ensureFn = async (folder) => {
|
|
102
|
+
const pluginsFolder = join(rootFolder, folder);
|
|
103
|
+
if (!(await pathExists(pluginsFolder))) await mkdir(pluginsFolder);
|
|
104
|
+
const symLinkDst = join(pluginsFolder, "node_modules");
|
|
105
|
+
const symLinkSrc = (await isGitCheckout())
|
|
106
|
+
? join(__dirname, "..", "..", "node_modules")
|
|
107
|
+
: join(dirname(resolveGlobal("@saltcorn/cli")), "..", "node_modules");
|
|
108
|
+
if (!(await pathExists(symLinkDst)))
|
|
109
|
+
await symlink(symLinkSrc, symLinkDst, "dir");
|
|
110
|
+
};
|
|
111
|
+
for (const folder of ["plugins_folder", "git_plugins"])
|
|
112
|
+
await ensureFn(folder);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
isFixedVersion() {
|
|
116
|
+
return !!this.plugin.version && this.plugin.version !== "latest";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async versionIsInstalled() {
|
|
120
|
+
if (!this.pckJson || !this.isFixedVersion()) return false;
|
|
121
|
+
else {
|
|
122
|
+
const vInstalled = this.pckJson.version;
|
|
123
|
+
if (vInstalled === this.plugin.version) return true;
|
|
124
|
+
else return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async remove() {
|
|
129
|
+
if (await pathExists(this.pluginDir))
|
|
130
|
+
await rm(this.pluginDir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async loadMainFile() {
|
|
134
|
+
const isWindows = process.platform === "win32";
|
|
135
|
+
if (process.env.NODE_ENV === "test") {
|
|
136
|
+
// in jest, downgrad to require
|
|
137
|
+
return require(normalize(join(this.pluginDir, this.pckJson.main)));
|
|
138
|
+
} else {
|
|
139
|
+
const res = await import(
|
|
140
|
+
`${isWindows ? `file://` : ""}${normalize(
|
|
141
|
+
join(this.pluginDir, this.pckJson.main)
|
|
142
|
+
)}`
|
|
143
|
+
);
|
|
144
|
+
return res.default;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async removeDependencies() {
|
|
149
|
+
const pckJson = await this.readPackageJson();
|
|
150
|
+
const oldDepsLength = Object.keys(pckJson.dependencies || {}).length;
|
|
151
|
+
const oldDevDepsLength = Object.keys(pckJson.devDependencies || {}).length;
|
|
152
|
+
|
|
153
|
+
const satisfiedRemover = (deps) => {
|
|
154
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
155
|
+
try {
|
|
156
|
+
const vInstalled = require(`${name}/package.json`).version;
|
|
157
|
+
if (semver.satisfies(vInstalled, version)) {
|
|
158
|
+
delete deps[name];
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {} // continue, npm installs it
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const staticsRemover = (deps) => {
|
|
164
|
+
for (const staticDep of staticDeps) {
|
|
165
|
+
if (deps[staticDep]) delete deps[staticDep];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
if (pckJson.dependencies) {
|
|
169
|
+
satisfiedRemover(pckJson.dependencies);
|
|
170
|
+
staticsRemover(pckJson.dependencies);
|
|
171
|
+
}
|
|
172
|
+
if (pckJson.devDependencies) {
|
|
173
|
+
satisfiedRemover(pckJson.devDependencies);
|
|
174
|
+
staticsRemover(pckJson.devDependencies);
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
Object.keys(pckJson.dependencies || {}).length !== oldDepsLength ||
|
|
178
|
+
Object.keys(pckJson.devDependencies || {}).length !== oldDevDepsLength
|
|
179
|
+
)
|
|
180
|
+
await writeFile(this.pckJsonPath, JSON.stringify(pckJson, null, 2));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async npmInstall() {
|
|
184
|
+
if (
|
|
185
|
+
Object.keys(this.pckJson.dependencies || {}).length > 0 ||
|
|
186
|
+
Object.keys(this.pckJson.devDependencies || {}).length > 0
|
|
187
|
+
) {
|
|
188
|
+
const child = spawn("npm", ["install"], {
|
|
189
|
+
cwd: this.pluginDir,
|
|
190
|
+
});
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
child.on("exit", (exitCode, signal) => {
|
|
193
|
+
resolve({ success: exitCode === 0 });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
child.on("error", (msg) => {
|
|
197
|
+
reject(msg);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async readPackageJson() {
|
|
204
|
+
if (await pathExists(this.pckJsonPath)) {
|
|
205
|
+
const str = await fs.promises.readFile(this.pckJsonPath);
|
|
206
|
+
return JSON.parse(str);
|
|
207
|
+
} else {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = PluginInstaller;
|