Skip to content

Commit 7c02a59

Browse files
authored
Modular runtime env vars (#3)
* 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
1 parent a92d75c commit 7c02a59

File tree

11 files changed

+378
-0
lines changed

11 files changed

+378
-0
lines changed

.profile.d/inject_react_app_env.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
3+
# Fail immediately on non-zero exit code.
4+
set -e
5+
# Debug, echo every command
6+
#set -x
7+
8+
# Each bundle is generated with a unique hash name
9+
# to bust browser cache.
10+
js_bundle=/app/build/static/js/main.*.js
11+
12+
if [ -f $js_bundle ]
13+
then
14+
15+
# Get exact filename.
16+
js_bundle_filename=`ls $js_bundle`
17+
18+
echo "Injecting runtime env into $js_bundle_filename (from .profile.d/inject_react_app_env.sh)"
19+
20+
# Render runtime env vars into bundle.
21+
ruby -E utf-8:utf-8 \
22+
-r /app/.heroku/create-react-app/injectable_env.rb \
23+
-e "InjectableEnv.replace('$js_bundle_filename')"
24+
fi

.rspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--color
2+
--format documentation
3+
--require spec_helper

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
language: ruby
2+
rvm:
3+
- 1.9.3

Gemfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# encoding: utf-8
2+
# frozen_string_literal: true
3+
source "https://rubygems.org"
4+
ruby '1.9.3'
5+
6+
group :test do
7+
gem 'rake'
8+
gem 'rspec'
9+
end

Gemfile.lock

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
diff-lcs (1.2.5)
5+
rake (11.3.0)
6+
rspec (3.5.0)
7+
rspec-core (~> 3.5.0)
8+
rspec-expectations (~> 3.5.0)
9+
rspec-mocks (~> 3.5.0)
10+
rspec-core (3.5.4)
11+
rspec-support (~> 3.5.0)
12+
rspec-expectations (3.5.0)
13+
diff-lcs (>= 1.2.0, < 2.0)
14+
rspec-support (~> 3.5.0)
15+
rspec-mocks (3.5.0)
16+
diff-lcs (>= 1.2.0, < 2.0)
17+
rspec-support (~> 3.5.0)
18+
rspec-support (3.5.0)
19+
20+
PLATFORMS
21+
ruby
22+
23+
DEPENDENCIES
24+
rake
25+
rspec
26+
27+
RUBY VERSION
28+
ruby 1.9.3p551
29+
30+
BUNDLED WITH
31+
1.13.4

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,12 @@ Inner layer of Heroku Buildpack for create-react-app
22
====================================================
33

44
See: [create-react-app-buildpack](https://github.com/mars/create-react-app-buildpack)
5+
6+
[![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)
7+
8+
Development
9+
-----------
10+
11+
Use Ruby 1.9.3 as built-in to Cedar-14.
12+
13+
Run tests: `bundle exec rake`

Rakefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
begin
2+
require 'rspec/core/rake_task'
3+
4+
RSpec::Core::RakeTask.new(:spec)
5+
task :default => :spec
6+
7+
rescue LoadError
8+
# no rspec available
9+
end

bin/compile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ else
3939
echo '{ "root": "build/" }' > static.json
4040
fi
4141

42+
echo ' Enabling runtime environment variables'
43+
44+
cra_dir="$BUILD_DIR/.heroku/create-react-app"
45+
mkdir -p "$cra_dir"
46+
cp "$BP_DIR/lib/injectable_env.rb" "$cra_dir/"
47+
48+
profile_d_dir="$BUILD_DIR/.profile.d"
49+
mkdir -p "$profile_d_dir"
50+
cp "$BP_DIR/.profile.d/inject_react_app_env.sh" "$profile_d_dir/"
51+
4252
# Support env vars during build:
4353
# * `REACT_APP_*`
4454
# * https://github.com/facebookincubator/create-react-app/blob/v0.2.3/template/README.md#adding-custom-environment-variables

lib/injectable_env.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# encoding: utf-8
2+
require 'json'
3+
4+
class InjectableEnv
5+
DefaultVarMatcher = /^REACT_APP_/
6+
Placeholder='{{REACT_APP_VARS_AS_JSON}}'
7+
8+
def self.create(var_matcher=DefaultVarMatcher)
9+
vars = ENV.find_all {|name,value| var_matcher===name }
10+
11+
json = '{'
12+
is_first = true
13+
vars.each do |name,value|
14+
json += ',' unless is_first
15+
json += "#{escape(name)}:#{escape(value)}"
16+
is_first = false
17+
end
18+
json += '}'
19+
end
20+
21+
def self.render(*args)
22+
$stdout.write create(*args)
23+
$stdout.flush
24+
end
25+
26+
def self.replace(file, *args)
27+
injectee = IO.read(file)
28+
return unless injectee.index(Placeholder)
29+
30+
env = create(*args)
31+
head,_,tail = injectee.partition(Placeholder)
32+
injected = head + env + tail
33+
File.open(file, 'w') do |f|
34+
f.write(injected)
35+
end
36+
end
37+
38+
# Escape JSON name/value double-quotes so payload can be injected
39+
# into Webpack bundle where embedded in a double-quoted string.
40+
#
41+
def self.escape(v)
42+
v.dup
43+
.force_encoding('utf-8') # UTF-8 encoding for content
44+
.to_json
45+
.gsub(/\\\\/, '\\\\\\\\\\\\\\\\') # single slash in content
46+
.gsub(/\\([bfnrt])/, '\\\\\\\\\1') # control sequence in content
47+
.gsub(/([^\A])\"([^\Z])/, '\1\\\\\\"\2') # double-quote in content
48+
.gsub(/(\A\"|\"\Z)/, '\\\"') # double-quote around JSON token
49+
end
50+
51+
end

spec/injectable_env_spec.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# encoding: utf-8
2+
require './lib/injectable_env'
3+
require 'yaml'
4+
require 'tempfile'
5+
6+
RSpec.describe InjectableEnv do
7+
8+
describe '.create' do
9+
it "returns empty object" do
10+
expect(InjectableEnv.create).to eq('{}')
11+
end
12+
13+
describe 'for REACT_APP_ vars' do
14+
before do
15+
ENV['REACT_APP_HELLO'] = 'Hello World'
16+
ENV['REACT_APP_EMOJI'] = '🍒🍊🍍'
17+
ENV['REACT_APP_EMBEDDED_QUOTES'] = '"e=MC(2)"'
18+
ENV['REACT_APP_SLASH_CONTENT'] = '\\'
19+
ENV['REACT_APP_NEWLINE'] = "I am\na poet."
20+
end
21+
after do
22+
ENV.delete 'REACT_APP_HELLO'
23+
ENV.delete 'REACT_APP_EMOJI'
24+
ENV.delete 'REACT_APP_EMBEDDED_QUOTES'
25+
ENV.delete 'REACT_APP_SLASH_CONTENT'
26+
ENV.delete 'REACT_APP_NEWLINE'
27+
end
28+
29+
it "returns entries" do
30+
result = InjectableEnv.create
31+
# puts result
32+
# puts unescape(result)
33+
object = JSON.parse(unescape(result))
34+
expect(object['REACT_APP_HELLO']).to eq('Hello World')
35+
expect(object['REACT_APP_EMOJI']).to eq('🍒🍊🍍')
36+
expect(object['REACT_APP_EMBEDDED_QUOTES']).to eq('"e=MC(2)"')
37+
expect(object['REACT_APP_SLASH_CONTENT']).to eq('\\')
38+
expect(object['REACT_APP_NEWLINE']).to eq("I am\na poet.")
39+
end
40+
end
41+
42+
describe 'for unmatches vars' do
43+
before do
44+
ENV['ANOTHER_HELLO'] = 'Hello World'
45+
end
46+
after do
47+
ENV.delete 'ANOTHER_HELLO'
48+
end
49+
50+
it "ignores them" do
51+
result = InjectableEnv.create
52+
object = JSON.parse(unescape(result))
53+
expect(object).not_to have_key('ANOTHER_HELLO')
54+
end
55+
end
56+
end
57+
58+
describe '.render' do
59+
it "writes result to stdout" do
60+
expect { InjectableEnv.render }.to output('{}').to_stdout
61+
end
62+
end
63+
64+
describe '.replace' do
65+
before do
66+
ENV['REACT_APP_HELLO'] = "Hello\n\"World\" we \\ prices today"
67+
end
68+
after do
69+
ENV.delete 'REACT_APP_HELLO'
70+
end
71+
72+
it "writes into file" do
73+
begin
74+
file = Tempfile.new('injectable_env_test')
75+
file.write('var injected="{{REACT_APP_VARS_AS_JSON}}"')
76+
file.rewind
77+
78+
InjectableEnv.replace(file.path)
79+
80+
expected_value='var injected="{\\"REACT_APP_HELLO\\":\\"Hello\\\\n\\\\\"World\\\\\" we \\\\\\\\ prices today\\"}"'
81+
actual_value=file.read
82+
expect(actual_value).to eq(expected_value)
83+
ensure
84+
if file
85+
file.close
86+
file.unlink
87+
end
88+
end
89+
end
90+
91+
it "does not write when the placeholder is missing" do
92+
begin
93+
file = Tempfile.new('injectable_env_test')
94+
file.write('template is not present in file')
95+
file.rewind
96+
97+
InjectableEnv.replace(file.path)
98+
99+
expected_value='template is not present in file'
100+
actual_value=file.read
101+
expect(actual_value).to eq(expected_value)
102+
ensure
103+
if file
104+
file.close
105+
file.unlink
106+
end
107+
end
108+
end
109+
end
110+
111+
describe '.escape' do
112+
it 'slash-escapes the JSON token double-quotes' do
113+
expect(InjectableEnv.escape('value')).to eq('\\"value\\"')
114+
end
115+
it 'double-escapes double-quotes in the value' do
116+
# This looks insane, but the six-slashes '\\\\\\' test for three '\\\'
117+
expect(InjectableEnv.escape('"quoted"')).to eq('\\"\\\\\\"quoted\\\\\\"\\"')
118+
end
119+
end
120+
end
121+
122+
# For the sake of parsing the test output,
123+
# undo the "injectable" JSON escape sequences.
124+
def unescape(s)
125+
YAML.load(%Q(---\n"#{s}"\n))
126+
end

0 commit comments

Comments
 (0)