UNPKG

6.49 kBJavaScriptView Raw
1import R from "ramda";
2import Promise from "bluebird";
3import shell from "shelljs";
4import fsPath from "path";
5import github from "file-system-github";
6import Route from "./route";
7import { isEmpty } from "./util";
8import { DEFAULT_APP_PORT, DEFAULT_TARGET_FOLDER } from "./const";
9
10import appInstall from "./app-install";
11import appVersion from "./app-version";
12import appDownload from "./app-download";
13import appUpdate from "./app-update";
14import { getLocalPackage, getRemotePackage } from "./app-package";
15
16
17/**
18 * Creates a new object representing an application.
19 * @param options:
20 * - id: The unique name of the app (ID).
21 * - userAgent: https://developer.github.com/v3/#user-agent-required
22 * - token: The Github authorization token to use for calls to
23 * restricted resources.
24 * see: https://github.com/settings/tokens
25 * - route: Route details for directing requests to the app.
26 * - targetFolder: The root location where apps are downloaded to.
27 * - repo: The Github 'username/repo'.
28 * Optionally you can specify a sub-path within the repos
29 * like this:
30 * 'username/repo/my/sub/path'
31 * - port: The port the app runs on.
32 * - branch: The branch to query. Default: "master".
33 * - publishEvent: A function that publishes an event to all other instances.
34 */
35export default (settings = {}) => {
36 // Setup initial conditions.
37 let { userAgent, token, targetFolder, id, repo, port, branch, route, publishEvent } = settings;
38 if (isEmpty(id)) { throw new Error(`'id' for the app is required`); }
39 if (isEmpty(repo)) { throw new Error(`'repo' name required, eg. 'username/my-repo'`); }
40 if (isEmpty(userAgent)) { throw new Error(`The github API user-agent must be specified. See: https://developer.github.com/v3/#user-agent-required`); }
41 if (isEmpty(route)) { throw new Error(`A 'route' must be specified for the '${ id }' app.`); }
42 route = Route.parse(route);
43 branch = branch || "master";
44 targetFolder = targetFolder || DEFAULT_TARGET_FOLDER;
45 port = port || DEFAULT_APP_PORT;
46 const WORKING_DIRECTORY = process.cwd();
47
48 // Extract the repo and sub-path.
49 const fullPath = repo;
50 let parts = repo.split("/");
51 if (parts.length < 2) { throw new Error(`A repo must have a 'username' and 'repo-name', eg 'username/repo'.`); }
52 const repoUser = parts[0];
53 const repoName = parts[1];
54 repo = github.repo(userAgent, `${ repoUser }/${ repoName }`, { token });
55 parts = R.takeLast(parts.length - 2, parts);
56 const repoSubFolder = parts.join("/");
57 const localFolder = fsPath.resolve(fsPath.join(targetFolder, id));
58 repo.path = repoSubFolder;
59 repo.fullPath = fullPath;
60
61 // Store values.
62 const app = {
63 id,
64 repo,
65 route,
66 port,
67 branch,
68 localFolder,
69
70
71 /**
72 * Retrieves the local [package.json] file.
73 * @return {Promise}
74 */
75 localPackage() { return getLocalPackage(id, this.localFolder); },
76
77
78 /**
79 * Retrieves the remote [package.json] file.
80 * @return {Promise}
81 */
82 remotePackage() { return getRemotePackage(id, repo, repoSubFolder, branch); },
83
84
85 /**
86 * Gets the local and remote versions.
87 * @return {Promise}
88 */
89 version() { return appVersion(id, this.localPackage(), this.remotePackage()); },
90
91
92 /**
93 * Downloads the app from the remote repository.
94 * @param options:
95 * - install: Flag indicating if `npm install` should be run on the directory.
96 * Default: true.
97 * - force: Flag indicating if the repository should be downloaded if
98 * is already present on the local disk.
99 * Default: true
100 * @return {Promise}.
101 */
102 download(options = {}) {
103 // Don't continue if a download operation is in progress.
104 if (this.downloading) { return this.downloading; }
105
106 // Start the download process.
107 this.downloading = appDownload(id, localFolder, repo, repoSubFolder, branch, options)
108 .then(result => {
109 this.isDownloading = false;
110 delete this.downloading;
111 return result;
112 });
113 return this.downloading;
114 },
115
116
117 /**
118 * Downloads a new version of the app (if necessary) and restarts it.
119 * @param options
120 * - start: Flag indicating if the app should be started after an update.
121 * Default: true.
122 * @return {Promise}.
123 */
124 update(options = {}) {
125 // Start the update process.
126 const updating = appUpdate(
127 id,
128 localFolder,
129 () => this.version(),
130 (args) => this.download(args),
131 (args) => this.start(args),
132 options
133 );
134
135 // If the app has been updated alert other containers.
136 updating.then(result => {
137 if (publishEvent && result.updated) {
138 publishEvent("app:updated", {
139 id: this.id,
140 version: result.version
141 });
142 }
143 });
144
145 // Finish up.
146 return updating;
147 },
148
149
150 /**
151 * Runs `npm install` on the app.
152 * @return {Promise}.
153 */
154 install() { return appInstall(localFolder); },
155
156
157
158 /**
159 * Starts the app within the `pm2` process monitor.
160 * @return {Promise}.
161 */
162 start() {
163 return new Promise((resolve, reject) => {
164 Promise.coroutine(function*() {
165 // Update and stop.
166 const status = yield this.update({ start: false }).catch(err => reject(err));
167 yield this.stop().catch(err => reject(err));
168
169 // Start the app.
170 shell.cd(localFolder);
171 shell.exec(`pm2 start . --name '${ id }:${ port }' --node-args '. --port ${ port }'`);
172 shell.cd(WORKING_DIRECTORY);
173
174 // Finish.
175 resolve({ id, started: true, port: this.port, route: this.route, version: status.version });
176 }).call(this);
177 });
178 },
179
180
181 /**
182 * Stops the app running within the 'pm2' process monitor.
183 * @return {Promise}.
184 */
185 stop() {
186 return new Promise((resolve) => {
187 shell.exec(`pm2 stop '${ id }:${ port }'`);
188 resolve({ id });
189 });
190 }
191 };
192
193
194 // Finish up.
195 return app;
196};