Phoenix framework - execute NPM script in subdirectory

Viewed 702

In my Phoenix app, I am running a React frontend that sits in an assets/ directory within the app... it has it's own package.json file, and I would like to be able to run aliased NPM scripts from the Phoenix app's mix.exs file. I've been experimenting with Elixir's System.cmd(), but it doesn't seem to be doing the trick.

Basically, I've set up a lint command for my frontend - in assets/package.json, I've got

"lint": "eslint ./*.js ./js/*.js || exit 0"

and running $ npm run lint from the assets/ directory works as expected... but it would be smashing to just be able to run the command from the app's top-level.

In my mix.exs file, I was experimenting with an alias like so:

defp aliases do
  [
    "lint": [System.cmd("npm", ["run lint"], cd: "assets")
  ]
end

but running mix lint produces a pretty wordy error:

** (FunctionClauseError) no function clause matching in Mix.Task.run_alias/3    

The following arguments were given to Mix.Task.run_alias/3:

    # 1
    [{"\nUsage: npm <command>\n\nwhere <command> is one of:\n    access, adduser, bin, bugs, c, cache, completion, config,\n    ddp, dedupe, deprecate, dist-tag, docs, edit, explore, get,\n    help, help-search, i, init, install, install-test, it, link,\n    list, ln, login, logout, ls, outdated, owner, pack, ping,\n    prefix, prune, publish, rb, rebuild, repo, restart, root,\n    run, run-script, s, se, search, set, shrinkwrap, star,\n    stars, start, stop, t, tag, team, test, tst, un, uninstall,\n    unpublish, unstar, up, update, v, version, view, whoami\n\nnpm <cmd> -h     quick help on <cmd>\nnpm -l           display full usage info\nnpm help <term>  search for help on <term>\nnpm help npm     involved overview\n\nSpecify configs in the ini-formatted file:\n    /Users/user/.npmrc\nor on the command line via: npm <command> --key value\nConfig info can be viewed via: npm help config\n\nnpm@3.10.10 /Users/user/.nvm/versions/node/v6.11.1/lib/node_modules/npm\n", 1}]

    # 2
    []

    # 3
    :ok

Attempted function clauses (showing 3 out of 3):

    defp run_alias([h | t], alias_args, _res) when is_binary(h)
    defp run_alias([h | t], alias_args, _res) when is_function(h, 1)
    defp run_alias([], _alias_task, res)

(mix) lib/mix/task.ex:331: Mix.Task.run_alias/3
(mix) lib/mix/task.ex:266: Mix.Task.run/2
(mix) lib/mix/cli.ex:75: Mix.CLI.run_task/2
(elixir) lib/code.ex:376: Code.require_file/2

Ok so obviously I'm doing something wrong. Is it possible to execute an NPM command on a sub-directory of a Phoenix app?

3 Answers

Firstly, run and lint should be separate strings in the arguments list.

Secondly, the code you have will immediately run the command instead of when the alias is invoked.

You can use a "run -e ..." alias to execute arbitrary code in an alias like this:

"lint": [~s|run -e 'System.cmd("npm", ["run", "lint"], cd: "assets")'|)

(I'm using ~s|| syntax just to make it easier to type a string with both single and double quotes.)

An alternative to the ~s|| notation with a cd, we can also use the --prefix flag on the npm command.

lintjs: ["cmd npm run format:ci --prefix assets"]

If using an umbrella project, define the alias in the app where it runs, and add an alias in the project root.

<root>/mix.exs

defp aliases do
    [
      # run `mix setup` in all child apps
      setup: ["cmd mix setup"],
      # app-specific alias defined in onstage_web/mix.exs
      lint: ["cmd --app myapp_name mix lintjs"]
    ]
end

<root>/apps/myapp_name/mix.exs

defp aliases do
    [
      # run npm install in child app
      setup: ["deps.get", "cmd npm install --prefix assets"],,
      # simplified/equivalent of accepted answer
      lintjs: ["cmd npm run format:ci --prefix assets"]
    ]
end

Now, we can run mix lint from the project root to lint the JS in a child app.

Also, remember to run mix compile after making changes.

Related