froxy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ba923cd0a854a238a03b4fb501c027282477ccc90ca2005379001492f1f531f1
4
+ data.tar.gz: 43e8f7e24dc5ace4efc8c1ff7cba5af6786db2dd7a94b271bc83916ac09c6a84
5
+ SHA512:
6
+ metadata.gz: 4fee298a6f956749521698d204ea6422d2a7fbfdc43d5e1d1443554cff8dba5bf59c57c92836c6759f79c1287614d20f6a7ee4ebd44e61f563021464666387b5
7
+ data.tar.gz: 5d5c56b0a0dff9581f339ee563dc83a5c00cc9896355996a887b95a70e461f07a4777b9cac202a0bed9cdda49df80f4cc32aac630b6be100a5b50e376140255f
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /test/internal/public
10
+ /node_modules/
data/.rubocop.yml ADDED
@@ -0,0 +1,6 @@
1
+ Style/LineLength:
2
+ Max: 100
3
+ Style/Documentation:
4
+ Enabled: false
5
+ Style/ClassVars:
6
+ Enabled: false
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at joel@developwithstyle.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in froxy.gemspec
6
+ gemspec
7
+
8
+ gem 'combustion', '~> 1.3'
9
+ gem 'minitest', '~> 5.0'
10
+ gem 'minitest-focus'
11
+ gem 'puma'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rubocop', require: false
14
+ gem 'rubocop-minitest', require: false
15
+ gem 'rubocop-rake', require: false
data/Gemfile.lock ADDED
@@ -0,0 +1,113 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ froxy (0.1.0)
5
+ activesupport (>= 5.0.0, < 7.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actionpack (6.1.3)
11
+ actionview (= 6.1.3)
12
+ activesupport (= 6.1.3)
13
+ rack (~> 2.0, >= 2.0.9)
14
+ rack-test (>= 0.6.3)
15
+ rails-dom-testing (~> 2.0)
16
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
17
+ actionview (6.1.3)
18
+ activesupport (= 6.1.3)
19
+ builder (~> 3.1)
20
+ erubi (~> 1.4)
21
+ rails-dom-testing (~> 2.0)
22
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
23
+ activesupport (6.1.3)
24
+ concurrent-ruby (~> 1.0, >= 1.0.2)
25
+ i18n (>= 1.6, < 2)
26
+ minitest (>= 5.1)
27
+ tzinfo (~> 2.0)
28
+ zeitwerk (~> 2.3)
29
+ ast (2.4.2)
30
+ builder (3.2.4)
31
+ combustion (1.3.1)
32
+ activesupport (>= 3.0.0)
33
+ railties (>= 3.0.0)
34
+ thor (>= 0.14.6)
35
+ concurrent-ruby (1.1.8)
36
+ crass (1.0.6)
37
+ erubi (1.10.0)
38
+ i18n (1.8.9)
39
+ concurrent-ruby (~> 1.0)
40
+ loofah (2.9.0)
41
+ crass (~> 1.0.2)
42
+ nokogiri (>= 1.5.9)
43
+ method_source (1.0.0)
44
+ mini_portile2 (2.5.0)
45
+ minitest (5.14.3)
46
+ minitest-focus (1.2.1)
47
+ minitest (>= 4, < 6)
48
+ nio4r (2.5.5)
49
+ nokogiri (1.11.1)
50
+ mini_portile2 (~> 2.5.0)
51
+ racc (~> 1.4)
52
+ parallel (1.20.1)
53
+ parser (3.0.0.0)
54
+ ast (~> 2.4.1)
55
+ puma (5.2.1)
56
+ nio4r (~> 2.0)
57
+ racc (1.5.2)
58
+ rack (2.2.3)
59
+ rack-test (1.1.0)
60
+ rack (>= 1.0, < 3)
61
+ rails-dom-testing (2.0.3)
62
+ activesupport (>= 4.2.0)
63
+ nokogiri (>= 1.6)
64
+ rails-html-sanitizer (1.3.0)
65
+ loofah (~> 2.3)
66
+ railties (6.1.3)
67
+ actionpack (= 6.1.3)
68
+ activesupport (= 6.1.3)
69
+ method_source
70
+ rake (>= 0.8.7)
71
+ thor (~> 1.0)
72
+ rainbow (3.0.0)
73
+ rake (13.0.3)
74
+ regexp_parser (2.0.3)
75
+ rexml (3.2.4)
76
+ rubocop (1.10.0)
77
+ parallel (~> 1.10)
78
+ parser (>= 3.0.0.0)
79
+ rainbow (>= 2.2.2, < 4.0)
80
+ regexp_parser (>= 1.8, < 3.0)
81
+ rexml
82
+ rubocop-ast (>= 1.2.0, < 2.0)
83
+ ruby-progressbar (~> 1.7)
84
+ unicode-display_width (>= 1.4.0, < 3.0)
85
+ rubocop-ast (1.4.1)
86
+ parser (>= 2.7.1.5)
87
+ rubocop-minitest (0.10.3)
88
+ rubocop (>= 0.87, < 2.0)
89
+ rubocop-rake (0.5.1)
90
+ rubocop
91
+ ruby-progressbar (1.11.0)
92
+ thor (1.1.0)
93
+ tzinfo (2.0.4)
94
+ concurrent-ruby (~> 1.0)
95
+ unicode-display_width (2.0.0)
96
+ zeitwerk (2.4.2)
97
+
98
+ PLATFORMS
99
+ ruby
100
+
101
+ DEPENDENCIES
102
+ combustion (~> 1.3)
103
+ froxy!
104
+ minitest (~> 5.0)
105
+ minitest-focus
106
+ puma
107
+ rake (~> 13.0)
108
+ rubocop
109
+ rubocop-minitest
110
+ rubocop-rake
111
+
112
+ BUNDLED WITH
113
+ 2.2.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Joel Moss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Froxy - Fast ESModule based Frontend Bundling for Rails
2
+
3
+ Froxy serves as a delivery machanism for all your frontend assets in Rails. It is
4
+ designed specifically for Rails applications, and can completely replace Webpacker and the Rails
5
+ asset pipeline (Sprockets) by bundling your ESModule based frontend code in real time and on demand.
6
+ It does this by proxying all frontend requests to the amazing [esbuild](https://esbuild.github.io).
7
+
8
+ ## ! NOT PRODUCTION READY !
9
+
10
+ Froxy is currently an experimental library and does not yet have a production or deployment mode.
11
+ While you could deploy it and run it in production, it is highly recommended that you do not do
12
+ this. This is because almost every asset is bundled and built in real time by request. A future
13
+ production mode will likely include pre-built cachable assets.
14
+
15
+ -- _YOU HAVE BEEN WARNED!_
16
+
17
+ ## Features
18
+
19
+ - Real-time bundling of JS, JSX and CSS.
20
+ - Import CSS and other static assets (images, fonts, etc.)
21
+ - Serve assets from anywhere within the Rails root. (eg. `/app/views/layouts/application.css`, or `/lib/utils/time.js`)
22
+ - Side loaded JS/CSS for your layouts and views.
23
+ - [Tree shaking](https://esbuild.github.io/api/#tree-shaking).
24
+ - Code Splitting.
25
+ - Source Maps.
26
+ - Minification.
27
+
28
+ ## Roadmap
29
+
30
+ In no particular order:
31
+
32
+ - Pre-bundling / cached assets.
33
+ - Typescript.
34
+ - CSS Modules.
35
+ - PostCSS support.
36
+
37
+ ## Installation
38
+
39
+ Froxy requires Rails 6+ and Node.
40
+
41
+ Add this line to your application's Gemfile:
42
+
43
+ ```ruby
44
+ gem 'froxy'
45
+ ```
46
+
47
+ And then execute:
48
+
49
+ $ bundle install
50
+
51
+ Or install it yourself as:
52
+
53
+ $ gem install froxy
54
+
55
+ ## Usage
56
+
57
+ ### Javascript
58
+
59
+ Import any JS:
60
+
61
+ ```javascript
62
+ import start from '/my/start' // Local absolute path
63
+ import start from './start' // Local relative path
64
+ import start from 'start' // From node modules
65
+ ```
66
+
67
+ All JS is [bundled](https://esbuild.github.io/api/#bundle) by esbuild, which will inline any
68
+ imported dependencies into the file itself.
69
+
70
+ The JS file extension is not required and is assumed.
71
+
72
+ ### CSS
73
+
74
+ CSS requested directly will return a plain stylesheet - as you would expect. But CSS that is
75
+ imported from JS will result in the requested CSS injected into an HTML `link` tag.
76
+
77
+ ```javascript
78
+ import '/my/styles.css'
79
+ ```
80
+
81
+ ### Images/Fonts, etc.
82
+
83
+ When called directly, images are served directly - avoiding a call to esbuild. But when an image is
84
+ imported from JS or used in a `url()` in CSS, the URL path is returned.
85
+
86
+ Examples (where 'avatar.png' is located in '/app/images', but could be anywhere):
87
+
88
+ ```javascript
89
+ // /app/views/home.js
90
+ import imgUrl from '../images/avatar.png' // imgUrl == "/app/images/avatar.png"
91
+ ```
92
+
93
+ ```css
94
+ body {
95
+ background-image: url('/app/images/avatar.png');
96
+ }
97
+ ```
98
+
99
+ ### Side Loaded JS/CSS
100
+
101
+ Froxy also has built in support for automatically side loading JS and CSS with your views and
102
+ layouts.
103
+
104
+ Just create a JS and/or CSS file with the same name as any view or layout, and make sure your
105
+ layouts include the `<%= yield :side_loaded_js %>` and `<%= yield :side_loaded_css %>`. Something
106
+ like this:
107
+
108
+ ```html
109
+ <!DOCTYPE html>
110
+ <html>
111
+ <head>
112
+ <title>Hello World</title>
113
+ <%= yield :side_loaded_css %>
114
+ </head>
115
+ <body>
116
+ <%= yield %> <%= yield :side_loaded_js %>
117
+ </body>
118
+ </html>
119
+ ```
120
+
121
+ On each page request, Froxy will check if your layout and view has a JS/CSS file of the same name,
122
+ and include them into your layout HTML.
123
+
124
+ ### Import aliases
125
+
126
+ Module aliases can be defined in your package.json, supporting local and node modules.
127
+
128
+ In your package.json:
129
+
130
+ ```json
131
+ "froxy": {
132
+ "aliases": {
133
+ "_": "lodash", // a node module
134
+ "myalias": "/absolute/path/to/alias.js", // local path
135
+ }
136
+ }
137
+ ```
138
+
139
+ Then import:
140
+
141
+ ```javascript
142
+ import { map } from '_'
143
+ import axios from 'myaxios'
144
+ ```
145
+
146
+ ### Configuration
147
+
148
+ There are a few options that you can customise, and they are all defined in your `package.json`. For
149
+ example:
150
+
151
+ ```json
152
+ "froxy": {
153
+ "target": [],
154
+ "aliases": {
155
+ "_": "lodash"
156
+ }
157
+ }
158
+ ```
159
+
160
+ #### `target`
161
+
162
+ See esbuild's documentation on [defining targets](https://esbuild.github.io/api/#target).
163
+
164
+ #### `inject`
165
+
166
+ See esbuild's documentation on [inject](https://esbuild.github.io/api/#inject).
167
+
168
+ #### `aliases`
169
+
170
+ See [aliases](#import-aliases)
171
+
172
+ #### `minify`
173
+
174
+ (default: `false`)
175
+
176
+ See esbuild's documentation on [minification](https://esbuild.github.io/api/#minify).
177
+
178
+ #### `sourcemap`
179
+
180
+ (default: `true`)
181
+
182
+ See esbuild's documentation on [sourcemap](https://esbuild.github.io/api/#sourcemap).
183
+
184
+ ## Development
185
+
186
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
187
+
188
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
189
+
190
+ ## Contributing
191
+
192
+ Bug reports and pull requests are welcome on GitHub at https://github.com/joelmoss/froxy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/joelmoss/froxy/blob/master/CODE_OF_CONDUCT.md).
193
+
194
+ ## License
195
+
196
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
197
+
198
+ ## Code of Conduct
199
+
200
+ Everyone interacting in the Froxy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/joelmoss/froxy/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'froxy'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/froxy ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path')
4
+ const fs = require('fs')
5
+ const cli = require('cac')()
6
+ const esbuild = require('esbuild')
7
+ const crypto = require('crypto')
8
+
9
+ const parsed = cli.parse()
10
+ const [absWorkingDir, entryPoint] = parsed.args
11
+ const entryPointKey = crypto.createHash('sha1').update(entryPoint).digest('base64')
12
+
13
+ const { resolve, config } = require('../lib/froxy/esbuild/utils')
14
+ const loadStylePlugin = require('../lib/froxy/esbuild/plugins/load_style')
15
+ const envPlugin = require('../lib/froxy/esbuild/plugins/env')
16
+ const aliasPlugin = require('../lib/froxy/esbuild/plugins/alias')(absWorkingDir)
17
+ const cssPlugin = require('../lib/froxy/esbuild/plugins/css')(absWorkingDir)
18
+ const imagesPlugin = require('../lib/froxy/esbuild/plugins/images')(absWorkingDir)
19
+ const rootPlugin = require('../lib/froxy/esbuild/plugins/root')(absWorkingDir)
20
+
21
+ const testPlugin = {
22
+ name: 'froxy.test',
23
+ setup(build) {
24
+ build.onResolve({ filter: /.*/ }, args => {
25
+ console.log('onResolve', args)
26
+ })
27
+ build.onLoad({ filter: /.*/ }, args => {
28
+ console.log('onLoad', args)
29
+ })
30
+ }
31
+ }
32
+
33
+ const conf = config(absWorkingDir)
34
+
35
+ const buildOptions = {
36
+ absWorkingDir,
37
+ entryPoints: [entryPoint],
38
+ bundle: true,
39
+ target: conf.target,
40
+ minify: conf.minify,
41
+ inject: conf.inject,
42
+ sourcemap: conf.sourcemap,
43
+ format: 'esm',
44
+ splitting: true,
45
+ outdir: 'public/froxy/build',
46
+ outbase: '.',
47
+ logLevel: 'error',
48
+ define: {
49
+ global: 'globalThis',
50
+ 'process.env.NODE_ENV': `"${process.env.NODE_ENV || 'development'}"`,
51
+ 'process.env.RAILS_ENV': `"${process.env.RAILS_ENV || 'development'}"`
52
+ },
53
+ // metafile: `public/froxy/meta/${entryPointKey}.json`,
54
+ plugins: [aliasPlugin, envPlugin, loadStylePlugin, cssPlugin, imagesPlugin, rootPlugin]
55
+ }
56
+
57
+ esbuild.build(buildOptions).catch(() => {
58
+ process.exit(1)
59
+ })
60
+ // .then(results => {
61
+ // // console.log(results)
62
+ // const meta = JSON.parse(fs.readFileSync(`${absWorkingDir}/${buildOptions.metafile}`, 'utf8'))
63
+ // console.log(meta)
64
+ // // console.log(Object.keys(meta.inputs))
65
+ // // console.log(Object.keys(meta.outputs))
66
+
67
+ // process.stdout.write(buildOptions.metafile)
68
+ // // process.stdout.write(fs.readFileSync(`${absWorkingDir}/${buildOptions.metafile}`, 'utf8'))
69
+ // })
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config.ru ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.path = 'test/internal'
9
+ Combustion.initialize! :action_controller, :action_view
10
+ run Combustion::Application
data/froxy.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/froxy/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'froxy'
7
+ spec.version = Froxy::VERSION
8
+ spec.authors = ['Joel Moss']
9
+ spec.email = ['joel@developwithstyle.com']
10
+
11
+ spec.summary = 'Fast ESModule based Frontend Bundling for Rails'
12
+ spec.homepage = 'https://github.com/joelmoss/froxy'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_runtime_dependency 'activesupport', ['>= 5.0.0', '< 7.0']
30
+ end
data/lib/froxy.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view'
4
+ require 'active_support/dependencies/autoload'
5
+
6
+ module Froxy
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :Proxy
10
+ end
11
+
12
+ require 'froxy/railtie'
@@ -0,0 +1,47 @@
1
+ const { resolve, config } = require('../utils')
2
+
3
+ // esbuild plugin to support module aliases as defined in package.json. Supports local and node
4
+ // modules.
5
+ //
6
+ // In your package.json:
7
+ //
8
+ // "froxy": {
9
+ // "aliases": {
10
+ // "_": "lodash", // a node module
11
+ // "myalias": "/absolutle/path/to/alias.js", // local path
12
+ // }
13
+ // }
14
+ //
15
+ // Then import:
16
+ //
17
+ // import { map } from '_'
18
+ // import axios from 'myaxios'
19
+ //
20
+ module.exports = absWorkingDir => ({
21
+ name: 'froxy.alias',
22
+ setup(build) {
23
+ let map = []
24
+ let aliases = {}
25
+
26
+ try {
27
+ map = config(absWorkingDir).aliases
28
+ aliases = Object.keys(map)
29
+ } catch {
30
+ // Fail silently, as there is no package.json.
31
+ return
32
+ }
33
+
34
+ if (aliases.length > 0) {
35
+ const re = new RegExp(`^${aliases.map(x => escapeRegExp(x)).join('|')}$`)
36
+
37
+ build.onResolve({ filter: re }, async ({ resolveDir, path }) => ({
38
+ path: await resolve(absWorkingDir, resolveDir, map[path], { fallbackToEsbuild: true })
39
+ }))
40
+ }
41
+ }
42
+ })
43
+
44
+ function escapeRegExp(string) {
45
+ // $& means the whole matched string
46
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
47
+ }
@@ -0,0 +1,50 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ const { resolve } = require('../utils')
5
+
6
+ module.exports = absWorkingDir => ({
7
+ name: 'froxy.css',
8
+ setup(build) {
9
+ // Handle CSS imported from JS.
10
+ build.onResolve({ filter: /\.css$/ }, args => {
11
+ let resolvedPath = resolve(absWorkingDir, args.resolveDir, args.path)
12
+
13
+ if (resolvedPath === args.path) {
14
+ const nodeModulesDir = path.join(absWorkingDir, 'node_modules')
15
+
16
+ try {
17
+ fs.accessSync(nodeModulesDir, fs.constants.R_OK)
18
+ resolvedPath = path.join(nodeModulesDir, args.path)
19
+ } catch {
20
+ // Do nothing
21
+ }
22
+ }
23
+
24
+ return {
25
+ path: resolvedPath,
26
+ namespace: args.importer.endsWith('.js') ? 'cssFromJs' : 'file'
27
+ }
28
+ })
29
+
30
+ // Handles CSS imports from JS (eg `import from 'some.css'`) by simply marking it as external.
31
+ // This then allows the browser to handle the import. However, browsers do not yet support
32
+ // importing non-JS assets, and will not include the CSS. So the Froxy proxy will return the
33
+ // imported CSS as a JS file that inserts the CSS directly into the DOM. This unfortunately may
34
+ // result in a flash of unstyled content (FOUC).
35
+ //
36
+ // --- OR
37
+ //
38
+ // esbuild returns the content of both the JS and CSS. Then Froxy returns the JS as normal,
39
+ // and additionally includes the CSS directly into the rendered HTML. This way, there will be no
40
+ // FOUC. But this method is a little more complex, as Froxy will need to somehow pass the CSS
41
+ // content to Rails for insertion into the rendered view.
42
+ build.onLoad({ filter: /\.css$/, namespace: 'cssFromJs' }, args => ({
43
+ contents: `
44
+ import loadStyle from 'loadStyle'
45
+ loadStyle("${args.path.slice(absWorkingDir.length)}")
46
+ `,
47
+ loader: 'js'
48
+ }))
49
+ }
50
+ })
@@ -0,0 +1,19 @@
1
+ module.exports = {
2
+ name: 'froxy.env',
3
+ setup(build) {
4
+ // Intercept import paths called "env" so esbuild doesn't attempt
5
+ // to map them to a file system location. Tag them with the "env-ns"
6
+ // namespace to reserve them for this plugin.
7
+ build.onResolve({ filter: /^env$/ }, args => ({
8
+ path: args.path,
9
+ namespace: 'env-ns'
10
+ }))
11
+
12
+ // Load paths tagged with the "env-ns" namespace and behave as if
13
+ // they point to a JSON file containing the environment variables.
14
+ build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
15
+ contents: JSON.stringify(process.env),
16
+ loader: 'json'
17
+ }))
18
+ }
19
+ }
@@ -0,0 +1,32 @@
1
+ const { resolve } = require('../utils')
2
+
3
+ module.exports = absWorkingDir => ({
4
+ name: 'froxy.images',
5
+ setup(build) {
6
+ const IMAGE_TYPES = /\.(png|gif|jpe?g|svg|ico|webp|avif)$/
7
+
8
+ // Froxy proxy will render images directly. esbuild will just rewrite the path when an image is
9
+ // imported from JS, and embed the file name into the bundle as a string. This string is
10
+ // exported using the default export. Including an image in CSS using `url()`, will simply
11
+ // return the relative URL of the image.
12
+ build.onResolve({ filter: IMAGE_TYPES }, args => {
13
+ const resolvedPath = resolve(absWorkingDir, args.resolveDir, args.path)
14
+
15
+ if (args.importer.endsWith('.css')) {
16
+ return {
17
+ path: resolvedPath.slice(absWorkingDir.length),
18
+ external: true
19
+ }
20
+ } else {
21
+ return { path: resolvedPath }
22
+ }
23
+ })
24
+
25
+ build.onLoad({ filter: IMAGE_TYPES }, args => {
26
+ return {
27
+ contents: `export default '${args.path.slice(absWorkingDir.length)}';`,
28
+ loader: 'js'
29
+ }
30
+ })
31
+ }
32
+ })
@@ -0,0 +1,22 @@
1
+ module.exports = {
2
+ name: "froxy.loadStyle",
3
+
4
+ setup(build) {
5
+ build.onResolve({ filter: /^loadStyle$/ }, () => ({
6
+ path: "loadStyle",
7
+ namespace: "loadStyleShim",
8
+ }));
9
+
10
+ build.onLoad({ filter: /^loadStyle$/, namespace: "loadStyleShim" }, () => ({
11
+ contents: `
12
+ export default function (path) {
13
+ const ele = document.createElement('link')
14
+ ele.setAttribute('rel', 'stylesheet')
15
+ ele.setAttribute('media', 'all')
16
+ ele.setAttribute('href', path)
17
+ document.head.appendChild(ele)
18
+ }
19
+ `,
20
+ }));
21
+ },
22
+ };
@@ -0,0 +1,14 @@
1
+ const path = require('path')
2
+
3
+ module.exports = absWorkingDir => ({
4
+ name: 'froxy.root',
5
+ setup(build) {
6
+ // Resolves paths starting with a `/` to the Rails root.
7
+ //
8
+ // Example:
9
+ // import '/my/lib.js' //-> import '{Rails.root}/my/lib.js'
10
+ build.onResolve({ filter: /^\// }, args => ({
11
+ path: path.join(absWorkingDir, args.path)
12
+ }))
13
+ }
14
+ })
@@ -0,0 +1,83 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const esbuild = require('esbuild')
4
+
5
+ // Resolve the given path (`p`) to an absolute path.
6
+ const resolve = (absWorkingDir, b, p, options = {}) => {
7
+ options = {
8
+ fallbackToEsbuild: false,
9
+ ...options
10
+ }
11
+
12
+ if (p.startsWith('/')) return path.resolve(absWorkingDir, p.slice(1))
13
+ if (p.startsWith('.')) return path.resolve(b, p)
14
+
15
+ if (options.fallbackToEsbuild) {
16
+ return (async () => await esbuildResolve(p, b))()
17
+ }
18
+
19
+ return p
20
+ }
21
+
22
+ const config = absWorkingDir => {
23
+ let packageConfig = {}
24
+ const defaultConfig = {
25
+ minify: false,
26
+ sourcemap: true
27
+ }
28
+
29
+ try {
30
+ const pkg = fs.readFileSync(path.join(absWorkingDir, 'package.json'))
31
+ packageConfig = JSON.parse(pkg).froxy
32
+ } catch {
33
+ // Fail silently, as there is no package.json.
34
+ }
35
+
36
+ return { ...defaultConfig, ...packageConfig }
37
+ }
38
+
39
+ // Resolves a module using esbuild module resolution.
40
+ //
41
+ // @param {string} id Module to resolve
42
+ // @param {string} [resolveDir] The directory to resolve from
43
+ // @returns {string} The resolved module
44
+ async function esbuildResolve(id, resolveDir = process.cwd()) {
45
+ let _resolve
46
+ const resolvedPromise = new Promise(resolve => (_resolve = resolve))
47
+ return Promise.race([
48
+ resolvedPromise,
49
+ esbuild
50
+ .build({
51
+ sourcemap: false,
52
+ write: false,
53
+ bundle: true,
54
+ format: 'esm',
55
+ logLevel: 'silent',
56
+ platform: 'node',
57
+ stdin: {
58
+ contents: `import ${JSON.stringify(id)}`,
59
+ loader: 'js',
60
+ resolveDir,
61
+ sourcefile: __filename
62
+ },
63
+ plugins: [
64
+ {
65
+ name: 'esbuildResolve',
66
+ setup(build) {
67
+ build.onLoad({ filter: /.*/ }, ({ path }) => {
68
+ id = path
69
+ _resolve(id)
70
+ return { contents: '' }
71
+ })
72
+ }
73
+ }
74
+ ]
75
+ })
76
+ .then(() => id)
77
+ ])
78
+ }
79
+
80
+ module.exports = {
81
+ resolve,
82
+ config
83
+ }
@@ -0,0 +1,11 @@
1
+ require 'action_view'
2
+
3
+ module Froxy
4
+ module Helper
5
+ include ActionView::Helpers::AssetUrlHelper
6
+
7
+ def compute_asset_path(source, _options = {})
8
+ File.join('', source)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/log_subscriber'
4
+
5
+ module Froxy
6
+ class LogSubscriber < ActiveSupport::LogSubscriber
7
+ VIEWS_PATTERN = %r{^app/views/}.freeze
8
+
9
+ def side_loaded_assets(event)
10
+ return if (asset_types = event.payload[:asset_types]).empty?
11
+
12
+ identifier_from_root = from_rails_root(event.payload[:identifier])
13
+
14
+ info do
15
+ message = +" Side loaded #{asset_types.join(',')} for #{identifier_from_root}"
16
+ message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ EMPTY = ''
23
+ def from_rails_root(string)
24
+ string = string.sub(rails_root, EMPTY)
25
+ string.sub!(VIEWS_PATTERN, EMPTY)
26
+ string
27
+ end
28
+
29
+ def rails_root
30
+ @rails_root ||= "#{Rails.root}/"
31
+ end
32
+ end
33
+ end
34
+
35
+ Froxy::LogSubscriber.attach_to :action_view
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'froxy/log_subscriber'
4
+
5
+ module Froxy
6
+ module Monkey
7
+ module SideLoadAssets
8
+ def render_template(view, template, layout_name, locals)
9
+ return super if template.type != :html
10
+
11
+ # Side load layout assets - if any.
12
+ if layout_name
13
+ layout = find_layout(layout_name, locals.keys, [formats.first])
14
+ layout && side_load_assets(view, layout)
15
+ end
16
+
17
+ # Side load view assets - if any.
18
+ side_load_assets view, template
19
+
20
+ super
21
+ end
22
+
23
+ private
24
+
25
+ def side_load_assets(view, tpl)
26
+ path = tpl.short_identifier.delete_suffix('.html.erb')
27
+
28
+ instrument :side_loaded_assets, identifier: tpl.identifier, asset_types: [] do |payload|
29
+ side_load_js path, view, payload
30
+ side_load_css path, view, payload
31
+ end
32
+ end
33
+
34
+ def side_load_js(path, view, log_payload)
35
+ # Check that the file actually exists.
36
+ return unless Rails.root.join(path).sub_ext('.js').exist?
37
+
38
+ view.content_for :side_loaded_js do
39
+ view.javascript_include_tag(path, type: :module).tap do |tag|
40
+ !tag.nil? && (log_payload[:asset_types] << :js)
41
+ end
42
+ end
43
+ end
44
+
45
+ def side_load_css(path, view, log_payload)
46
+ # Check that the file actually exists.
47
+ return unless Rails.root.join(path).sub_ext('.css').exist?
48
+
49
+ view.content_for :side_loaded_css do
50
+ view.stylesheet_link_tag(path).tap do |tag|
51
+ !tag.nil? && (log_payload[:asset_types] << :css)
52
+ end
53
+ end
54
+ end
55
+
56
+ def instrument(action, payload, &block)
57
+ ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'rack/utils'
5
+
6
+ # Proxies files to esbuild.
7
+ module Froxy
8
+ class Proxy
9
+ BUILD_PATH = 'public/froxy/build'
10
+ CLI = File.expand_path('../../bin/froxy', __dir__)
11
+ IMAGE_TYPES = /\.(png|gif|jpeg|jpg|svg|ico|webp|avif)$/i.freeze
12
+ FILE_EXT_MAP = {
13
+ '.jsx' => '.js'
14
+ }.freeze
15
+
16
+ def initialize(app)
17
+ @app = app
18
+ @file_server = Rack::Files.new(Rails.root)
19
+ @build_file_server = Rack::Files.new(Rails.root.join(BUILD_PATH))
20
+ end
21
+
22
+ def call(env)
23
+ req = Rack::Request.new(env)
24
+ path_info = req.path_info
25
+
26
+ if req.get? || req.head?
27
+ # Let images through.
28
+ return @file_server.call(env) if IMAGE_TYPES.match?(path_info)
29
+
30
+ # Let JS sourcemaps through.
31
+ return @build_file_server.call(env) if /\.js\.map$/i.match?(path_info)
32
+
33
+ # Let esbuild handle JS and CSS.
34
+ if /\.(js|jsx|css)$/i.match?(path_info)
35
+ return unless (path = clean_path(path_info))
36
+ return [404, {}, []] unless file_readable?(path)
37
+ return @file_server.call(env) unless Rails.application.config.froxy.use_esbuild
38
+
39
+ return build env, req, path
40
+ end
41
+ end
42
+
43
+ @app.call req.env
44
+ end
45
+
46
+ private
47
+
48
+ def path_to_file(env, request, path)
49
+ ext = Pathname.new(path).extname
50
+ request.path_info = path.sub(/#{ext}$/, FILE_EXT_MAP[ext]) if FILE_EXT_MAP.key?(ext)
51
+
52
+ @build_file_server.call env
53
+ end
54
+
55
+ # Build the file from the given `path` using ESbuild. Returns a Rack::Response.
56
+ def build(env, request, path)
57
+ stdout, stderr, status = Open3.capture3(CLI, Rails.root.to_s, path)
58
+
59
+ if status.success?
60
+ Rails.logger.info "[froxy] built #{path}"
61
+ raise "[froxy] build failed: #{stderr}" unless stderr.empty?
62
+ else
63
+ non_empty_streams = [stdout, stderr].delete_if(&:empty?)
64
+ raise "[froxy] build failed:\n#{non_empty_streams.join("\n\n")}"
65
+ end
66
+
67
+ path_to_file env, request, path
68
+ end
69
+
70
+ def file_readable?(path)
71
+ file_stat = File.stat(Rails.root.join(path.delete_prefix('/').b).to_s)
72
+ rescue SystemCallError
73
+ false
74
+ else
75
+ file_stat.file? && file_stat.readable?
76
+ end
77
+
78
+ def clean_path(path_info)
79
+ path = Rack::Utils.unescape_path path_info.chomp('/').delete_prefix('/')
80
+ Rack::Utils.clean_path_info path if Rack::Utils.valid_path? path
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Froxy
6
+ class Railtie < ::Rails::Railtie
7
+ config.froxy = ActiveSupport::OrderedOptions.new
8
+
9
+ initializer 'froxy.configuration' do |app|
10
+ options = app.config.froxy
11
+
12
+ options.use_proxy = true if options.use_proxy.nil?
13
+ options.use_esbuild = true if options.use_esbuild.nil?
14
+ options.side_load_assets = true if options.side_load_assets.nil?
15
+ end
16
+
17
+ initializer 'froxy.proxy' do |app|
18
+ next unless app.config.froxy.use_proxy
19
+
20
+ app.middleware.insert_after ActionDispatch::Static, Froxy::Proxy
21
+ end
22
+
23
+ initializer 'froxy.side_load_assets' do |app|
24
+ next unless app.config.froxy.side_load_assets
25
+
26
+ ActiveSupport.on_load :action_view do
27
+ require 'froxy/monkey/side_load_assets'
28
+ ActionView::TemplateRenderer.prepend Froxy::Monkey::SideLoadAssets
29
+
30
+ require 'froxy/helper'
31
+ include Froxy::Helper
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Froxy
4
+ VERSION = '0.1.0'
5
+ end
data/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "froxy",
3
+ "private": true,
4
+ "prettier": {
5
+ "trailingComma": "none",
6
+ "printWidth": 100,
7
+ "semi": false,
8
+ "singleQuote": true,
9
+ "arrowParens": "avoid"
10
+ },
11
+ "dependencies": {
12
+ "cac": "^6.7.1",
13
+ "esbuild": "^0.8.1"
14
+ }
15
+ }
data/yarn.lock ADDED
@@ -0,0 +1,13 @@
1
+ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
+ # yarn lockfile v1
3
+
4
+
5
+ cac@^6.7.1:
6
+ version "6.7.2"
7
+ resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.2.tgz#e7f0d21f4776c46c7d0de7976e56fa5562e17597"
8
+ integrity sha512-w0bH1IF9rEjdi0a6lTtlXYT+vBZEJL9oytaXXRdsD68MH6+SrZGOGsu7s2saHQvYXqwo/wBdkW75tt8wFpj+mw==
9
+
10
+ esbuild@^0.8.1:
11
+ version "0.8.48"
12
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5"
13
+ integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A==
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: froxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Moss
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-02-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 5.0.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ description:
34
+ email:
35
+ - joel@developwithstyle.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - ".gitignore"
41
+ - ".rubocop.yml"
42
+ - CODE_OF_CONDUCT.md
43
+ - Gemfile
44
+ - Gemfile.lock
45
+ - LICENSE.txt
46
+ - README.md
47
+ - Rakefile
48
+ - bin/console
49
+ - bin/froxy
50
+ - bin/setup
51
+ - config.ru
52
+ - froxy.gemspec
53
+ - lib/froxy.rb
54
+ - lib/froxy/esbuild/plugins/alias.js
55
+ - lib/froxy/esbuild/plugins/css.js
56
+ - lib/froxy/esbuild/plugins/env.js
57
+ - lib/froxy/esbuild/plugins/images.js
58
+ - lib/froxy/esbuild/plugins/load_style.js
59
+ - lib/froxy/esbuild/plugins/root.js
60
+ - lib/froxy/esbuild/utils.js
61
+ - lib/froxy/helper.rb
62
+ - lib/froxy/log_subscriber.rb
63
+ - lib/froxy/monkey/side_load_assets.rb
64
+ - lib/froxy/proxy.rb
65
+ - lib/froxy/railtie.rb
66
+ - lib/froxy/version.rb
67
+ - package.json
68
+ - yarn.lock
69
+ homepage: https://github.com/joelmoss/froxy
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ homepage_uri: https://github.com/joelmoss/froxy
74
+ source_code_uri: https://github.com/joelmoss/froxy
75
+ changelog_uri: https://github.com/joelmoss/froxy/releases
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 2.7.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.2.3
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Fast ESModule based Frontend Bundling for Rails
95
+ test_files: []