heroku-buildpack-nodejs icon indicating copy to clipboard operation
heroku-buildpack-nodejs copied to clipboard

Support running an app from a subdirectory

Open jmorrell opened this issue 8 years ago • 82 comments
trafficstars

There is currently no supported way to run a node app from a directory within your git repo. You can work around it most of the time by using git subtree push but that wouldn't work if you have shared code outside of your directory.

A super common situation is to have the following repo structure:

├── shared-code
├── server
└── frontend-app

Being able to direct the buildpack to look in /server instead of / would be helpful for cases like this.

jmorrell avatar Mar 28 '17 22:03 jmorrell

Exactly what I need. Thanks @jmorrell!

Arrow7000 avatar Mar 29 '17 07:03 Arrow7000

@jmorrell here's a workaround. It ain't perfect but works...

  1. Make sure to add package.json under server and frontend-app. List dependencies (as you normally do) and specify a start script for the server.

  2. Create a root package.json file as follows:

{
  "name": "foobar-app",
  "version": "0.1.0",
  "scripts": {
    "postinstall": "npm install --prefix server && npm install --prefix frontend-app"
  }
}

Notice the --prefix argument which tells npm which subfolder to work on.

  1. Add the following to your procfile:
web: npm start --prefix server

Hope that helps,

jmike avatar Apr 03 '17 08:04 jmike

OK, I just realized you are working for Heroku, so this is probably not a real question, but rather a feature request. Sorry for the misunderstanding.

In any case, I will leave my answer above in case someone finds it useful.

jmike avatar Apr 03 '17 08:04 jmike

@jmike thanks for weighing in. Is this something you've tried yourself and has worked? Because in the absence of an official solution I'll give that a try.

Arrow7000 avatar Apr 03 '17 14:04 Arrow7000

@Arrow7000 yeap, works like a charm. The only problem is caching - see issue #387 even though it's somewhat unrelated to the application structure. See, I am using local files as npm dependencies.

In any case if you face a similar issue you can always disable caching according to heroku instructions https://devcenter.heroku.com/articles/nodejs-support#cache-behavior.

jmike avatar Apr 03 '17 14:04 jmike

Thanks @jmike that worked like a treat! It's not the cleanest solution, because it requires an extra package.json with important options duplicated - eg engines.node - but until we get an official solution this seems like the best and simplest way to get it done!

Arrow7000 avatar Apr 04 '17 10:04 Arrow7000

We had been using git subtree push for a while but started facing the exact issue described above, so we implemented the following custom buildpack https://github.com/Pagedraw/heroku-buildpack-select-subdir

which allows us to deploy multiple apps from the same Heroku repo. Then we just make each of the frontend-app and server require the shared-code as an npm local dependency.

One caveat is that we have to explicitly add the node_modules folder installed to the NODE_PATH so npm knows where to look for requires within shared-code.

It works for us. Let me know if it also works for you!

gablg1 avatar May 30 '17 13:05 gablg1

@jmike I tried your approach and it sadly does not work for me. Here is my package.json

{
  "name": "App",
  "engines": {
    "node": "8.1.x"
  },
  "scripts": {
    "postinstall": "npm install --prefix app/Resources && app/Resources/node_modules/.bin/gulp --gulpfile app/Resources/gulpfile.js"
  }
}

It fails though when trying to run gulp:

remote: -----> Building dependencies
remote:        Installing node modules (package.json)
remote:
remote:        > App@ postinstall /tmp/build_98b4546541f7050670cac9ef04e8ace1
remote:        > npm install --prefix app/Resources && app/Resources/node_modules/.bin/gulp --gulpfile app/Resources/gulpfile.js
remote:
remote:        added 14 packages in 2.164s
remote:        sh: 1: app/Resources/node_modules/.bin/gulp: not found
remote:        npm ERR! file sh
remote:        npm ERR! code ELIFECYCLE
remote:        npm ERR! errno ENOENT
remote:        npm ERR! syscall spawn
remote:        npm ERR! App@ postinstall: `npm install --prefix app/Resources && app/Resources/node_modules/.bin/gulp --gulpfile app/Resources/gulpfile.js`
remote:        npm ERR! spawn ENOENT
remote:        npm ERR!
remote:        npm ERR! Failed at the App@ postinstall script.
remote:        npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
remote:
remote:        npm ERR! A complete log of this run can be found in:
remote:        npm ERR!     /app/.npm/_logs/2017-07-14T10_49_30_508Z-debug.log

