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