Skip to content

Modular runtime env vars #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Oct 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a86568a
Set-up env var replacement in generated index file
mars Sep 6, 2016
7fdc50b
Render Mustaches in the Webpack bundle
mars Oct 13, 2016
94c96f8
Fix bundle name
mars Oct 13, 2016
1145054
Generate composite `REACT_APP_VARS_AS_JSON` env var at runtime
mars Sep 6, 2016
f9c2996
Add runtime config to slug
mars Sep 6, 2016
bedbeb3
Fix compile path
mars Sep 6, 2016
94f6ac9
Escape quotes so bundle replacement works
mars Oct 13, 2016
36628b1
Set utf-8 for JSON encoding function, because old-skool Ruby 1.9
mars Oct 13, 2016
082b609
Switch to Ruby script for env var to JSON conversion.
mars Oct 14, 2016
9b17224
Fix runtime paths
mars Oct 14, 2016
cf0e09b
Set utf-8 for JSON encoding function, because old-skool Ruby 1.9
mars Oct 14, 2016
321d270
Fix for env values with unknown encoding.
mars Oct 14, 2016
d3e0d51
Actually write the runtime bundle
mars Oct 14, 2016
cc46aa0
Another level of escapes.
mars Oct 14, 2016
525720b
Fix for space char breaking sed expression
mars Oct 14, 2016
25dd8ee
Escape forward slashes too; they break sed expression
mars Oct 14, 2016
127d4ed
Escape ampersand too; they break sed expression
mars Oct 14, 2016
210e890
Replace `sed` JSON injection with pure Ruby
mars Oct 14, 2016
335ec81
Fix pure Ruby injector command with correct args
mars Oct 15, 2016
95bcc21
Fix file path to JS bundle
mars Oct 15, 2016
e2081c4
More escapes for JSON values
mars Oct 15, 2016
7ac5909
Fix so injected values just work without further escaping by developer.
mars Oct 16, 2016
0e8f56d
TravisCI
mars Oct 16, 2016
d925979
Use rake to execute tests (for TravisCI)
mars Oct 16, 2016
4bae2af
Fix missing dependency
mars Oct 16, 2016
b3f8631
Simplification fix for double quote escaping.
mars Oct 16, 2016
b4516d3
Triple backslash escape for double-quote in JSON value.
mars Oct 16, 2016
b296fb9
Revise JSON escaping for control chars
mars Oct 16, 2016
8a7174a
Fail gracefully for old CRA versions
mars Oct 17, 2016
34940c9
Improve "Injecting runtime" log message
mars Oct 17, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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