Any idea on what I am doing wrong here would be highly appreciated :)

Jan0707 avatar Jul 14 '17 10:07 Jan0707

It appears that if you run the buildpack from within a subdirectory, the commands it's suppose to install are not available after release. My buildpack runs the heroku/php buildpack in a sub directory, but when I heroku run bash, which php returns nothing. Same goes if I cd into the directory.

atomkirk avatar Aug 04 '17 18:08 atomkirk

@atomkirk The buildpacks install their dependencies globally, and I'm not sure what you mean by running the "php buildpack in a sub directory". Could you open a support ticket at https://help.heroku.com/ so we can sort out what's going wrong?

jmorrell avatar Aug 04 '17 19:08 jmorrell

So it looks like they don't install them globally. When I deploy a root directory php app with heroku/php buildpack, then heroku run bash, and which php, its found in /app/.heroku/php/bin/php and echo $PATH is /app/.heroku/php/bin:/app/.heroku/php/sbin:/app/.heroku/php/bin:/app/.heroku/php/sbin:/app/.heroku/php/bin:/app/.heroku/php/sbin:/app/.heroku/php/bin:/usr/local/bin:/usr/bin:/bin:/app/vendor/bin

So this is actually an easy fix, just need to fix PATH so it points to /app/subdir/… instead of /app/…

Would be nice if the buildpack system provided an option so we didn't have to make our own buildpacks to work around it

atomkirk avatar Aug 04 '17 19:08 atomkirk

@atomkirk Sounds like you're trying to do something that's not supported cc @dzuelke

jmorrell avatar Aug 04 '17 19:08 jmorrell

@Jan0707 have you tried running gulp on postinstall from within the internal package.json file, i.e. app/Resources/package.json in your case?

Another idea might be to install gulp with root package.json and update your configuration as such:

{
  "name": "App",
  "engines": {
    "node": "8.1.x"
  },
  "dependencies": {
    "gulp": "^3.9.1"
  },
  "scripts": {
    "postinstall": "npm install --prefix app/Resources && gulp --gulpfile app/Resources/gulpfile.js"
  }
}

Please note that you don't really need to provide the qualified file path for gulp - npm knows what to do.

jmike avatar Aug 05 '17 06:08 jmike

Why a subdirectory, @atomkirk? Or, at least, why does the PHP runtime have to be in a subdirectory, and not just your application? A lot of moving parts are set up to look at $HOME/.heroku/… for buildpacks, and that includes how PHP is built and where it looks for its config files, where binaries look for other .sos, and so forth. It's not something that's easily changed, and so far I don't understand the use case for it.

dzuelke avatar Aug 07 '17 19:08 dzuelke

Well, thanks for the response, but @jmorrell is right, I'm trying to do something unsupported. I kind of took us off topic. Sounds like if you want to support the original issue, the buildpack will probably have to run in the subdirectory and then PATH point at where it put the .heroku folder (or if the buildpack puts the .heroku folder in the repo root, then change nothing)

atomkirk avatar Aug 07 '17 19:08 atomkirk

This feature is what I was looking for. I have a client and a server which have two independent package files, and I want them to be sibling directories and not nested. Therefore I need a way to instruct heroku to start from a specific package file which is not located at the root of my repository.

gplusdotgr avatar Nov 06 '17 08:11 gplusdotgr

Hi, I seem to face the same issue when running under sub directory =>

"postinstall": "npm install --prefix server && npm install --prefix ./server/client",

The entire project is inside server folder , including the client ... any help

ivanovivelin avatar Nov 06 '17 22:11 ivanovivelin

@IIvanov8888 could you open a support ticket at help.heroku.com?

jmorrell avatar Nov 06 '17 23:11 jmorrell

@gablg1 for me your solution doesn't work.

After valid deploy I see in console

app[client.1]: bash: npm: command not found. 

