index.js 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { invariant } from '@epic-web/invariant';
  2. import { spawn } from 'cross-spawn';
  3. import { commandConvert } from "./command.js";
  4. import { varValueConvert } from "./variable.js";
  5. const envSetterRegex = /(\w+)=('(.*)'|"(.*)"|(.*))/;
  6. export function crossEnv(args, options = {}) {
  7. const [envSetters, command, commandArgs] = parseCommand(args);
  8. const env = getEnvVars(envSetters);
  9. if (command) {
  10. const spawnOptions = {
  11. stdio: 'inherit',
  12. shell: options.shell,
  13. env,
  14. };
  15. const proc = spawn(
  16. // run `path.normalize` for command(on windows)
  17. commandConvert(command, env, true),
  18. // by default normalize is `false`, so not run for cmd args
  19. commandArgs.map((arg) => commandConvert(arg, env)), spawnOptions);
  20. process.on('SIGTERM', () => proc.kill('SIGTERM'));
  21. process.on('SIGINT', () => proc.kill('SIGINT'));
  22. process.on('SIGBREAK', () => proc.kill('SIGBREAK'));
  23. process.on('SIGHUP', () => proc.kill('SIGHUP'));
  24. proc.on('exit', (code, signal) => {
  25. let crossEnvExitCode = code;
  26. // exit code could be null when OS kills the process(out of memory, etc) or due to node handling it
  27. // but if the signal is SIGINT the user exited the process so we want exit code 0
  28. if (crossEnvExitCode === null) {
  29. crossEnvExitCode = signal === 'SIGINT' ? 0 : 1;
  30. }
  31. process.exit(crossEnvExitCode);
  32. });
  33. return proc;
  34. }
  35. return null;
  36. }
  37. function parseCommand(args) {
  38. const envSetters = {};
  39. let command = null;
  40. let commandArgs = [];
  41. for (let i = 0; i < args.length; i++) {
  42. const arg = args[i];
  43. if (!arg)
  44. continue;
  45. const match = envSetterRegex.exec(arg);
  46. if (match && match[1]) {
  47. let value;
  48. if (typeof match[3] !== 'undefined') {
  49. value = match[3];
  50. }
  51. else if (typeof match[4] === 'undefined') {
  52. value = match[5] || '';
  53. }
  54. else {
  55. value = match[4];
  56. }
  57. envSetters[match[1]] = value;
  58. }
  59. else {
  60. // No more env setters, the rest of the line must be the command and args
  61. const cStart = args
  62. .slice(i)
  63. // Regex:
  64. // match "\'" or "'"
  65. // or match "\" if followed by [$"\] (lookahead)
  66. .map((a) => {
  67. const re = /\\\\|(\\)?'|([\\])(?=[$"\\])/g;
  68. // Eliminate all matches except for "\'" => "'"
  69. return a.replace(re, (m) => {
  70. if (m === '\\\\')
  71. return '\\';
  72. if (m === "\\'")
  73. return "'";
  74. return '';
  75. });
  76. });
  77. const parsedCommand = cStart[0];
  78. invariant(parsedCommand, 'Command is required');
  79. command = parsedCommand;
  80. commandArgs = cStart.slice(1).filter(Boolean);
  81. break;
  82. }
  83. }
  84. return [envSetters, command, commandArgs];
  85. }
  86. function getEnvVars(envSetters) {
  87. const envVars = { ...process.env };
  88. if (process.env.APPDATA) {
  89. envVars.APPDATA = process.env.APPDATA;
  90. }
  91. Object.keys(envSetters).forEach((varName) => {
  92. const value = envSetters[varName];
  93. if (value !== undefined) {
  94. envVars[varName] = varValueConvert(value, varName);
  95. }
  96. });
  97. return envVars;
  98. }