Skip to content

Commit

Permalink
Modular runtime env vars (#3)
Browse files Browse the repository at this point in the history
* Set-up env var replacement in generated index file

* Render Mustaches in the Webpack bundle

* Fix bundle name

* Generate composite `REACT_APP_VARS_AS_JSON` env var at runtime

* Add runtime config to slug

* Fix compile path

* Escape quotes so bundle replacement works

* Set utf-8 for JSON encoding function, because old-skool Ruby 1.9

* Switch to Ruby script for env var to JSON conversion.

* Fix runtime paths

* Set utf-8 for JSON encoding function, because old-skool Ruby 1.9

* Fix for env values with unknown encoding.

* Actually write the runtime bundle

* Another level of escapes.

* Fix for space char breaking sed expression

* Escape forward slashes too; they break sed expression

* Escape ampersand too; they break sed expression

* Replace `sed` JSON injection with pure Ruby

* Fix pure Ruby injector command with correct args

* Fix file path to JS bundle

* More escapes for JSON values

* Fix so injected values just work without further escaping by developer.

* TravisCI

* Use rake to execute tests (for TravisCI)

* Fix missing dependency

* Simplification fix for double quote escaping.

* Triple backslash escape for double-quote in JSON value.

* Revise JSON escaping for control chars

* Fail gracefully for old CRA versions

* Improve "Injecting runtime" log message
  • Loading branch information
mars authored Oct 17, 2016
1 parent a92d75c commit 7c02a59
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .profile.d/inject_react_app_env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

# Fail immediately on non-zero exit code.
set -e
# Debug, echo every command
#set -x

# Each bundle is generated with a unique hash name
# to bust browser cache.
js_bundle=/app/build/static/js/main.*.js

if [ -f $js_bundle ]
then

# Get exact filename.
js_bundle_filename=`ls $js_bundle`

echo "Injecting runtime env into $js_bundle_filename (from .profile.d/inject_react_app_env.sh)"

# Render runtime env vars into bundle.
ruby -E utf-8:utf-8 \
-r /app/.heroku/create-react-app/injectable_env.rb \
-e "InjectableEnv.replace('$js_bundle_filename')"
fi
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--color
--format documentation
--require spec_helper
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
language: ruby
rvm:
- 1.9.3
9 changes: 9 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# encoding: utf-8
# frozen_string_literal: true
source "https://rubygems.org"
ruby '1.9.3'

group :test do
gem 'rake'
gem 'rspec'
end
31 changes: 31 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.2.5)
rake (11.3.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)

PLATFORMS
ruby

DEPENDENCIES
rake
rspec

RUBY VERSION
ruby 1.9.3p551

BUNDLED WITH
1.13.4
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@ Inner layer of Heroku Buildpack for create-react-app
====================================================