The same situation with node. Even If I change $PATH variable, the issue still occurs.

wmaciejak avatar Nov 08 '17 13:11 wmaciejak

I have a similar issue. I want to run an Ember Fastboot app without having to make a separate repo for my API backend (which is Rails in my case). Ideally I'd be able to specify multiple web processes for a single app. Since I can't do that, I have to make two separate apps for the frontend and backend. But I don't want to create multiple repos for this. I just want to be able to tell one app to use one subdirectory and tell the other app to use the other subdirectory from the git project root.

ClayShentrup avatar Nov 11 '17 18:11 ClayShentrup

As mentioned in the ticket, one can use git subtree push --prefix {app_subfolder} heroku master to push a subfolder only. It would only work if the subfolder is self-contained

WeishiZ avatar Apr 05 '18 06:04 WeishiZ

@WeishiZeng can you please elaborate? :)

Bnaya avatar Apr 05 '18 08:04 Bnaya

@Bnaya The git command is used to push a subfolder to remote. If the subfolder happens to be a self-contained app recognized by heroku, then it'll be deployed.

WeishiZ avatar Apr 05 '18 23:04 WeishiZ

One note to add to the conversation is that the git subtree push solutions only work when you're directly pushing your app to Heroku, but it doesn't solve the problem when you're setting up a Heroku CI Pipeline. In that case, the filesystem is readonly and (AFAIK) there is no way to control what subdirectory of the code is used for the test execution, deployment, etc.

I encountered this issue when trying to use a monorepo with a node.js app as I describe here: https://stackoverflow.com/questions/51449750/how-do-i-get-heroku-ci-to-run-tests-when-using-a-monorepo

javidjamae avatar Jul 20 '18 21:07 javidjamae

has there not been an official solution for this yet? is there a ticket somewhere that we can follow?

kris-campos avatar Aug 08 '18 20:08 kris-campos

@kris-campos I haven't had a chance to loop back on this yet, so there is no official solution. In the meantime I would look at Yarn workspaces: https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/ which I believe npm is also in the process of implementing.

This will be the first thing I explore as a potential solution. If you try them please report back with how it worked for you.

jmorrell avatar Aug 08 '18 20:08 jmorrell

@jmorrell I just starting switching to yarn workspaces & lerna. So far using https://github.com/timanovsky/subdir-heroku-buildpack I can get the package to run properly but I'm unable to access a shared module because it bascially wipes everything outside the sub directory so shared-code doesnt exist and is a local module, not published. Did you figure out a good way to solve this?

├── shared-code
├── server
└── frontend-app

danielmahon avatar Sep 08 '18 22:09 danielmahon

@jmorrell

UPDATE: Well I tried to avoid it but looks like I need to use https://www.npmjs.com/package/yalc instead of yarn link. Since the symlinks don't seem to always work properly inside Heroku. I could drop yarn workspaces completely for yalc and it would definitely simplify/remove most of these steps but the point of this was to try to use yarn workspaces so I'll leave it for now and just use yalc inside Heroku.

This is my current working deployment setup using a local lerna / yarn workspaces monorepo:

root
├── buildpack-run.sh
├── lerna.json
├── package.json
├── packages
│   ├── app
│   │   ├── package.json
│   ├── graphql
│   │   ├── package.json
│   ├── native
│   │   └── package.json
│   ├── shared
│   │   ├── package.json
│   └── site
│       ├── package.json
└── yarn.lock

First, I'm using these 3 buildpacks:

https://github.com/weibeld/heroku-buildpack-run
https://github.com/timanovsky/subdir-heroku-buildpack
https://github.com/danielmahon/create-react-app-buildpack

I use heroku-buildpack-run to run this script to copy the "sibling" shared package into a lib folder within the main package to be deployed, as well as the root level yarn.lock which while it contains ALL the package dependencies, yarn install should still only match the dependencies from package.json.

#!/bin/bash

echo "       Copying shared modules"

mkdir -p packages/app/lib/@myscope
cp -R packages/shared packages/app/lib/@myscope
cp yarn.lock packages/app
# repeat for other simultaneous deployments

