Home Manual Reference Source

src/services/cli/cliSHBuild.js

  1. const { provider } = require('jimple');
  2. const CLICommand = require('../../abstracts/cliCommand');
  3. /**
  4. * This is the _'real build command'_. This is a private command the shell script executes in order
  5. * to get a list of commands to run.
  6. * @extends {CLICommand}
  7. * @todo This whole class needs a refactor (args and params are the same!).
  8. */
  9. class CLISHBuildCommand extends CLICommand {
  10. /**
  11. * Class constructor.
  12. * @param {Builder} builder Needed to generate a target
  13. * build command.
  14. * @param {CLICleanCommand} cliCleanCommand Needed to generate the command
  15. * that cleans a target files.
  16. * @param {CLICopyProjectFilesCommand} cliCopyProjectFilesCommand Needed to generate the command
  17. * to copy the project files if
  18. * the feature of copying on
  19. * build is enabled.
  20. * @param {CLIRevisionCommand} cliRevisionCommand Needed to generate the command
  21. * that creates the revision file
  22. * if the feature of generating
  23. * it on build is enabled.
  24. * @param {CLISHCopyCommand} cliSHCopyCommand Needed to generate the command
  25. * to copy the target files if
  26. * the target doesn't require
  27. * bundling.
  28. * @param {CLISHNodeRunCommand} cliSHNodeRunCommand Needed to generate the command
  29. * to run a Node target if the
  30. * `run` option is used.
  31. * @param {CLISHNodeWatchCommand} cliSHNodeWatchCommand Needed to generate the command
  32. * to watch a Node target files
  33. * if the `watch` option is used.
  34. * @param {CLISHTranspileCommand} cliSHTranspileCommand Needed to generate the command
  35. * to transpile a Node target
  36. * code.
  37. * @param {Events} events Used to reduce the list of
  38. * commands generated.
  39. * @param {ProjectConfigurationSettings} projectConfiguration Used to read and validate the
  40. * features.
  41. * @param {Targets} targets Used to get the targets
  42. * information.
  43. */
  44. constructor(
  45. builder,
  46. buildTypeScriptHelper,
  47. cliCleanCommand,
  48. cliCopyProjectFilesCommand,
  49. cliRevisionCommand,
  50. cliSHCopyCommand,
  51. cliSHNodeRunCommand,
  52. cliSHNodeWatchCommand,
  53. cliSHTranspileCommand,
  54. events,
  55. projectConfiguration,
  56. targets
  57. ) {
  58. super();
  59. /**
  60. * A local reference for the `builder` service.
  61. * @type {Builder}
  62. */
  63. this.builder = builder;
  64. /**
  65. * A local reference for the `buildTypeScriptHelper` service.
  66. * @type {BuildTypeScriptHelper}
  67. */
  68. this.buildTypeScriptHelper = buildTypeScriptHelper;
  69. /**
  70. * A local reference for the `cliCleanCommand` service.
  71. * @type {CliCleanCommand}
  72. */
  73. this.cliCleanCommand = cliCleanCommand;
  74. /**
  75. * A local reference for the `cliCopyProjectFilesCommand` service.
  76. * @type {CliCopyProjectFilesCommand}
  77. */
  78. this.cliCopyProjectFilesCommand = cliCopyProjectFilesCommand;
  79. /**
  80. * A local reference for the `cliRevisionCommand` service.
  81. * @type {CliRevisionCommand}
  82. */
  83. this.cliRevisionCommand = cliRevisionCommand;
  84. /**
  85. * A local reference for the `cliSHCopyCommand` service.
  86. * @type {CliSHCopyCommand}
  87. */
  88. this.cliSHCopyCommand = cliSHCopyCommand;
  89. /**
  90. * A local reference for the `cliSHNodeRunCommand` service.
  91. * @type {CliSHNodeRunCommand}
  92. */
  93. this.cliSHNodeRunCommand = cliSHNodeRunCommand;
  94. /**
  95. * A local reference for the `cliSHNodeWatchCommand` service.
  96. * @type {CliSHNodeWatchCommand}
  97. */
  98. this.cliSHNodeWatchCommand = cliSHNodeWatchCommand;
  99. /**
  100. * A local reference for the `cliSHTranspileCommand` service.
  101. * @type {CliSHTranspileCommand}
  102. */
  103. this.cliSHTranspileCommand = cliSHTranspileCommand;
  104. /**
  105. * A local reference for the `events` service.
  106. * @type {Events}
  107. */
  108. this.events = events;
  109. /**
  110. * All the project settings.
  111. * @type {ProjectConfigurationSettings}
  112. */
  113. this.projectConfiguration = projectConfiguration;
  114. /**
  115. * A local reference for the `targets` service.
  116. * @type {Targets}
  117. */
  118. this.targets = targets;
  119. /**
  120. * The instruction needed to trigger the command.
  121. * @type {string}
  122. */
  123. this.command = 'sh-build [target]';
  124. /**
  125. * A description of the command, just to follow the interface as the command won't show up on
  126. * the help interface.
  127. * @type {string}
  128. */
  129. this.description = 'Get the build commands for the shell program to execute';
  130. this.addOption(
  131. 'type',
  132. '-t, --type [type]',
  133. 'Which build type: development (default) or production',
  134. 'development'
  135. );
  136. this.addOption(
  137. 'run',
  138. '-r, --run',
  139. 'Run the target after the build is completed. It only works when the ' +
  140. 'build type is development',
  141. false
  142. );
  143. this.addOption(
  144. 'watch',
  145. '-w, --watch',
  146. 'Rebuild the target every time one of its files changes. It only works ' +
  147. 'when the build type is development',
  148. false
  149. );
  150. this.addOption(
  151. 'inspect',
  152. '-i, --inspect',
  153. 'Enables the Node inspector. It only works when running Node targets',
  154. false
  155. );
  156. this.addOption(
  157. 'analyze',
  158. '-a, --analyze',
  159. 'Enables the bundle analyzer. It only works with targets with bundling',
  160. false
  161. );
  162. /**
  163. * Hide the command from the help interface.
  164. * @type {boolean}
  165. */
  166. this.hidden = true;
  167. /**
  168. * Enable unknown options so other services can customize the build command.
  169. * @type {boolean}
  170. */
  171. this.allowUnknownOptions = true;
  172. }
  173. /**
  174. * Handle the execution of the command and outputs the list of commands to run.
  175. * This method emits the event reducer `build-target-commands-list` with the list of commands,
  176. * the target information, the type of build and whether or not the target should be executed;
  177. * and it expects a list of commands on return.
  178. * @param {?string} name The name of the target.
  179. * @param {Command} command The executed command (sent by `commander`).
  180. * @param {CLIBuildCommandOptions} options The command options.
  181. * @param {Object} unknownOptions A dictionary of extra options that command may
  182. * have received.
  183. */
  184. handle(name, command, options, unknownOptions) {
  185. const { type } = options;
  186. // Get the target information
  187. const target = name ?
  188. // If the target doesn't exist, this will throw an error.
  189. this.targets.getTarget(name) :
  190. // Get the default target or throw an error if the project doesn't have targets.
  191. this.targets.getDefaultTarget();
  192.  
  193. const {
  194. development,
  195. analyze,
  196. run,
  197. inspect,
  198. watch,
  199. } = this._normalizeOptions(options, target);
  200. /**
  201. * Check whether or not a build will be created. This is always `true` for browser targets, but
  202. * it can be `false` for Node targets if bundling and transpiling is disabled.
  203. */
  204. let build = true;
  205. if (target.is.node) {
  206. build = !development || target.bundle || target.transpile;
  207. }
  208. // Define the parameters object to send to the other methods.
  209. const params = {
  210. target,
  211. type,
  212. run,
  213. build,
  214. watch,
  215. inspect,
  216. analyze,
  217. };
  218.  
  219. // Based on the target type, get the list of commands.
  220. const commands = target.is.node ?
  221. this._getCommandsForNodeTarget(params) :
  222. this._getCommandsForBrowserTarget(params);
  223. // Reduce the list of commands.
  224. const output = this.events.reduce(
  225. 'build-target-commands-list',
  226. commands.filter((cmd) => !!cmd),
  227. params,
  228. unknownOptions
  229. )
  230. // Join the commands on a single string.
  231. .join(';');
  232. // Outputs all the commands
  233. this.output(output);
  234. }
  235. /**
  236. * Normalizes the options received by the command in order to resolve "impossible combinations",
  237. * like trying to analyze a target that is not for bundling or trying to inspect a browser
  238. * target.
  239. * @param {CLIBuildCommandOptions} options The command options.
  240. * @param {Target} target The target information.
  241. * @return {CLIBuildCommandNormalizedOptions}
  242. * @access protected
  243. * @ignore
  244. */
  245. _normalizeOptions(options, target) {
  246. const development = options.type === 'development';
  247. // Check if there's a reason to analyze the target bundle.
  248. const analyze = options.analyze && (target.is.browser || target.bundle);
  249. // Check if there's a reason for the target to be executed.
  250. const run = !analyze && development && (target.runOnDevelopment || options.run);
  251. // Check if there's a reason for the Node inspector to be enabled.
  252. const inspect = run && target.is.node && (target.inspect.enabled || options.inspect);
  253. // Check if the target files should be watched.
  254. const watch = !run && (target.watch[options.type] || options.watch);
  255.  
  256. return {
  257. development,
  258. analyze,
  259. run,
  260. inspect,
  261. watch,
  262. };
  263. }
  264. /**
  265. * Get the build (and run) commands for a Node target.
  266. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  267. * service needs to run the command: The target
  268. * information, the build type, whether or not the target
  269. * will be executed, etc.
  270. * @return {Array}
  271. * @access protected
  272. * @ignore
  273. */
  274. _getCommandsForNodeTarget(params) {
  275. // Get the base commands.
  276. const commands = [
  277. this._getCleanCommandIfNeeded(params),
  278. this._getBuildCommandIfNeeded(params),
  279. this._getCopyCommandIfNeeded(params),
  280. this._getTranspileCommandIfNeeded(params),
  281. this._getTypeScriptDeclarationsCommandIfNeeded(params),
  282. ];
  283. // If the target won't be executed nor their files will be watched...
  284. if (!params.run && !params.watch) {
  285. // ...push the commands to create the revision file and copy the project files.
  286. commands.push(...[
  287. this._getRevisionCommandIfNeeded(params),
  288. this._getCopyProjectFilesCommand(params),
  289. ]);
  290. } else if (!params.target.bundle) {
  291. /**
  292. * If the target will be executed or their files will be watched, and is not a bundled target,
  293. * push the command to either run or watch. The reason it's handled this ways is because if
  294. * the target is bundled, the build engine will take care of the execution/watch.
  295. */
  296. if (params.run) {
  297. // Run the target with `nodemon`.
  298. commands.push(this._getNodeRunCommand(params));
  299. } else if (params.type === 'production' || params.target.transpile) {
  300. // Watch the target with `watchpack`.
  301. commands.push(this._getNodeWatchCommand(params));
  302. }
  303. }
  304.  
  305. return commands;
  306. }
  307. /**
  308. * Get the build (and run) commands for a browser target.
  309. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  310. * service needs to run the command: The target
  311. * information, the build type, whether or not the target
  312. * will be executed, etc.
  313. * @return {Array}
  314. * @access protected
  315. * @ignore
  316. */
  317. _getCommandsForBrowserTarget(params) {
  318. // Get the base commands.
  319. const commands = [
  320. this._getCleanCommandIfNeeded(params),
  321. this._getBuildCommandIfNeeded(params),
  322. this._getTypeScriptDeclarationsCommandIfNeeded(params),
  323. ];
  324. // If the target won't be executed...
  325. if (!params.run && !params.watch) {
  326. // ...push the commands to create the revision file and copy the project files.
  327. commands.push(...[
  328. this._getRevisionCommandIfNeeded(params),
  329. this._getCopyProjectFilesCommand(params),
  330. ]);
  331. }
  332.  
  333. return commands;
  334. }
  335. /**
  336. * Get the command to remove the previous build files of a target, but only if the target will be
  337. * build, otherwise, it will return an empty string.
  338. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  339. * service needs to run the command: The target
  340. * information, the build type, whether or not the target
  341. * will be executed, etc.
  342. * @return {string}
  343. * @access protected
  344. * @ignore
  345. */
  346. _getCleanCommandIfNeeded(params) {
  347. let command = '';
  348. if (params.build && params.target.cleanBeforeBuild) {
  349. command = this.cliCleanCommand.generate({
  350. target: params.target.name,
  351. });
  352. }
  353.  
  354. return command;
  355. }
  356. /**
  357. * Get the command to actually build a target.
  358. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  359. * service needs to run the command: The target
  360. * information, the build type, whether or not the target
  361. * will be executed, etc.
  362. * @return {string}
  363. * @access protected
  364. * @ignore
  365. */
  366. _getBuildCommandIfNeeded(params) {
  367. return this.builder.getTargetBuildCommand(
  368. params.target,
  369. params.type,
  370. params.run,
  371. params.watch,
  372. params.inspect,
  373. params.analyze
  374. );
  375. }
  376. /**
  377. * Get the command to copy a target files, but only if the target will be _'build'_ (transpiled
  378. * counts) and it doesn't support bundling, otherwise, it will return an empty string.
  379. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  380. * service needs to run the command: The target
  381. * information, the build type, whether or not the target
  382. * will be executed, etc.
  383. * @return {string}
  384. * @access protected
  385. * @ignore
  386. */
  387. _getCopyCommandIfNeeded(params) {
  388. let command = '';
  389. if (params.build && !params.target.bundle) {
  390. command = this.cliSHCopyCommand.generate({
  391. target: params.target.name,
  392. type: params.type,
  393. });
  394. }
  395.  
  396. return command;
  397. }
  398. /**
  399. * Get the command to transpile a target files, but only if the target will be _'build'_
  400. * (transpiled counts) and it doesn't support bundling, otherwise, it will return an empty string.
  401. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  402. * service needs to run the command: The target
  403. * information, the build type, whether or not the target
  404. * will be executed, etc.
  405. * @return {string}
  406. * @access protected
  407. * @ignore
  408. */
  409. _getTranspileCommandIfNeeded(params) {
  410. let command = '';
  411. if (params.build && !params.target.bundle) {
  412. command = this.cliSHTranspileCommand.generate({
  413. target: params.target.name,
  414. type: params.type,
  415. });
  416. }
  417.  
  418. return command;
  419. }
  420. /**
  421. * Get the command to generate a TypeScript target type declarations, but only if the target
  422. * uses TypeScript, won't run and won't be watched: The idea is to generate the declarations only
  423. * when you build the target and not during all the development process.
  424. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  425. * service needs to run the command: The target
  426. * information, the build type, whether or not the target
  427. * will be executed, etc.
  428. * @return {string}
  429. * @access protected
  430. * @ignore
  431. */
  432. _getTypeScriptDeclarationsCommandIfNeeded(params) {
  433. let command = '';
  434. if (
  435. params.target.typeScript &&
  436. params.build &&
  437. !params.run &&
  438. !params.watch
  439. ) {
  440. command = this.buildTypeScriptHelper.getDeclarationsCommand(params.target);
  441. }
  442.  
  443. return command;
  444. }
  445. /**
  446. * Get the command to run a Node target.
  447. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  448. * service needs to run the command: The target
  449. * information, the build type, whether or not the target
  450. * will be executed, etc.
  451. * @return {string}
  452. * @access protected
  453. * @ignore
  454. */
  455. _getNodeRunCommand(params) {
  456. return this.cliSHNodeRunCommand.generate({
  457. target: params.target.name,
  458. inspect: params.inspect,
  459. });
  460. }
  461. /**
  462. * Get the command to watch a Node target.
  463. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  464. * service needs to run the command: The target
  465. * information, the build type, whether or not the target
  466. * will be executed, etc.
  467. * @return {string}
  468. * @access protected
  469. * @ignore
  470. */
  471. _getNodeWatchCommand(params) {
  472. return this.cliSHNodeWatchCommand.generate({
  473. target: params.target.name,
  474. });
  475. }
  476. /**
  477. * Get the command to create the revision file, but only if the feature is enabled, otherwise,
  478. * it will return an empty string.
  479. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  480. * service needs to run the command: The target
  481. * information, the build type, whether or not the target
  482. * will be executed, etc.
  483. * @return {string}
  484. * @access protected
  485. * @ignore
  486. */
  487. _getRevisionCommandIfNeeded(params) {
  488. const {
  489. enabled,
  490. createRevisionOnBuild,
  491. } = this.projectConfiguration.version.revision;
  492. let command = '';
  493. if (enabled && createRevisionOnBuild.enabled) {
  494. const revisionEnvCheck = !createRevisionOnBuild.onlyOnProduction ||
  495. (createRevisionOnBuild.onlyOnProduction && params.type === 'production');
  496. const revisionTargetCheck = !createRevisionOnBuild.targets.length ||
  497. createRevisionOnBuild.targets.includes(params.target.name);
  498.  
  499. if (revisionEnvCheck && revisionTargetCheck) {
  500. command = this.cliRevisionCommand.generate();
  501. }
  502. }
  503.  
  504. return command;
  505. }
  506. /**
  507. * Get the command to copy the project files, but only if the feature is enabled, otherwise,
  508. * it will return an empty string.
  509. * @param {CLIBuildCommandParams} params A dictionary with all the required information the
  510. * service needs to run the command: The target
  511. * information, the build type, whether or not the target
  512. * will be executed, etc.
  513. * @return {string}
  514. * @access protected
  515. * @ignore
  516. */
  517. _getCopyProjectFilesCommand(params) {
  518. const { enabled, copyOnBuild } = this.projectConfiguration.copy;
  519. let command = '';
  520. if (enabled && copyOnBuild.enabled) {
  521. const copyEnvCheck = !copyOnBuild.onlyOnProduction ||
  522. (copyOnBuild.onlyOnProduction && params.type === 'production');
  523. const copyTargetCheck = !copyOnBuild.targets.length ||
  524. copyOnBuild.targets.includes(params.target.name);
  525.  
  526. if (copyEnvCheck && copyTargetCheck) {
  527. command = this.cliCopyProjectFilesCommand.generate();
  528. }
  529. }
  530.  
  531. return command;
  532. }
  533. }
  534. /**
  535. * The service provider that once registered on the app container will set an instance of
  536. * `CLISHBuildCommand` as the `cliSHBuildCommand` service.
  537. * @example
  538. * // Register it on the container
  539. * container.register(cliSHBuildCommand);
  540. * // Getting access to the service instance
  541. * const cliSHBuildCommand = container.get('cliSHBuildCommand');
  542. * @type {Provider}
  543. */
  544. const cliSHBuildCommand = provider((app) => {
  545. app.set('cliSHBuildCommand', () => new CLISHBuildCommand(
  546. app.get('builder'),
  547. app.get('buildTypeScriptHelper'),
  548. app.get('cliCleanCommand'),
  549. app.get('cliCopyProjectFilesCommand'),
  550. app.get('cliRevisionCommand'),
  551. app.get('cliSHCopyCommand'),
  552. app.get('cliSHNodeRunCommand'),
  553. app.get('cliSHNodeWatchCommand'),
  554. app.get('cliSHTranspileCommand'),
  555. app.get('events'),
  556. app.get('projectConfiguration').getConfig(),
  557. app.get('targets')
  558. ));
  559. });
  560.  
  561. module.exports = {
  562. CLISHBuildCommand,
  563. cliSHBuildCommand,
  564. };