A Lot Less Node with Airdrop

July 4th, 2022

Chipper CI is continuous integration, just for Laravel. Give it a try - free!

I'm on a mission to use NodeJS the least amount possible.

It's nothing against Node itself (well…in some respects it is), but mostly Webpack and friends is just so SLOW at building static assets.

The most insulting part: we often only need to generate static assets so we have a manifest file (and thus can make our tests pass).

This is true for both Laravel Mix and Vite - they both rely on a manifest file.

Sure, some build systems are faster - like ESBuild. But rather than switching out to a different static asset builder, I have a better solution. What if we just didn't run NodeJS tasks so much?

Luckily, Airdrop exists.

Airdrop is a tool that stores your built static assets. When you do CI runs, Airdrop will determine if the static assets have changed since the last time Airdrop checked.

If they have changed, Airdrop rebuilds the assets and uploads them somewhere.

If they have not changed, Airdrop gets the assets from storage and places them in the correct place.

That helps us determine when we can skip building static assets, saving a LOT of time and server resources in continuous integration and deployment.

Here's how to use Airdrop.

Install

First, we install Airdrop:

composer require hammerstone/airdrop

# Add config/airdrop.php to your project
php artisan airdrop:install

Pretty standard - get the package, and install any assets it needs (a configuration file in this case).

Configure

Then we can configure Airdrop! There's only a few things to care about.

Triggers

Triggers determine when to rebuild static assets.

There are 2 triggers out of the box:

  1. Configuration - Rebuild assets whenever the configured environment changed. Each set of static assets is unique to an environment (local, staging, production, etc).
  2. Files - Track configured files and rebuild assets if a file has changed

Both triggers are used by default.

I generally leave the configuration trigger active without modification.

However, you may need to configure the files trigger a bit. By default, it will check:

  1. Any files in the resources path (where css, js, blade files typically live)
  2. If the Webpack/Vite configuration file changes

On top of this, I tend to add the package lock file for npm.

<?php

use Hammerstone\Airdrop\Drivers\FilesystemDriver;
use Hammerstone\Airdrop\Drivers\GithubActionsDriver;
use Hammerstone\Airdrop\Triggers\ConfigTrigger;
use Hammerstone\Airdrop\Triggers\FileTrigger;

return [
    'driver' => env('AIRDROP_DRIVER', 'default'),
    'drivers' => [
        'default' => [...],
        'github' => [...],
    ],
    'triggers' => [
        ConfigTrigger::class => [
            'env' => env('APP_ENV')
    ],
        FileTrigger::class => [
            'include' => [
                resource_path(), // default
                base_path('webpack.mix.js'), // mix default
                base_path('vite.config.js'), // vite default
                base_path('package-lock.json'), // my addition here
            ],
        ],
    ],
    'outputs' => [...],
];

Drivers

The driver determines where files are stored. The default filesystem driver stores files into a configured Laravel filesystem (S3, local, whatever you want. S3 or some remote storage makes the most sense).

The GitHub Actions Driver lets you save files to the GH Actions cache, which is pretty handy!

I use the Filesystem s3 driver by default, and so the defaults work great for me. You can customize yours, of course!

return [
    // The driver you wish to use to stash and restore your files.
    'driver' => env('AIRDROP_DRIVER', 'default'),

    'drivers' => [
        'default' => [
            // The class responsible for implementing the stash and restore
            // logic. Must extend BaseDriver.
            'class' => FilesystemDriver::class,

            // The disk on which to store the built files.
            'disk' => env('AIRDROP_REMOTE_DISK', 's3'),

            // The folder (if any) where you'd like your stashed assets to reside.
            'remote_directory' => env('AIRDROP_REMOTE_DIR', 'airdrop'),

            // A writeable directory on the machine that builds the assets.
            // Used to build up the ZIP file before stashing it.
            'local_tmp_directory' => env('AIRDROP_LOCAL_TMP_DIR', storage_path('framework')),

            // The skip file is an empty file that will be created to
            // indicate that asset building can be skipped.
            'skip_file' => env('AIRDROP_SKIP_FILE', base_path('.airdrop_skip')),
        ],
        ],
        // ...
];

Outputs

The last thing to configure are outputs. These are the files that Airdrop stores (and later retrieves) in the driver of your choice.

For a standard Laravel install, the defaults are usually good to go! However you'll need to add to this as you create additional static assets to your application.

return [
    // ...
    'outputs' => [
        /*
         * Files or folders that should be included.
         */
        'include' => [
            // The mix-manifest file tells Laravel how to get your versioned assets.
            public_path('mix-manifest.json'),

            // Compiled CSS.
            public_path('css'),

            // Compiled JS.
            public_path('js'),
        ],

                // ...
    ],
];

Don't Commit Static Assets

Airdrop is best used when static assets are ignored via the .gitignore file. This means that your CI pipeline and/or deploy pipeline will handle building and storing static assets. This is my preferred way to develop when coding in teams, as it reduces merge conflicts.

Append the following to your .gitignore file:

/.airdrop_skip
public/css/*
public/js/**

S3 Bucket Permissions

In my case, I'll be using an S3 bucket. This means I need to create and configure a few things in AWS:

  1. Create a bucket (leaving the defaults of never allowing public objects is great)
  2. Create an IAM user (to get a key / secret key)
  3. Add permissions to that IAM user that allows uploading/downloading to that S3 bucket

Here's a sample JSON policy document to add to your IAM user that allows permissions to an S3 bucket. Adjust the name of the S3 bucket to match the bucket you created! You likely did not create a bucket named my-airdrop-bucket, since I stole the bucket name already. It's ALL MINE!

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1420044805000",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
                "arn:aws:s3:::my-airdrop-bucket",
        "arn:aws:s3:::my-airdrop-bucket/*"
      ]
    }
  ]
}

The following environment variables need to be set (assuming you haven't customized your filesystem configuration). These can be set as secrets or as environment variables in your Chipper CI project:

# Definitely Secrets:
AWS_ACCESS_KEY_ID="xxx"
AWS_SECRET_ACCESS_KEY="yyy"

# Optionally a secret:
AWS_BUCKET="my-airdrop-bucket"

# Probably a plain env var:
AWS_DEFAULT_REGION="us-east-2"

Finally, be sure you have the required composer package to use S3 (and compatible) filesystems in Laravel:

composer require league/flysystem-aws-s3-v3 "^3.0"

CI Pipeline

Finally, we can use Airdrop in our CI pipeline to determine when to install and build our static assets.

For Chipper CI, that means editing the .chipperci.yml file to add checks on if we should install and run our npm tasks (as documented here).

version: 1

environment:
  php: 8.1
  node: 14

pipeline:
  - name: Setup
    cmd: |
      cp -v .env.example .env
      composer install --no-interaction --prefer-dist --optimize-autoloader
      php artisan key:generate

  # Only build static assets if Airdrop 
  # determined assets have changed
  - name: Compile Dev Assets
    cmd: |
      php artisan airdrop:download

      if [ ! -f ".airdrop_skip" ]; then
          npm ci --no-audit
          npm run dev
      fi

      php artisan airdrop:upload

  - name: Run Tests
    cmd: pest

And voila, compiling static assets is just one second or less … since we didn't even have to build them!

chipper ci static asset compile time

⚠️ Don't forget that the assets are specific to your environment as set in your .env file (or environment variable APP_ENV). Be careful about which environment you want set in your CI pipeline.

Try out Chipper CI!
Chipper CI is the easiest way to test and deploy your Laravel applications. Try it out - it's free!