See: [create-react-app-buildpack](https://github.com/mars/create-react-app-buildpack)

[![Build Status](https://travis-ci.org/mars/create-react-app-inner-buildpack.svg?branch=master)](https://travis-ci.org/mars/create-react-app-inner-buildpack)

Development
-----------

Use Ruby 1.9.3 as built-in to Cedar-14.

Run tests: `bundle exec rake`
9 changes: 9 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
begin
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)
task :default => :spec

rescue LoadError
# no rspec available
end
10 changes: 10 additions & 0 deletions bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ else
echo '{ "root": "build/" }' > static.json
fi

echo ' Enabling runtime environment variables'

cra_dir="$BUILD_DIR/.heroku/create-react-app"
mkdir -p "$cra_dir"
cp "$BP_DIR/lib/injectable_env.rb" "$cra_dir/"

profile_d_dir="$BUILD_DIR/.profile.d"
mkdir -p "$profile_d_dir"
cp "$BP_DIR/.profile.d/inject_react_app_env.sh" "$profile_d_dir/"

# Support env vars during build:
# * `REACT_APP_*`
# * https://github.com/facebookincubator/create-react-app/blob/v0.2.3/template/README.md#adding-custom-environment-variables
Expand Down
51 changes: 51 additions & 0 deletions lib/injectable_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# encoding: utf-8
require 'json'

class InjectableEnv
DefaultVarMatcher = /^REACT_APP_/
Placeholder='{{REACT_APP_VARS_AS_JSON}}'

def self.create(var_matcher=DefaultVarMatcher)
vars = ENV.find_all {|name,value| var_matcher===name }

json = '{'
is_first = true
vars.each do |name,value|
json += ',' unless is_first
json += "#{escape(name)}:#{escape(value)}"
is_first = false
end
json += '}'
end

def self.render(*args)
$stdout.write create(*args)
$stdout.flush
end

def self.replace(file, *args)
injectee = IO.read(file)
return unless injectee.index(Placeholder)

env = create(*args)
head,_,tail = injectee.partition(Placeholder)
injected = head + env + tail
File.open(file, 'w') do |f|
f.write(injected)
end
end

# Escape JSON name/value double-quotes so payload can be injected
# into Webpack bundle where embedded in a double-quoted string.
#
def self.escape(v)
v.dup
.force_encoding('utf-8') # UTF-8 encoding for content
.to_json
.gsub(/\\\\/, '\\\\\\\\\\\\\\\\') # single slash in content
.gsub(/\\([bfnrt])/, '\\\\\\\\\1') # control sequence in content
.gsub(/([^\A])\"([^\Z])/, '\1\\\\\\"\2') # double-quote in content
.gsub(/(\A\"|\"\Z)/, '\\\"') # double-quote around JSON token
end

end
126 changes: 126 additions & 0 deletions spec/injectable_env_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# encoding: utf-8
require './lib/injectable_env'
require 'yaml'
require 'tempfile'

RSpec.describe InjectableEnv do

describe '.create' do
it "returns empty object" do
expect(InjectableEnv.create).to eq('{}')
end

describe 'for REACT_APP_ vars' do
before do
ENV['REACT_APP_HELLO'] = 'Hello World'
ENV['REACT_APP_EMOJI'] = '🍒🍊🍍'
ENV['REACT_APP_EMBEDDED_QUOTES'] = '"e=MC(2)"'
ENV['REACT_APP_SLASH_CONTENT'] = '\\'
ENV['REACT_APP_NEWLINE'] = "I am\na poet."
end
after do
ENV.delete 'REACT_APP_HELLO'
ENV.delete 'REACT_APP_EMOJI'
ENV.delete 'REACT_APP_EMBEDDED_QUOTES'
ENV.delete 'REACT_APP_SLASH_CONTENT'
ENV.delete 'REACT_APP_NEWLINE'
end

it "returns entries" do
result = InjectableEnv.create
# puts result
# puts unescape(result)
object = JSON.parse(unescape(result))
expect(object['REACT_APP_HELLO']).to eq('Hello World')
expect(object['REACT_APP_EMOJI']).to eq('🍒🍊🍍')
expect(object['REACT_APP_EMBEDDED_QUOTES']).to eq('"e=MC(2)"')
expect(object['REACT_APP_SLASH_CONTENT']).to eq('\\')
expect(object['REACT_APP_NEWLINE']).to eq("I am\na poet.")
end
end

describe 'for unmatches vars' do
before do
ENV['ANOTHER_HELLO'] = 'Hello World'
end
after do
ENV.delete 'ANOTHER_HELLO'
end

it "ignores them" do
result = InjectableEnv.create
object = JSON.parse(unescape(result))
expect(object).not_to have_key('ANOTHER_HELLO')
end
end
end

describe '.render' do
it "writes result to stdout" do
expect { InjectableEnv.render }.to output('{}').to_stdout
end
end

describe '.replace' do
before do
ENV['REACT_APP_HELLO'] = "Hello\n\"World\" we \\ prices today"
end
after do
ENV.delete 'REACT_APP_HELLO'
end

it "writes into file" do
begin
file = Tempfile.new('injectable_env_test')
file.write('var injected="{{REACT_APP_VARS_AS_JSON}}"')
file.rewind

InjectableEnv.replace(file.path)

expected_value='var injected="{\\"REACT_APP_HELLO\\":\\"Hello\\\\n\\\\\"World\\\\\" we \\\\\\\\ prices today\\"}"'
actual_value=file.read
expect(actual_value).to eq(expected_value)
ensure
if file
file.close
file.unlink
end
end
end

it "does not write when the placeholder is missing" do
begin
file = Tempfile.new('injectable_env_test')
file.write('template is not present in file')
file.rewind

InjectableEnv.replace(file.path)

expected_value='template is not present in file'
actual_value=file.read
expect(actual_value).to eq(expected_value)
ensure
if file
file.close
file.unlink
end
end
end
end

describe '.escape' do
it 'slash-escapes the JSON token double-quotes' do
expect(InjectableEnv.escape('value')).to eq('\\"value\\"')
end
it 'double-escapes double-quotes in the value' do
# This looks insane, but the six-slashes '\\\\\\' test for three '\\\'
expect(InjectableEnv.escape('"quoted"')).to eq('\\"\\\\\\"quoted\\\\\\"\\"')
end
end
end

# For the sake of parsing the test output,
# undo the "injectable" JSON escape sequences.
def unescape(s)
YAML.load(%Q(---\n"#{s}"\n))
end
Loading

0 comments on commit 7c02a59

Please sign in to comment.