froxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []