UNPKG

11.1 kBJavaScriptView Raw
1/*
2 * Copyright 2018 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13const path = require('path');
14const fse = require('fs-extra');
15const opn = require('open');
16const chokidar = require('chokidar');
17const chalk = require('chalk');
18const { HelixProject } = require('@adobe/helix-simulator');
19const { GitUrl } = require('@adobe/helix-shared');
20const GitUtils = require('./git-utils.js');
21const BuildCommand = require('./build.cmd');
22const pkgJson = require('../package.json');
23
24const HELIX_CONFIG = 'helix-config.yaml';
25const HELIX_QUERY = 'helix-query.yaml';
26const GIT_HEAD = '.git/HEAD';
27
28class UpCommand extends BuildCommand {
29 constructor(logger) {
30 super(logger);
31 this._httpPort = -1;
32 this._open = false;
33 this._liveReload = false;
34 this._saveConfig = false;
35 this._overrideHost = null;
36 this._localRepos = [];
37 this._devDefault = {};
38 this._devDefaultFile = () => ({});
39 this._githubToken = '';
40 this._algoliaAppID = null;
41 this._algoliaAPIKey = null;
42 }
43
44 withHttpPort(p) {
45 this._httpPort = p;
46 return this;
47 }
48
49 withOpen(o) {
50 this._open = !!o;
51 return this;
52 }
53
54 withLiveReload(value) {
55 this._liveReload = value;
56 return this;
57 }
58
59 withSaveConfig(value) {
60 this._saveConfig = value;
61 return this;
62 }
63
64 withOverrideHost(value) {
65 this._overrideHost = value;
66 return this;
67 }
68
69 withLocalRepo(value = []) {
70 if (Array.isArray(value)) {
71 this._localRepos.push(...value.filter((r) => !!r));
72 } else if (value) {
73 this._localRepos.push(value);
74 }
75 return this;
76 }
77
78 withHelixPagesRepo(url) {
79 this._helixPagesRepo = url;
80 return this;
81 }
82
83 withDevDefault(value) {
84 this._devDefault = value;
85 return this;
86 }
87
88 withDevDefaultFile(value) {
89 this._devDefaultFile = value;
90 return this;
91 }
92
93 withGithubToken(value) {
94 this._githubToken = value;
95 return this;
96 }
97
98 withAlgoliaAppID(value) {
99 this._algoliaAppID = value;
100 return this;
101 }
102
103 withAlgoliaAPIKey(value) {
104 this._algoliaAPIKey = value;
105 return this;
106 }
107
108 get project() {
109 return this._project;
110 }
111
112 async stop() {
113 if (this._watcher) {
114 await this._watcher.close();
115 this._watcher = null;
116 }
117 if (this._project) {
118 try {
119 await this._project.stop();
120 } catch (e) {
121 // ignore
122 }
123 this._project = null;
124 }
125 this.log.info('Helix project stopped.');
126 this.emit('stopped', this);
127 }
128
129 /**
130 * Sets up the source file watcher.
131 * @private
132 */
133 _initSourceWatcher(fn) {
134 let timer = null;
135 let modifiedFiles = {};
136
137 this._watcher = chokidar.watch([
138 'src', 'cgi-bin', '.hlx/pages/master/src', '.hlx/pages/master/cgi-bin',
139 HELIX_CONFIG, HELIX_QUERY, GIT_HEAD,
140 ], {
141 ignored: /(.*\.swx|.*\.swp|.*~)/,
142 persistent: true,
143 ignoreInitial: true,
144 cwd: this.directory,
145 });
146
147 this._watcher.on('all', (eventType, file) => {
148 modifiedFiles[file] = true;
149 if (timer) {
150 clearTimeout(timer);
151 }
152 // debounce a bit in case several files are changed at once
153 timer = setTimeout(async () => {
154 timer = null;
155 const files = modifiedFiles;
156 modifiedFiles = {};
157 await fn(files);
158 }, 250);
159 });
160 }
161
162 async setup() {
163 await super.init();
164 // check for git repository
165 if (!await fse.pathExists(path.join(this.directory, '.git'))) {
166 throw Error('hlx up needs local git repository.');
167 }
168 // init dev default file params
169 this._devDefault = Object.assign(this._devDefaultFile(this.directory), this._devDefault);
170
171 let hasConfigFile = await this.config.hasFile();
172 if (this._saveConfig) {
173 if (hasConfigFile) {
174 this.log.warn(chalk`Cowardly refusing to overwrite existing {cyan helix-config.yaml}.`);
175 } else {
176 await this.config.saveConfig();
177 this.log.info(chalk`Wrote new default config to {cyan ${path.relative(process.cwd(), this.config.configPath)}}.`);
178 hasConfigFile = true;
179 }
180 }
181
182 // check for git repository
183 if (!hasConfigFile && this._localRepos.length === 0) {
184 if (!this.config.strains.get('default').content.isLocal) {
185 // ensure local repository will be mounted for default config with origin
186 this._localRepos.push('.');
187 }
188 }
189
190 // check all local repos
191 const localRepos = (await Promise.all(this._localRepos.map(async (repo) => {
192 const repoPath = path.resolve(this.directory, repo);
193 if (!await fse.pathExists(path.join(repoPath, '.git'))) {
194 throw Error(`Specified --local-repo=${repo} is not a git repository.`);
195 }
196 const gitUrl = await GitUtils.getOriginURL(repoPath);
197 if (!gitUrl) {
198 if (repoPath !== this.directory) {
199 this.log.warn(`Ignoring --local-repo=${repo}. No remote 'origin' defined.`);
200 }
201 return null;
202 }
203 return {
204 gitUrl,
205 repoPath,
206 };
207 }))).filter((e) => !!e);
208
209 // add github token to action params
210 if (this._githubToken && !this._devDefault.GITHUB_TOKEN) {
211 this._devDefault.GITHUB_TOKEN = this._githubToken;
212 }
213
214 // algolia default credentials
215 const ALGOLIA_APP_ID = 'A8PL9E4TZT';
216 const ALGOLIA_API_KEY = '3934d5173a5fedf1cb7c619a6a26f300';
217
218 this._project = new HelixProject()
219 .withCwd(this.directory)
220 .withBuildDir(this._target)
221 .withHelixConfig(this.config)
222 .withIndexConfig(this.indexConfig)
223 .withAlgoliaAppID(this._algoliaAppID || ALGOLIA_APP_ID)
224 .withAlgoliaAPIKey(this._algoliaAPIKey || ALGOLIA_API_KEY)
225 .withActionParams(this._devDefault)
226 .withLiveReload(this._liveReload)
227 .withRuntimeModulePaths(module.paths);
228
229 // special handling for helix pages project
230 if (await this.helixPages.isPagesProject()) {
231 this.log.info(' __ __ ___ ___ ');
232 this.log.info(' / // /__ / (_)_ __ / _ \\___ ____ ____ ___');
233 this.log.info(' / _ / -_) / /\\ \\ // ___/ _ `/ _ `/ -_|_-<');
234 this.log.info(' /_//_/\\__/_/_//_\\_\\/_/ \\_,_/\\_, /\\__/___/');
235 this.log.info(` /___/ v${pkgJson.version}`);
236 this.log.info('');
237
238 // use bundled helix-pages sources
239 this._project.withSourceDir(this.helixPages.srcDirectory);
240
241 // local branch should be considered as default for pages project
242 const ref = await GitUtils.getBranch(this.directory);
243 const defaultStrain = this.config.strains.get('default');
244 defaultStrain.content = new GitUrl({
245 ...defaultStrain.content.toJSON(),
246 ref,
247 });
248
249 // use bundled helix-pages htdocs
250 if (!await fse.pathExists(path.join(this.directory, 'htdocs'))) {
251 defaultStrain.static.url = this.helixPages.staticURL;
252 localRepos.push({
253 repoPath: this.helixPages.checkoutDirectory,
254 gitUrl: this.helixPages.staticURL,
255 });
256 }
257 } else {
258 this.log.info(' __ __ ___ ');
259 this.log.info(' / // /__ / (_)_ __ ');
260 this.log.info(' / _ / -_) / /\\ \\ / ');
261 this.log.info(` /_//_/\\__/_/_//_\\_\\ v${pkgJson.version}`);
262 this.log.info(' ');
263 this._project.withSourceDir(path.resolve(this._sourceRoot, 'src'));
264 this._project.withSourceDir(path.resolve(this._sourceRoot, 'cgi-bin'));
265 }
266
267 // start debugger (#178)
268 // https://nodejs.org/en/docs/guides/debugging-getting-started/#enable-inspector
269 if (process.platform !== 'win32') {
270 process.kill(process.pid, 'SIGUSR1');
271 }
272
273 if (this._httpPort >= 0) {
274 this._project.withHttpPort(this._httpPort);
275 }
276 if (this._overrideHost) {
277 this._project.withRequestOverride({
278 headers: {
279 host: this._overrideHost,
280 },
281 });
282 }
283
284 try {
285 await this._project.init();
286 } catch (e) {
287 throw Error(`Unable to start helix: ${e.message}`);
288 }
289
290 // register the local repositories
291 localRepos.forEach((repo) => {
292 this._project.registerGitRepository(repo.repoPath, repo.gitUrl);
293 });
294 }
295
296 async run() {
297 await this.setup();
298 let buildStartTime;
299 let buildMessage;
300 const onBuildStart = async () => {
301 if (this._project.started) {
302 buildMessage = 'Rebuilding project files...';
303 } else {
304 buildMessage = 'Building project files...';
305 }
306 this.log.info(buildMessage);
307 buildStartTime = Date.now();
308 };
309
310 const onBuildEnd = async () => {
311 try {
312 const buildTime = Date.now() - buildStartTime;
313 this.log.info(`${buildMessage}done ${buildTime}ms`);
314 if (this._project.started) {
315 this.emit('build', this);
316 this._project.invalidateCache();
317 return;
318 }
319
320 // ensure correct module paths
321 this._project.withRuntimeModulePaths([...this.modulePaths, ...module.paths]);
322
323 await this._project.start();
324
325 // init source watcher after everything is built
326 this._initSourceWatcher(async (files) => {
327 const dirtyConfigFiles = Object.keys(files || {})
328 .filter((f) => f === HELIX_CONFIG || f === HELIX_QUERY || f === GIT_HEAD);
329 if (dirtyConfigFiles.length) {
330 this.log.info(`${dirtyConfigFiles.join(', ')} modified. Restarting dev server...`);
331 await this._project.stop();
332 await this.reloadConfig();
333 await this.setup();
334 // ensure correct module paths
335 this._project.withRuntimeModulePaths([...this.modulePaths, ...module.paths]);
336 await this._project.start();
337 if (Object.keys(files).length === 1) {
338 return Promise.resolve();
339 }
340 }
341 return this.build();
342 });
343
344 this.emit('started', this);
345 if (this._open) {
346 opn(`http://localhost:${this._project.server.port}/`, { url: true });
347 }
348 if (!await this.config.hasFile() && !await this.helixPages.isPagesProject()) {
349 this.log.info(chalk`{green Note:}
350The project does not have a {cyan helix-config.yaml} which is necessary to
351access remote content and to deploy helix. Consider running
352{gray hlx up --save-config} to generate a default config.`);
353 }
354 } catch (e) {
355 this.log.error(`Error: ${e.message}`);
356 await this.stop();
357 }
358 };
359
360 this.on('buildStart', onBuildStart);
361 this.on('buildEnd', onBuildEnd);
362
363 this.build();
364 }
365}
366
367module.exports = UpCommand;