1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const path = require('path');
|
14 | const fse = require('fs-extra');
|
15 | const opn = require('open');
|
16 | const chokidar = require('chokidar');
|
17 | const chalk = require('chalk');
|
18 | const { HelixProject } = require('@adobe/helix-simulator');
|
19 | const { GitUrl } = require('@adobe/helix-shared');
|
20 | const GitUtils = require('./git-utils.js');
|
21 | const BuildCommand = require('./build.cmd');
|
22 | const pkgJson = require('../package.json');
|
23 |
|
24 | const HELIX_CONFIG = 'helix-config.yaml';
|
25 | const HELIX_QUERY = 'helix-query.yaml';
|
26 | const GIT_HEAD = '.git/HEAD';
|
27 |
|
28 | class 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 |
|
122 | }
|
123 | this._project = null;
|
124 | }
|
125 | this.log.info('Helix project stopped.');
|
126 | this.emit('stopped', this);
|
127 | }
|
128 |
|
129 | |
130 |
|
131 |
|
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 |
|
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 |
|
165 | if (!await fse.pathExists(path.join(this.directory, '.git'))) {
|
166 | throw Error('hlx up needs local git repository.');
|
167 | }
|
168 |
|
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 |
|
183 | if (!hasConfigFile && this._localRepos.length === 0) {
|
184 | if (!this.config.strains.get('default').content.isLocal) {
|
185 |
|
186 | this._localRepos.push('.');
|
187 | }
|
188 | }
|
189 |
|
190 |
|
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 |
|
210 | if (this._githubToken && !this._devDefault.GITHUB_TOKEN) {
|
211 | this._devDefault.GITHUB_TOKEN = this._githubToken;
|
212 | }
|
213 |
|
214 |
|
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 |
|
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 |
|
239 | this._project.withSourceDir(this.helixPages.srcDirectory);
|
240 |
|
241 |
|
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 |
|
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 |
|
268 |
|
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 |
|
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 |
|
321 | this._project.withRuntimeModulePaths([...this.modulePaths, ...module.paths]);
|
322 |
|
323 | await this._project.start();
|
324 |
|
325 |
|
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 |
|
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:}
|
350 | The project does not have a {cyan helix-config.yaml} which is necessary to
|
351 | access 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 |
|
367 | module.exports = UpCommand;
|