@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.
@@ -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;