Guest User

Untitled

a guest
Aug 14th, 2018
136
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.32 KB | None | 0 0
  1. // @flow
  2. import { ipcRenderer } from 'electron';
  3. import * as childProcess from 'child_process';
  4. import {
  5. RUN_TASK,
  6. ABORT_TASK,
  7. COMPLETE_TASK,
  8. LAUNCH_DEV_SERVER,
  9. completeTask,
  10. attachTaskMetadata,
  11. receiveDataFromTaskExecution,
  12. loadDependencyInfoFromDisk,
  13. } from '../actions';
  14. import { getProjectById } from '../reducers/projects.reducer';
  15. import { getPathForProjectId } from '../reducers/paths.reducer';
  16. import { isDevServerTask } from '../reducers/tasks.reducer';
  17. import findAvailablePort from '../services/find-available-port.service';
  18. import { isWin, getPathForPlatform } from '../services/platform.services';
  19.  
  20. import type { Task, ProjectType } from '../types';
  21. import { PACKAGE_MANAGER_CMD } from '../services/platform.services';
  22.  
  23. export default (store: any) => (next: any) => (action: any) => {
  24. if (!action.task) {
  25. return next(action);
  26. }
  27.  
  28. const { task } = action;
  29.  
  30. const state = store.getState();
  31.  
  32. const project = getProjectById(state, task.projectId);
  33. const projectPath = getPathForProjectId(state, task.projectId);
  34.  
  35. // eslint-disable-next-line default-case
  36. switch (action.type) {
  37. case LAUNCH_DEV_SERVER: {
  38. findAvailablePort()
  39. .then(port => {
  40. const { args, env } = getDevServerCommand(task, project.type, port);
  41.  
  42. const child = childProcess.spawn(PACKAGE_MANAGER_CMD, args, {
  43. cwd: projectPath,
  44. env: {
  45. ...window.process.env,
  46. ...env,
  47. },
  48. });
  49.  
  50. // Now that we have a port/processId for the server, attach it to
  51. // the task. The port is used for opening the app, the pid is used
  52. // to kill the process
  53. next(attachTaskMetadata(task, child.pid, port));
  54.  
  55. ipcRenderer.send('addProcessId', child.pid);
  56.  
  57. child.stdout.on('data', data => {
  58. // Ok so, unfortunately, failure-to-compile is still pushed
  59. // through stdout, not stderr. We want that message specifically
  60. // to trigger an error state, and so we need to parse it.
  61. const text = data.toString();
  62.  
  63. const isError = text.includes('Failed to compile.');
  64.  
  65. next(receiveDataFromTaskExecution(task, text, isError));
  66. });
  67.  
  68. child.stderr.on('data', data => {
  69. next(receiveDataFromTaskExecution(task, data.toString()));
  70. });
  71.  
  72. child.on('exit', code => {
  73. // For Windows Support
  74. // Windows sends code 1 (I guess its because we foce kill??)
  75. const successfulCode = isWin() ? 1 : 0;
  76. const wasSuccessful = code === successfulCode || code === null;
  77. const timestamp = new Date();
  78.  
  79. store.dispatch(completeTask(task, timestamp, wasSuccessful));
  80. });
  81. })
  82. .catch(err => {
  83. // TODO: Error handling (this can happen if the first 15 ports are
  84. // occupied, or if there's some generic Node error)
  85. console.error(err);
  86. });
  87.  
  88. break;
  89. }
  90.  
  91. // TODO: As tasks start to get more customized for the project types,
  92. // it probably makes sense to have separate actions (eg. RUN_TESTS,
  93. // BUILD_FOR_PRODUCTION), and use RUN_TASK just for user-added tasks.
  94. case RUN_TASK: {
  95. const { projectId, name } = action.task;
  96.  
  97. const project = getProjectById(store.getState(), projectId);
  98.  
  99. // TEMPORARY HACK
  100. // By default, create-react-app runs tests in interactive watch mode.
  101. // This is a brilliant way to do it, but it's interactive, which won't
  102. // work as-is.
  103. // In the future, I expect "Tests" to get its own module on the project
  104. // page, in which case we can support the interactive mode, except with
  105. // descriptive buttons instead of cryptic letters!
  106. // Alas, this would be mucho work, and this is an MVP. So for now, I'm
  107. // disabling watch mode, and doing "just run all the tests once" mode.
  108. // This is bad, and I feel bad, but it's a corner that needs to be cut,
  109. // for now.
  110. const additionalArgs = [];
  111. if (project.type === 'create-react-app' && name === 'test') {
  112. additionalArgs.push('--', '--coverage');
  113. }
  114.  
  115. /* Bypasses 'Are you sure?' check when ejecting CRA
  116. */
  117. const child = childProcess.spawn(
  118. PACKAGE_MANAGER_CMD,
  119. ['run', name, ...additionalArgs],
  120. {
  121. cwd: projectPath,
  122. env: {
  123. ...window.process.env,
  124. },
  125. }
  126. );
  127.  
  128. // When this application exits, we want to kill this process.
  129. // Send it up to the main process.
  130. ipcRenderer.send('addProcessId', child.pid);
  131.  
  132. // TODO: Does the renderer process still need to know about the child
  133. // processId?
  134. next(attachTaskMetadata(task, child.pid));
  135.  
  136. child.stdout.on('data', data => {
  137. // The 'eject' task prompts the user, to ask if they're sure.
  138. // We can bypass this prompt, as our UI already has an alert that
  139. // confirms this action.
  140. // TODO: Eject deserves its own Redux action, to avoid cluttering up
  141. // this generic "RUN_TASK" action.
  142. // TODO: Is there a way to "future-proof" this, in case the CRA
  143. // confirmation copy changes?
  144. const isEjectPrompt = data
  145. .toString()
  146. .includes('Are you sure you want to eject? This action is permanent');
  147.  
  148. if (isEjectPrompt) {
  149. sendCommandToProcess(child, 'y');
  150. }
  151. next(receiveDataFromTaskExecution(task, data.toString()));
  152. });
  153.  
  154. child.stderr.on('data', data => {
  155. next(receiveDataFromTaskExecution(task, data.toString()));
  156. });
  157.  
  158. child.on('exit', code => {
  159. const timestamp = new Date();
  160.  
  161. store.dispatch(completeTask(task, timestamp, code === 0));
  162.  
  163. if (task.name === 'eject') {
  164. store.dispatch(loadDependencyInfoFromDisk(project.id, project.path));
  165. }
  166. });
  167.  
  168. break;
  169. }
  170.  
  171. case ABORT_TASK: {
  172. const { task } = action;
  173. const { processId, name } = task;
  174.  
  175. childProcess.spawn('kill', ['-9', processId]);
  176.  
  177. ipcRenderer.send('removeProcessId', processId);
  178.  
  179. // Once the task is killed, we should dispatch a notification
  180. // so that the terminal shows something about this update.
  181. // My initial thought was that all tasks would have the same message,
  182. // but given that we're treating `start` as its own special thing,
  183. // I'm realizing that it should vary depending on the task type.
  184. // TODO: Find a better place for this to live.
  185. const abortMessage = isDevServerTask(name)
  186. ? 'Server stopped'
  187. : 'Task aborted';
  188.  
  189. next(
  190. receiveDataFromTaskExecution(
  191. task,
  192. `\u001b[31;1m${abortMessage}\u001b[0m`
  193. )
  194. );
  195.  
  196. break;
  197. }
  198.  
  199. case COMPLETE_TASK: {
  200. const { task } = action;
  201.  
  202. // Send a message to add info to the terminal about the task being done.
  203. // TODO: ASCII fish art?
  204.  
  205. const message = 'Task completed';
  206.  
  207. next(
  208. receiveDataFromTaskExecution(task, `\u001b[32;1m${message}\u001b[0m`)
  209. );
  210.  
  211. if (task.processId) {
  212. ipcRenderer.send('removeProcessId', task.processId);
  213. }
  214.  
  215. // The `eject` task is special; after running it, its dependencies will
  216. // have changed.
  217. // TODO: We should really have a `EJECT_PROJECT_COMPLETE` action that does
  218. // this instead.
  219. if (task.name === 'eject') {
  220. const project = getProjectById(store.getState(), task.projectId);
  221.  
  222. store.dispatch(loadDependencyInfoFromDisk(project.id, project.path));
  223. }
  224.  
  225. break;
  226. }
  227. }
  228.  
  229. // Pass all actions through, unless the function returns early (which happens
  230. // when deferring the 'eject' task)
  231. return next(action);
  232. };
  233.  
  234. const getDevServerCommand = (
  235. task: Task,
  236. projectType: ProjectType,
  237. port: string
  238. ) => {
  239. switch (projectType) {
  240. case 'create-react-app':
  241. return {
  242. args: ['run', task.name],
  243. env: {
  244. PORT: port,
  245. },
  246. };
  247. case 'gatsby':
  248. return {
  249. args: ['run', task.name, '--', `-p ${port}`],
  250. env: {},
  251. };
  252. default:
  253. throw new Error('Unrecognized project type: ' + projectType);
  254. }
  255. };
  256.  
  257. const sendCommandToProcess = (child: any, command: string) => {
  258. // Commands have to be suffixed with '\n' to signal that the command is
  259. // ready to be sent. Same as a regular command + hitting the enter key.
  260. child.stdin.write(`${command}\n`);
  261. };
Add Comment
Please, Sign In to add comment