I use subdir-heroku-buildpack which just pulls out and replaces your root build folder with one specified at PROJECT_PATH. This is why I needed to copy the shared package from the first step into this one.

I use a forked create-react-app-buildpack because I needed to update it's own dependancy of heroku-buildpack-nodejs with a forked version of my own https://github.com/danielmahon/heroku-buildpack-nodejs that simply adds the --ignore-optional flag to the default yarn install command.

Maybe we can set --ignore-optional with an env variable so the buildpacks don't need forked?

In my package.json for the deployed module, I have the following npm scripts to yalc add the shared package that's now in the lib folder.

  "scripts": {
    "heroku-postbuild": "npm-run-all -s yalc:publish yalc:add ",
    "yalc:publish": "cd lib/@myscope/shared && yalc publish",
    "yalc:add": "yalc add @myscope/shared"
    ...
  },

I also needed to set the shared package as an optional dependency since it is unpublished.

  "optionalDependancies": {
    "@myscope/shared": "*"
  },

As of now, running lerna version --exact --force-publish in the root of the monorepo, properly updates all the package versions, and performs a git push which triggers a new build on Heroku for two apps which share the same local sibling package.

This works but obviously requires much more setup than I would like, let me know if anyone sees anything that can be simplified (I suppose a dedicated buildpack would help), until there is a better option.

danielmahon avatar Sep 09 '18 18:09 danielmahon

@danielmahon I'm unclear on why you need the subdir buildpack. With a package.json listing the workspaces at the root you could push the whole repo up and get all of your dependencies installed.

There are a couple of things that would be missing:

  • support for build commands like heroku-postbuild, but you could add the scripts at the root
  • no automatically picking up the npm start command in the subdirectory, so this command needs to live at the package root

If you open a support ticket, I can help you simplify this setup

jmorrell avatar Sep 10 '18 21:09 jmorrell

re: using yarn workspaces more generally

I got a support ticket recently from a user who had multiple services within the same repo, using yarn workspaces. They used an env var in each app ($APP_DIR) to direct that dyno to start that service.

One drawback to this is that it installs all of your dependencies for all of your applications, which was ballooning their app size over Heroku's limits. Here was the workaround we found.

tl;dr - delete all of the directories not needed by the app in $APP_DIR


Our temporary workaround was to delete folders we didn’t need in a heroku build script in our package.json. This works OK, but it means we have to maintain a list of folders to delete in each service which is kind of a pain.

To resolve this, you can get a map of how each workspace depends on the others using yarn workspaces info. In this example workspace-b depends on workspace-a, but workspace-a and workspace-c are independent:

❯ yarn workspaces info       
yarn workspaces v1.9.4
{
  "workspace-a": {
    "location": "workspace-a",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "workspace-b": {
    "location": "workspace-b",
    "workspaceDependencies": [
      "workspace-a"
    ],
    "mismatchedWorkspaceDependencies": []
  },
  "workspace-c": {
    "location": "workspace-c",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  }
}
✨  Done in 0.04s.

You could create a heroku-prebuild Node script that uses this output to delete any directories not required by $APP_DIR. Here is a quick sketch to what that might look like:

"scripts": {
  ...
  "heroku-prebuild": "node remove-workspaces.js"
}
remove-workspaces.js
const { exec } = require('child_process');
const app = process.env['APP_DIR']

exec('yarn workspaces info --json', (err, stdout, stderr) => {
  const output = JSON.parse(stdout);
  const info = JSON.parse(output.data);

  const dependencies = gatherDependencies(info, app);
  const unneeded = Object.keys(info).filter(i => !dependencies.includes(i));

  unneeded.forEach(i => exec(`rm -rf ${i}`));
});

// Gather all of the workspaces that `workspace` depends on
function gatherDependencies(info, workspace) {
  let deps = [workspace];
  let ws = [workspace];
  while (ws.length) {
    info[ws[0]].workspaceDependencies.forEach(w => {
      ws.push(w);
      deps.push(w);
    });
    ws.shift()
  }
  return deps;
}

This should leave only the workspaces needed by $APP_DIR, and yarn will only install the dependencies needed by that directory.

jmorrell avatar Sep 10 '18 21:09 jmorrell