UNPKG

@flourish/sdk

Version:
478 lines (421 loc) 13.9 kB
"use strict"; const fs = require("fs"), path = require("path"), cross_spawn = require("cross-spawn"), mod_request = require("request"), shell_quote = require("shell-quote"), yaml = require("js-yaml"), nodeResolve = require("resolve"), semver = require("@flourish/semver"), log = require("./log"), validateConfig = require("./validate_config"); const sdk_tokens_file = path.join(process.env.HOME || process.env.USERPROFILE, ".flourish_sdk"); const YAML_DUMP_OPTS = { flowLevel: 4 }; const package_json_filename = path.join(__dirname, "..", "package.json"); var sdk_version = null; function getSDKVersion() { if (sdk_version) return Promise.resolve(sdk_version); return new Promise(function(resolve, reject) { fs.readFile(package_json_filename, "utf8", function(error, package_json) { if (error) return reject(error); const package_object = JSON.parse(package_json); resolve(sdk_version = package_object.version); }); }); } function getSDKMajorVersion() { return getSDKVersion() .then((sdk_version) => { const version_tuple = sdk_version.split(".").map((x) => parseInt(x)); return version_tuple[0]; }); } function getSdkToken(server_opts) { return new Promise(function(resolve, reject) { fs.chmod(sdk_tokens_file, 0o600, function(error) { if (error) return reject(error); fs.readFile(sdk_tokens_file, "utf8", function(error, body) { if (error) return reject(error); resolve(JSON.parse(body)[server_opts.host]); }); }); }); } function setSdkToken(server_opts, sdk_token) { return new Promise(function(resolve, reject) { fs.readFile(sdk_tokens_file, function(error, body) { let sdk_tokens; if (error && error.code === "ENOENT") { sdk_tokens = {}; } else { if (error) log.die(`Failed to read ${sdk_tokens_file}`, error.message); try { sdk_tokens = JSON.parse(body); } catch (error) { log.die(`Failed to parse ${sdk_tokens_file}`, "Remove it and try again"); } } sdk_tokens[server_opts.host] = sdk_token; fs.writeFile(sdk_tokens_file, JSON.stringify(sdk_tokens), { mode: 0o600 }, function(error) { if (error) log.die(`Failed to save ${sdk_tokens_file}`, error.message); resolve(); }); }); }); } function deleteSdkTokens() { return new Promise(function(resolve, reject) { fs.unlink(sdk_tokens_file, function(error) { if (error) log.die("Failed to delete " + sdk_tokens_file, error.message); resolve(); }); }); } const AUTHENTICATED_REQUEST_METHODS = new Set([ "template/assign-version-number", "template/publish", "template/delete", "template/list", "template/history", "user/whoami" ]); const MULTIPART_REQUEST_METHODS = new Set([ "template/publish", ]); function request(server_opts, method, data) { let read_sdk_token_if_necessary; if (AUTHENTICATED_REQUEST_METHODS.has(method)) { read_sdk_token_if_necessary = getSdkToken(server_opts) .catch((error) => { log.problem(`Failed to read ${sdk_tokens_file}`, error.message); }) .then((sdk_token) => { if (!sdk_token) { log.die("You are not logged in. Try ‘flourish login’ or ‘flourish register’ first."); } return sdk_token; }); } else { read_sdk_token_if_necessary = Promise.resolve(); } return Promise.all([read_sdk_token_if_necessary, getSDKVersion()]) .then(([sdk_token, sdk_version]) => new Promise(function(resolve, reject) { let protocol = "https"; if (server_opts.host.match(/^(localhost|127\.0\.0\.1|.*\.local)(:\d+)?$/)) { protocol = "http"; } let url = protocol + "://" + server_opts.host + "/api/v1/" + method; let request_params = { method: "POST", uri: url, }; Object.assign(data, { sdk_token, sdk_version }); if (server_opts.user) { request_params.auth = { user: server_opts.user, pass: server_opts.password, sendImmediately: true, }; } if (MULTIPART_REQUEST_METHODS.has(method)) { request_params.formData = data; } else { request_params.headers = { "Content-Type": "application/json" }; request_params.body = JSON.stringify(data); } mod_request(request_params, function(error, res) { if (error) log.die(error); if (res.statusCode == 200) { let r; try { r = JSON.parse(res.body); } catch (error) { log.die("Failed to parse response from server", error, res.body); } return resolve(r); } // We got an error response. See if we can parse it to extract an error message try { let r = JSON.parse(res.body); if ("error" in r) log.die("Error from server", r.error); } catch (e) { } log.die("Server error", res.body); }); })); } function runBuildCommand(template_dir, command, node_env) { const command_parts = shell_quote.parse(command), prog = command_parts[0], args = command_parts.slice(1); return new Promise(function(resolve, reject) { log.info("Running build command: " + command); try { const env = process.env; if (typeof node_env !== "undefined") env.NODE_ENV = node_env; cross_spawn.spawn(prog, args, { cwd: template_dir, stdio: "inherit", env }) .on("error", function(error) { reject(new Error(`Failed to run build command ‘${command}’: ${error.message}`)); }) .on("exit", function(exit_code) { if (exit_code != 0) { reject(new Error(`Failed to run build command ‘${command}’`)); } resolve(); }); } catch (error) { reject(new Error(`Failed to run build command ‘${command}’ in ${template_dir}: ${error.message}`)); } }); } function buildTemplate(template_dir, node_env) { return checkTemplateVersion(template_dir) .then(() => installNodeModules(template_dir, node_env)) .then(() => buildRules(template_dir)) .then((build_rules) => Promise.all([...build_rules].map((rule) => runBuildCommand(template_dir, rule.script, node_env)))); } function readConfig(template_dir) { return Promise.all([ readYaml(path.join(template_dir, "template.yml")), readJson(path.join(template_dir, "package.json")) ]).then(([yaml, json]) => { if (json) { if (!("id" in yaml) && ("name" in json)) { yaml.id = json.name; } if (!("author" in yaml) && ("author" in json)) { yaml.author = json.author; } if (!("description" in yaml) && ("description" in json)) { yaml.description = json.description; } if (!("version" in yaml) && ("version" in json)) { yaml.version = json.version; } } return yaml; }); } function readYaml(yaml_file) { return new Promise(function(resolve, reject) { fs.readFile(yaml_file, "utf8", function(error, text) { if (error) return reject(new Error(`Failed to read ${yaml_file}: ${error.message}`)); try { return resolve(yaml.safeLoad(text)); } catch (error) { return reject(new Error(`Failed to parse ${yaml_file}: ${error.message}`)); } }); }); } function readJson(json_file) { return new Promise(function(resolve, reject) { fs.readFile(json_file, "utf8", function(error, text) { if (error && error.code === "ENOENT") return resolve(undefined); else if (error) return reject(new Error(`Failed to read ${json_file}: ${error.message}`)); try { return resolve(JSON.parse(text)); } catch (error) { return reject(new Error(`Failed to parse ${json_file}: ${error.message}`)); } }); }); } function qualifyNames(settings, namespace) { for (let i = 0; i < settings.length; i++) { const setting = settings[i]; if (typeof setting !== "object") continue; if ("show_if" in setting) { if (typeof setting.show_if === "string") { setting.show_if = namespace + "." + setting.show_if; } else { const r = {}; for (const k in setting.show_if) { r[namespace + "." + k] = setting.show_if[k]; } setting.show_if = r; } } if ("hide_if" in setting) { if (typeof setting.hide_if === "string") { setting.hide_if = namespace + "." + setting.hide_if; } else { const r = {}; for (const k in setting.hide_if) { r[namespace + "." + k] = setting.hide_if[k]; } setting.hide_if = r; } } } } async function resolveImports(config, template_dir) { const settings = config.settings; if (!settings) return config; for (let i = 0; i < settings.length; i++) { const setting = settings[i]; if (typeof setting === "object" && "import" in setting) { const imported_resolved = nodeResolve.sync(path.join(setting.import, "settings.yml"), { basedir: template_dir }); const imported_settings = await readYaml(imported_resolved); qualifyNames(imported_settings, setting.property); if ("overrides" in setting) { setting.overrides.forEach(function(override) { const s = imported_settings.find(function(setting) { return setting.property == override.property; }); if (!s) return; for (let name in override) { if (name !== "property") s[name] = override[name]; if (name == "show_if" && s.hide_if) delete s.hide_if; if (name == "hide_if" && s.show_if) delete s.show_if; } }); } for (let j = 0; j < imported_settings.length; j++) { const s = imported_settings[j]; if (typeof s === "object") s.property = setting.property + "." + s.property; } settings.splice.apply(settings, [i, 1].concat(imported_settings)); } } return config; } function readAndValidateConfig(template_dir) { return readConfig(template_dir) .then((config) => { validateConfig(config, template_dir); return config; }) .then(config => resolveImports(config, template_dir)); } function changeVersionNumberInPackageJson(template_dir, change_function) { if (!fs.existsSync(path.join(template_dir, "package.json"))) { throw new Error("There is no version number in template.yml, and no package.json"); } return readJson(path.join(template_dir, "package.json")) .then(json => { if (!json.version) { throw new Error("There is no version number in template.yml or package.json"); } const v = semver.parse(json.version); change_function(v); json.version = semver.join(v); return writePackageJson(template_dir, json); }); } function changeVersionNumber(template_dir, change_function) { return readYaml(path.join(template_dir, "template.yml")) .then(yaml => { if (!yaml.version) { return changeVersionNumberInPackageJson(template_dir, change_function); } const v = semver.parse(yaml.version); change_function(v); yaml.version = semver.join(v); return writeConfig(template_dir, yaml); }); } function incrementPrereleaseTag(template_dir) { return changeVersionNumber(template_dir, v => { if (v.length == 3) { v[2] += 1; v.push("prerelease", 1); return; } if (typeof v[v.length - 1] === "number") { v[v.length - 1] += 1; } else v.push(1); }); } function removePrereleaseTag(template_dir) { return changeVersionNumber(template_dir, v => { if (v.length == 3) { throw new Error("There is no prerelease tag to remove."); } v.splice(3); }); } function incrementPatchVersion(template_dir) { return changeVersionNumber(template_dir, v => { v[2] += 1; v.splice(3); }); } function installNodeModules(template_dir, node_env) { if (fs.existsSync(path.join(template_dir, "package.json")) && !fs.existsSync(path.join(template_dir, "node_modules"))) { return runBuildCommand(template_dir, "npm install", node_env); } else { return Promise.resolve(); } } function buildRules(template_dir) { return readConfig(template_dir) .then((config) => { const build_rules = []; for (let key in config.build) { build_rules.push(Object.assign({ key }, config.build[key])); } return build_rules; }); } function writeConfig(template_dir, config) { return new Promise(function(resolve, reject) { const config_file = path.join(template_dir, "template.yml"); fs.writeFile(config_file, yaml.safeDump(config, YAML_DUMP_OPTS), function(error) { if (error) return reject(new Error(`Failed to write ${config_file}: ${error.message}`)); return resolve(); }); }); } function writePackageJson(template_dir, json) { return new Promise(function(resolve, reject) { const package_json_file = path.join(template_dir, "package.json"); fs.writeFile(package_json_file, JSON.stringify(json, null, 2) + "\n", function(error) { if (error) return reject(new Error(`Failed to write ${package_json_file}: ${error.message}`)); return resolve(); }); }); } function checkTemplateVersion(template_dir) { return Promise.all([ readConfig(template_dir), getSDKMajorVersion(), ]).then(([config, sdk_major_version]) => { const template_sdk_version = config.sdk_version; if (!template_sdk_version) { throw new Error("Template does not specify an sdk_version"); } if (template_sdk_version < sdk_major_version) { throw new Error("This template was built for an older version of Flourish. Try running 'flourish upgrade'"); } if (template_sdk_version > sdk_major_version) { throw new Error("This template was built for an newer version of Flourish than you have. Try updating the SDK."); } }); } // Files and directories in a template that are treated specially by Flourish const TEMPLATE_SPECIAL_FILES = new Set([ "index.html", "template.js", "template.yml", "thumbnail.png", "thumbnail.jpg", "README.md", ]); const TEMPLATE_SPECIAL_DIRECTORIES = new Set([ "static", "data", ]); const TEMPLATE_SPECIAL = new Set([ "index.html", "template.js", "template.yml", "thumbnail.png", "thumbnail.jpg", "README.md", "static", "data", ]); module.exports = { checkTemplateVersion, getSDKVersion, getSDKMajorVersion, getSdkToken, setSdkToken, deleteSdkTokens, request, runBuildCommand, buildTemplate, readConfig, readAndValidateConfig, writeConfig, buildRules, incrementPrereleaseTag, removePrereleaseTag, incrementPatchVersion, TEMPLATE_SPECIAL_FILES, TEMPLATE_SPECIAL_DIRECTORIES, TEMPLATE_SPECIAL, };