UNPKG

7.33 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2017, Shopgate, Inc. All rights reserved.
3 *
4 * This source code is licensed under the Apache 2.0 license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7const fsEx = require('fs-extra')
8const path = require('path')
9const inquirer = require('inquirer')
10const logger = require('../logger')
11const FrontendProcess = require('../app/frontend/FrontendProcess')
12const FrontendSetup = require('../app/frontend/FrontendSetup')
13const utils = require('../utils/utils')
14const ExtensionConfigWatcher = require('../app/ExtensionConfigWatcher')
15const { extensionConfigChanged } = require('../utils/EventHandler')
16
17const { SETTINGS_FOLDER, THEMES_FOLDER } = require('../app/Constants')
18
19/**
20 * The FrontendAction class.
21 */
22class FrontendAction {
23 /**
24 * @param {AppSettings} appSettings
25 * @param {UserSettings} userSettings
26 * @param {DcHttpClient} dcHttpClient
27 */
28 constructor (appSettings, userSettings, dcHttpClient) {
29 this.appSettings = appSettings
30 this.userSettings = userSettings
31 this.settingsFolder = path.join(this.appSettings.getApplicationFolder(), SETTINGS_FOLDER)
32 this.dcHttpClient = dcHttpClient
33 this.frontendSetup = new FrontendSetup(this.dcHttpClient, this.appSettings)
34 this.extensionConfigWatcher = new ExtensionConfigWatcher(this.appSettings)
35 }
36
37 /**
38 * Registers the frontend command.
39 * @param {Command} caporal Instance of the commander module.
40 * @param {AppSettings} appSettings
41 * @param {UserSettings} userSettings
42 * @param {DcHttpClient} dcHttpClient
43 */
44 static register (caporal, appSettings, userSettings, dcHttpClient) {
45 caporal
46 .command('frontend start')
47 .description('Starts the webpack dev server for the frontend development')
48 .option('-t, --theme [value]', 'The name of the theme that you want to be compiled by webpack.')
49 .option('-h, --host [value]', 'The URL or IP address of your local development environment.')
50 .option('-p, --port [value]', 'The port to use for accessing the output via browser.')
51 .option('-a, --analyze', 'Whether to analyze the bundle sizes.')
52 .action(async (args, options) => {
53 try {
54 await new FrontendAction(appSettings, userSettings, dcHttpClient).run('start', args, options)
55 } catch (err) {
56 // istanbul ignore next
57 logger.error(err.message)
58 process.exit(1)
59 }
60 })
61
62 caporal
63 .command('frontend setup')
64 .description('Changes the settings for the frontend development')
65 .action(async (args, options) => {
66 try {
67 await new FrontendAction(appSettings, userSettings, dcHttpClient).run('setup', args, options)
68 } catch (err) /* istanbul ignore next */ {
69 logger.error(err.message)
70 process.exit(1)
71 }
72 })
73 }
74
75 /**
76 * Inquires a theme selection if not theme was set as an option.
77 * @return {Promise}
78 */
79 requestThemeOption () {
80 return new Promise(async (resolve, reject) => {
81 const themes = await utils.findThemes(this.appSettings)
82 if (themes.length === 1) return resolve(themes[0])
83
84 inquirer
85 .prompt([{
86 type: 'list',
87 name: 'theme',
88 message: 'Please choose a theme to use',
89 choices: themes
90 }])
91 .then(answers => resolve(answers.theme))
92 .catch(error => reject(error))
93 })
94 }
95
96 /**
97 * Runs the frontend process.
98 * @param {string} action The action to perform.
99 * @param {Object} [args={}] The process args.
100 * @param {Object} [options={}] The process options.
101 */
102 async run (action, args, options = {}) {
103 await this.userSettings.validate()
104 await this.appSettings.validate()
105
106 const pid = await utils.getProcessId('frontend', this.settingsFolder)
107 if (pid) throw new Error(`Frontend process is already running with pid: ${pid}. Please quit this process first.`)
108
109 switch (action) {
110 default:
111 case 'start': {
112 await utils.generateComponentsJson(this.appSettings)
113 await utils.writeExtensionConfigs(this.appSettings, this.dcHttpClient)
114
115 let theme = options.theme
116 if (!theme) theme = await this.requestThemeOption()
117 const frontendSettings = this.appSettings.getFrontendSettings()
118 await this.setStartPage(
119 await this.appSettings.getId(),
120 await frontendSettings.getIpAddress(),
121 await frontendSettings.getPort()
122 )
123 await this.start({ ...options, theme }, await this.buildThemePath(theme))
124 break
125 }
126 case 'setup': {
127 const showMessage = process.env.INTEGRATION_TEST !== 'true'
128 await this.frontendSetup.run(showMessage)
129 break
130 }
131 }
132 }
133
134 /**
135 * Builds the theme folder path.
136 * @param {string} theme The theme folder.
137 * @return {string}
138 */
139 async buildThemePath (theme) {
140 const themePath = path.join(this.appSettings.getApplicationFolder(), THEMES_FOLDER, theme)
141 if (!await fsEx.exists(themePath)) {
142 throw new Error(`Can't find theme '${theme}'. Please make sure you passed the right theme.`)
143 }
144 return themePath
145 }
146
147 /**
148 * @param {string} appId
149 * @param {string} address
150 * @param {number} port
151 */
152 async setStartPage (appId, address, port) {
153 await this.dcHttpClient.setStartPageUrl(appId, `http://${address}:${port}`)
154 }
155
156 /**
157 * @param {string} appId
158 */
159 async resetStartPage (appId) {
160 await this.dcHttpClient.setStartPageUrl(appId, '')
161 }
162
163 /**
164 * Runs the 'start' command.
165 * @param {Object} options The process options.
166 * @param {string} themeFolder The theme folder.
167 */
168 async start (options, themeFolder) {
169 const frontend = new FrontendProcess(options, this.frontendSetup, this.appSettings)
170
171 process.on('SIGINT', async () => {
172 try {
173 frontend.stop()
174 await this.extensionConfigWatcher.stop()
175 await utils.deleteProcessFile('frontend', this.settingsFolder)
176 await this.resetStartPage(await this.appSettings.getId())
177 process.exit(0)
178 } catch (err) {
179 logger.error(err.message)
180 process.exit(err.code)
181 }
182 })
183
184 await this.extensionConfigWatcher.start('frontend')
185 this.extensionConfigWatcher.on('configChange', (config) => extensionConfigChanged(config, this.appSettings, this.dcHttpClient))
186 await this.updateThemeConfig(themeFolder)
187 await frontend.run()
188 await utils.setProcessFile('frontend', this.settingsFolder, process.pid)
189 }
190
191 async updateThemeConfig (templateFolder) {
192 logger.info(`Generating theme config`)
193 const extensionConfigFile = await fsEx.readJSON(path.join(templateFolder, 'extension-config.json'))
194
195 return this.dcHttpClient.generateExtensionConfig(extensionConfigFile, await this.appSettings.getId())
196 .then((extConfig) => {
197 if (!extConfig.frontend) return logger.warn('No config with the destination \'frontend\' found')
198 const appJsonFile = path.join(templateFolder, 'config', 'app.json')
199 return fsEx.outputJson(appJsonFile, extConfig.frontend, { spaces: 2 })
200 })
201 .then(() => {
202 logger.info(`Updated theme config`)
203 })
204 .catch(err => {
205 throw new Error('Could not generate config: ' + err.message)
206 })
207 }
208}
209
210module.exports = FrontendAction