@flourish/sdk 3.17.3 → 3.18.0

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/lib/sdk.js~ ADDED
@@ -0,0 +1,540 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs"),
4
+ path = require("path"),
5
+
6
+ cross_spawn = require("cross-spawn"),
7
+ mod_request = require("request"),
8
+ shell_quote = require("shell-quote"),
9
+ yaml = require("js-yaml"),
10
+ nodeResolve = require("resolve"),
11
+
12
+ semver = require("@flourish/semver"),
13
+
14
+ log = require("./log"),
15
+ validateConfig = require("./validate_config"),
16
+ { extendItem } = require("./common") ;
17
+
18
+ const sdk_tokens_file = path.join(process.env.HOME || process.env.USERPROFILE, ".flourish_sdk");
19
+
20
+ const YAML_DUMP_OPTS = { flowLevel: 4 };
21
+
22
+ const package_json_filename = path.join(__dirname, "..", "package.json");
23
+ var sdk_version = null;
24
+ function getSDKVersion() {
25
+ if (sdk_version) return Promise.resolve(sdk_version);
26
+ return new Promise(function(resolve, reject) {
27
+ fs.readFile(package_json_filename, "utf8", function(error, package_json) {
28
+ if (error) return reject(error);
29
+ const package_object = JSON.parse(package_json);
30
+ resolve(sdk_version = package_object.version);
31
+ });
32
+ });
33
+ }
34
+
35
+ function getSDKMajorVersion() {
36
+ return getSDKVersion()
37
+ .then((sdk_version) => {
38
+ const version_tuple = sdk_version.split(".").map((x) => parseInt(x));
39
+ return version_tuple[0];
40
+ });
41
+ }
42
+
43
+ function getSdkToken(server_opts) {
44
+ return new Promise(function(resolve, reject) {
45
+ fs.chmod(sdk_tokens_file, 0o600, function(error) {
46
+ if (error) return reject(error);
47
+
48
+ fs.readFile(sdk_tokens_file, "utf8", function(error, body) {
49
+ if (error) return reject(error);
50
+ resolve(JSON.parse(body)[server_opts.host]);
51
+ });
52
+ });
53
+ });
54
+ }
55
+
56
+ function setSdkToken(server_opts, sdk_token) {
57
+ return new Promise(function(resolve, reject) {
58
+ fs.readFile(sdk_tokens_file, function(error, body) {
59
+ let sdk_tokens;
60
+ if (error && error.code === "ENOENT") {
61
+ sdk_tokens = {};
62
+ }
63
+ else {
64
+ if (error) log.die(`Failed to read ${sdk_tokens_file}`, error.message);
65
+
66
+ try {
67
+ sdk_tokens = JSON.parse(body);
68
+ }
69
+ catch (error) {
70
+ log.die(`Failed to parse ${sdk_tokens_file}`, "Remove it and try again");
71
+ }
72
+ }
73
+
74
+ sdk_tokens[server_opts.host] = sdk_token;
75
+ fs.writeFile(sdk_tokens_file, JSON.stringify(sdk_tokens), { mode: 0o600 }, function(error) {
76
+ if (error) log.die(`Failed to save ${sdk_tokens_file}`, error.message);
77
+ resolve();
78
+ });
79
+ });
80
+ });
81
+ }
82
+
83
+ function deleteSdkTokens() {
84
+ return new Promise(function(resolve, reject) {
85
+ fs.unlink(sdk_tokens_file, function(error) {
86
+ if (error) log.die("Failed to delete " + sdk_tokens_file, error.message);
87
+ resolve();
88
+ });
89
+ });
90
+ }
91
+
92
+ const AUTHENTICATED_REQUEST_METHODS = new Set([
93
+ "template/assign-version-number", "template/publish", "template/delete", "template/list", "template/history",
94
+ "user/whoami"
95
+ ]);
96
+
97
+ const MULTIPART_REQUEST_METHODS = new Set([
98
+ "template/publish",
99
+ ]);
100
+
101
+ function request(server_opts, method, data) {
102
+ let read_sdk_token_if_necessary;
103
+ if (AUTHENTICATED_REQUEST_METHODS.has(method)) {
104
+ read_sdk_token_if_necessary = getSdkToken(server_opts)
105
+ .catch((error) => {
106
+ log.problem(`Failed to read ${sdk_tokens_file}`, error.message);
107
+ })
108
+ .then((sdk_token) => {
109
+ if (!sdk_token) {
110
+ log.die("You are not logged in. Try ‘flourish login’ or ‘flourish register’ first.");
111
+ }
112
+ return sdk_token;
113
+ });
114
+ }
115
+ else {
116
+ read_sdk_token_if_necessary = Promise.resolve();
117
+ }
118
+
119
+ return Promise.all([read_sdk_token_if_necessary, getSDKVersion()])
120
+ .then(([sdk_token, sdk_version]) => new Promise(function(resolve, reject) {
121
+ let protocol = "https";
122
+ if (server_opts.host.match(/^(localhost|127\.0\.0\.1|.*\.local)(:\d+)?$/)) {
123
+ protocol = "http";
124
+ }
125
+ let url = protocol + "://" + server_opts.host + "/api/v1/" + method;
126
+ let request_params = {
127
+ method: "POST",
128
+ uri: url,
129
+ };
130
+
131
+ Object.assign(data, { sdk_token, sdk_version });
132
+ if (server_opts.user) {
133
+ request_params.auth = {
134
+ user: server_opts.user,
135
+ pass: server_opts.password,
136
+ sendImmediately: true,
137
+ };
138
+ }
139
+
140
+ if (MULTIPART_REQUEST_METHODS.has(method)) {
141
+ request_params.formData = data;
142
+ }
143
+ else {
144
+ request_params.headers = { "Content-Type": "application/json" };
145
+ request_params.body = JSON.stringify(data);
146
+ }
147
+
148
+ mod_request(request_params, function(error, res) {
149
+ if (error) log.die(error);
150
+ if (res.statusCode == 200) {
151
+ let r;
152
+ try { r = JSON.parse(res.body); }
153
+ catch (error) {
154
+ log.die("Failed to parse response from server", error, res.body);
155
+ }
156
+ return resolve(r);
157
+ }
158
+
159
+ // We got an error response. See if we can parse it to extract an error message
160
+ try {
161
+ let r = JSON.parse(res.body);
162
+ if ("error" in r) log.die("Error from server", r.error);
163
+ }
164
+ catch (e) { }
165
+ log.die("Server error", res.body);
166
+ });
167
+ }));
168
+ }
169
+
170
+ function runBuildCommand(template_dir, command, node_env) {
171
+ const command_parts = shell_quote.parse(command),
172
+ prog = command_parts[0],
173
+ args = command_parts.slice(1);
174
+
175
+ return new Promise(function(resolve, reject) {
176
+ log.info("Running build command: " + command);
177
+ try {
178
+ const env = process.env;
179
+ if (typeof node_env !== "undefined") env.NODE_ENV = node_env;
180
+
181
+ cross_spawn.spawn(prog, args, { cwd: template_dir, stdio: "inherit", env })
182
+ .on("error", function(error) {
183
+ reject(new Error(`Failed to run build command ‘${command}’: ${error.message}`));
184
+ })
185
+ .on("exit", function(exit_code) {
186
+ if (exit_code != 0) {
187
+ reject(new Error(`Failed to run build command ‘${command}’`));
188
+ }
189
+ resolve();
190
+ });
191
+ }
192
+ catch (error) {
193
+ reject(new Error(`Failed to run build command ‘${command}’ in ${template_dir}: ${error.message}`));
194
+ }
195
+ });
196
+ }
197
+
198
+ function buildTemplate(template_dir, node_env, purpose) {
199
+ return checkTemplateVersion(template_dir)
200
+ .then(() => installNodeModules(template_dir, node_env))
201
+ .then(() => buildRules(template_dir))
202
+ .then((build_rules) => Promise.all([...build_rules].map((rule) => {
203
+ // If we’re building the template in order to run it,
204
+ // and there is a watch script defined, don’t build it
205
+ // and rely on the watch script instead.
206
+ if (purpose !== "run" || !("watch" in rule)) {
207
+ return runBuildCommand(template_dir, rule.script, node_env);
208
+ }
209
+ })));
210
+ }
211
+
212
+
213
+ function readConfig(template_dir) {
214
+ return Promise.all([
215
+ readYaml(path.join(template_dir, "template.yml")),
216
+ readJson(path.join(template_dir, "package.json"))
217
+ ]).then(([yaml, json]) => {
218
+ if (json) {
219
+ if (!("id" in yaml) && ("name" in json)) {
220
+ yaml.id = json.name;
221
+ }
222
+ if (!("author" in yaml) && ("author" in json)) {
223
+ yaml.author = json.author;
224
+ }
225
+ if (!("description" in yaml) && ("description" in json)) {
226
+ yaml.description = json.description;
227
+ }
228
+ if (!("version" in yaml) && ("version" in json)) {
229
+ yaml.version = json.version;
230
+ }
231
+ }
232
+ return yaml;
233
+ });
234
+ }
235
+
236
+ function readYaml(yaml_file) {
237
+ return new Promise(function(resolve, reject) {
238
+ fs.readFile(yaml_file, "utf8", function(error, text) {
239
+ if (error) return reject(new Error(`Failed to read ${yaml_file}: ${error.message}`));
240
+ try {
241
+ return resolve(yaml.safeLoad(text));
242
+ }
243
+ catch (error) {
244
+ return reject(new Error(`Failed to parse ${yaml_file}: ${error.message}`));
245
+ }
246
+ });
247
+ });
248
+ }
249
+
250
+ function readJson(json_file) {
251
+ return new Promise(function(resolve, reject) {
252
+ fs.readFile(json_file, "utf8", function(error, text) {
253
+ if (error && error.code === "ENOENT") return resolve(undefined);
254
+ else if (error) return reject(new Error(`Failed to read ${json_file}: ${error.message}`));
255
+
256
+ try {
257
+ return resolve(JSON.parse(text));
258
+ }
259
+ catch (error) {
260
+ return reject(new Error(`Failed to parse ${json_file}: ${error.message}`));
261
+ }
262
+ });
263
+ });
264
+ }
265
+
266
+
267
+ function addShowCondition(setting) {
268
+ if (typeof setting === "string") return;
269
+ if (!["show_if", "hide_if"].some(d => d in setting)) return;
270
+ if (!setting.show_condition) setting.show_condition = [];
271
+ if (setting.show_if !== undefined) {
272
+ setting.show_condition.push({ type: "show", condition: setting.show_if });
273
+ delete setting.show_if;
274
+ }
275
+ else {
276
+ setting.show_condition.push({ type: "hide", condition: setting.hide_if });
277
+ delete setting.hide_if;
278
+ }
279
+ }
280
+
281
+
282
+ function addShowConditions(config) {
283
+ const settings = config.settings || [];
284
+ for (const setting of settings) {
285
+ addShowCondition(setting);
286
+ }
287
+ return config;
288
+ }
289
+
290
+
291
+ function qualifyNames(settings, namespace) {
292
+ for (let i = 0; i < settings.length; i++) {
293
+ const setting = settings[i];
294
+
295
+ if (typeof setting !== "object") continue;
296
+
297
+ if ("show_if" in setting) {
298
+ const type = typeof setting.show_if;
299
+ if (type === "string") {
300
+ setting.show_if = namespace + "." + setting.show_if;
301
+ }
302
+ else if (type === "object") {
303
+ const r = {};
304
+ for (const k in setting.show_if) {
305
+ r[namespace + "." + k] = setting.show_if[k];
306
+ }
307
+ setting.show_if = r;
308
+ }
309
+ // Else pass through unmodified: to support literal true/false values.
310
+ }
311
+
312
+ if ("hide_if" in setting) {
313
+ const type = typeof setting.hide_if;
314
+ if (typeof setting.hide_if === "string") {
315
+ setting.hide_if = namespace + "." + setting.hide_if;
316
+ }
317
+ else if (type === "object") {
318
+ const r = {};
319
+ for (const k in setting.hide_if) {
320
+ r[namespace + "." + k] = setting.hide_if[k];
321
+ }
322
+ setting.hide_if = r;
323
+ }
324
+ // Else pass through unmodified: to support literal true/false values.
325
+ }
326
+ }
327
+ }
328
+
329
+ async function resolveImports(config, template_dir) {
330
+ const settings = config.settings;
331
+ if (!settings) return config;
332
+
333
+ for (let i = 0; i < settings.length; i++) {
334
+ const setting = settings[i];
335
+ if (typeof setting === "object" && "import" in setting) {
336
+ const imported_resolved = nodeResolve.sync(path.join(setting.import, "settings.yml"), { basedir: template_dir });
337
+ const imported_settings = await readYaml(imported_resolved);
338
+ qualifyNames(imported_settings, setting.property);
339
+ if ("overrides" in setting) {
340
+ setting.overrides.forEach(function(override) {
341
+ const properties = Array.isArray(override.property) ? override.property : [override.property];
342
+ const method = override.method || "replace";
343
+ for (let property of properties) {
344
+ const s = imported_settings.find(function(setting) { return setting.property === property; });
345
+ if (!s) continue;
346
+ for (let name in override) {
347
+ if (name === "property" || name === "method") continue;
348
+ if (method === "extend") {
349
+ let extendee = s[name];
350
+ if (extendee === undefined) {
351
+ if (name === "show_if" && s.hide_if !== undefined) {
352
+ Error(`Cannot extend a show_if when hide_if defined for property ${s.property}`);
353
+ }
354
+ else if (name === "hide_if" && s.show_if !== undefined) {
355
+ Error(`Cannot extend a hide_if when show_if defined for property ${s.property}`);
356
+ }
357
+ extendee = {};
358
+ }
359
+ s[name] = extendItem(extendee, override[name]);
360
+ }
361
+ else {
362
+ s[name] = override[name];
363
+ if (name === "show_if" && s.hide_if) delete s.hide_if;
364
+ else if (name === "hide_if" && s.show_if) delete s.show_if;
365
+ }
366
+ }
367
+ }
368
+ });
369
+ }
370
+ for (let s of imported_settings) {
371
+ if (typeof s !== "object") continue;
372
+ s.property = setting.property + "." + s.property;
373
+ if (setting.show_condition) s.show_condition = setting.show_condition.slice();
374
+ addShowCondition(s);
375
+ }
376
+ settings.splice.apply(settings, [i, 1].concat(imported_settings));
377
+ }
378
+ }
379
+
380
+ return config;
381
+ }
382
+
383
+ function readAndValidateConfig(template_dir) {
384
+ return readConfig(template_dir)
385
+ .then((config) => {
386
+ validateConfig(config, template_dir);
387
+ return config;
388
+ })
389
+ .then(config => addShowConditions(config))
390
+ .then(config => resolveImports(config, template_dir));
391
+ }
392
+
393
+ function changeVersionNumberInPackageJson(template_dir, change_function) {
394
+ if (!fs.existsSync(path.join(template_dir, "package.json"))) {
395
+ throw new Error("There is no version number in template.yml, and no package.json");
396
+ }
397
+
398
+ return readJson(path.join(template_dir, "package.json"))
399
+ .then(json => {
400
+ if (!json.version) {
401
+ throw new Error("There is no version number in template.yml or package.json");
402
+ }
403
+ const v = semver.parse(json.version);
404
+ change_function(v);
405
+ json.version = semver.join(v);
406
+ return writePackageJson(template_dir, json);
407
+ });
408
+ }
409
+
410
+ function changeVersionNumber(template_dir, change_function) {
411
+ return readYaml(path.join(template_dir, "template.yml"))
412
+ .then(yaml => {
413
+ if (!yaml.version) {
414
+ return changeVersionNumberInPackageJson(template_dir, change_function);
415
+ }
416
+
417
+ const v = semver.parse(yaml.version);
418
+ change_function(v);
419
+ yaml.version = semver.join(v);
420
+ return writeConfig(template_dir, yaml);
421
+ });
422
+ }
423
+
424
+ function incrementPrereleaseTag(template_dir) {
425
+ return changeVersionNumber(template_dir, v => {
426
+ if (v.length == 3) {
427
+ v[2] += 1;
428
+ v.push("prerelease", 1);
429
+ return;
430
+ }
431
+ if (typeof v[v.length - 1] === "number") {
432
+ v[v.length - 1] += 1;
433
+ }
434
+ else v.push(1);
435
+ });
436
+ }
437
+
438
+ function removePrereleaseTag(template_dir) {
439
+ return changeVersionNumber(template_dir, v => {
440
+ if (v.length == 3) {
441
+ throw new Error("There is no prerelease tag to remove.");
442
+ }
443
+ v.splice(3);
444
+ });
445
+ }
446
+
447
+ function incrementPatchVersion(template_dir) {
448
+ return changeVersionNumber(template_dir, v => {
449
+ v[2] += 1;
450
+ v.splice(3);
451
+ });
452
+ }
453
+
454
+ function installNodeModules(template_dir, node_env) {
455
+ if (fs.existsSync(path.join(template_dir, "package.json"))
456
+ && !fs.existsSync(path.join(template_dir, "node_modules")))
457
+ {
458
+ if (fs.existsSync(path.join(template_dir, "package-lock.json"))) {
459
+ return runBuildCommand(template_dir, "npm ci", node_env);
460
+ }
461
+ return runBuildCommand(template_dir, "npm install", node_env);
462
+ }
463
+ else {
464
+ return Promise.resolve();
465
+ }
466
+ }
467
+
468
+ function buildRules(template_dir) {
469
+ return readConfig(template_dir)
470
+ .then((config) => {
471
+ const build_rules = [];
472
+ for (let key in config.build) {
473
+ build_rules.push(Object.assign({ key }, config.build[key]));
474
+ }
475
+ return build_rules;
476
+ });
477
+ }
478
+
479
+ function writeConfig(template_dir, config) {
480
+ return new Promise(function(resolve, reject) {
481
+ const config_file = path.join(template_dir, "template.yml");
482
+ fs.writeFile(config_file, yaml.safeDump(config, YAML_DUMP_OPTS), function(error) {
483
+ if (error) return reject(new Error(`Failed to write ${config_file}: ${error.message}`));
484
+ return resolve();
485
+ });
486
+ });
487
+ }
488
+
489
+ function writePackageJson(template_dir, json) {
490
+ return new Promise(function(resolve, reject) {
491
+ const package_json_file = path.join(template_dir, "package.json");
492
+ fs.writeFile(package_json_file, JSON.stringify(json, null, 2) + "\n", function(error) {
493
+ if (error) return reject(new Error(`Failed to write ${package_json_file}: ${error.message}`));
494
+ return resolve();
495
+ });
496
+ });
497
+ }
498
+
499
+ function checkTemplateVersion(template_dir) {
500
+ return Promise.all([
501
+ readConfig(template_dir),
502
+ getSDKMajorVersion(),
503
+ ]).then(([config, sdk_major_version]) => {
504
+ const template_sdk_version = config.sdk_version;
505
+ if (!template_sdk_version) {
506
+ throw new Error("Template does not specify an sdk_version");
507
+ }
508
+ if (template_sdk_version < sdk_major_version) {
509
+ throw new Error("This template was built for an older version of Flourish. Try running 'flourish upgrade'");
510
+ }
511
+ if (template_sdk_version > sdk_major_version) {
512
+ throw new Error("This template was built for an newer version of Flourish than you have. Try updating the SDK.");
513
+ }
514
+ });
515
+ }
516
+
517
+
518
+ // Files and directories in a template that are treated specially by Flourish
519
+ const TEMPLATE_SPECIAL_FILES = new Set([
520
+ "index.html", "template.js", "template.yml", "thumbnail.png", "thumbnail.jpg", "README.md",
521
+ ]);
522
+ const TEMPLATE_SPECIAL_DIRECTORIES = new Set([
523
+ "static", "data",
524
+ ]);
525
+ const TEMPLATE_SPECIAL = new Set([
526
+ "index.html", "template.js", "template.yml", "thumbnail.png", "thumbnail.jpg", "README.md",
527
+ "static", "data",
528
+ ]);
529
+
530
+ module.exports = {
531
+ checkTemplateVersion, getSDKVersion, getSDKMajorVersion,
532
+
533
+ getSdkToken, setSdkToken, deleteSdkTokens,
534
+ request,
535
+ runBuildCommand, buildTemplate,
536
+ readConfig, readAndValidateConfig, writeConfig, buildRules,
537
+ incrementPrereleaseTag, removePrereleaseTag, incrementPatchVersion,
538
+
539
+ TEMPLATE_SPECIAL_FILES, TEMPLATE_SPECIAL_DIRECTORIES, TEMPLATE_SPECIAL,
540
+ };
@@ -158,7 +158,8 @@ function validateImport(template_directory, setting) {
158
158
  if (k == "overrides") {
159
159
  if (!Array.isArray(setting.overrides)) throw new Error(`template.yml Setting import overrides must be an array`);
160
160
  setting.overrides.forEach(function(override) {
161
- if (!("property" in override)) throw new Error(`template.yml Setting import overrides must each specify overridden “property”`);
161
+ if (!("property" in override) && !("tag" in override)) throw new Error(`template.yml Setting import overrides must each specify overridden “property” or “tag”`);
162
+ if (("property" in override) && ("tag" in override)) throw new Error(`template.yml Setting import overrides cannot contain both “property” and “tag” property`);
162
163
  if (![undefined, "replace", "extend"].includes(override.method)) {
163
164
  throw new Error(`template.yml Setting import override “method” method must be either “replace” or “extend”`);
164
165
  }