isorun 0.1.0.pre-x86_64-darwin
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/Rakefile +80 -0
- data/app/helpers/isorun/app_helper.rb +38 -0
- data/ext/isorun/Cargo.lock +327 -0
- data/ext/isorun/Cargo.toml +27 -0
- data/ext/isorun/extconf.rb +6 -0
- data/ext/isorun/src/call.js +27 -0
- data/ext/isorun/src/isorun/configure.rs +9 -0
- data/ext/isorun/src/isorun/context.rs +46 -0
- data/ext/isorun/src/isorun/function.rs +60 -0
- data/ext/isorun/src/isorun/mod.rs +7 -0
- data/ext/isorun/src/isorun/module.rs +33 -0
- data/ext/isorun/src/isorun/utils.rs +156 -0
- data/ext/isorun/src/js/mod.rs +3 -0
- data/ext/isorun/src/js/module.rs +51 -0
- data/ext/isorun/src/js/module_item.rs +71 -0
- data/ext/isorun/src/js/worker.rs +265 -0
- data/ext/isorun/src/lib.rs +51 -0
- data/lib/isorun/2.7/isorun.bundle +0 -0
- data/lib/isorun/3.0/isorun.bundle +0 -0
- data/lib/isorun/3.1/isorun.bundle +0 -0
- data/lib/isorun/config/abstract_builder.rb +28 -0
- data/lib/isorun/config/option.rb +82 -0
- data/lib/isorun/config/validations.rb +12 -0
- data/lib/isorun/config.rb +43 -0
- data/lib/isorun/context.rb +84 -0
- data/lib/isorun/engine.rb +20 -0
- data/lib/isorun/function.rb +6 -0
- data/lib/isorun/module.rb +6 -0
- data/lib/isorun/resolver.rb +21 -0
- data/lib/isorun/version.rb +5 -0
- data/lib/isorun.rb +29 -0
- metadata +172 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f6ba42c88d6b859550c970a2093723eefa763e74d14c49b0b63ff5154d1ff003
|
4
|
+
data.tar.gz: 401bf887c7f8efba03285e4c79708df1a57dfb05a579ea7301612d54cd6448e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 984acc2e6755a5290a099a8125fbeda42c21e2f53355b0ca94521e437e2e0f23283bc0ceed35c0fea59635d321645a36ab8a5c0eb8bc9f39b429f65b99419896
|
7
|
+
data.tar.gz: 7b09a4686edae82becb6fc1acc003ee16af5dc608bf926a22b5db4139f1c5dfd70b9e7b7a4b7ec83992d3cdf23ea9f7a1ea2324fbd5fe05c288ed8fd702129b8
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022 Hannes Moser
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<img alt="isorun" src="./docs/assets/logo.png" width="200" />
|
3
|
+
</p>
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
> Run JavaScript applications in your Rails application.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
* Import JavaScript functions, objects, or just values and use them in Ruby
|
12
|
+
* An EMCAScript like Ruby DSL to load modules and import items
|
13
|
+
* Automatically converts arguments and return values
|
14
|
+
* Send messages between *JavaScript*<->*Ruby* (allows to intercept network requests and avoid network round-trips for e.g. API calls)
|
15
|
+
* Automatically reload modules when updated in development
|
16
|
+
* Automatically extracts state (Apollo) and hydrates client-side
|
17
|
+
* Supports server-side rendering of multiple apps on a single page
|
18
|
+
* Examples for [React](./examples/rails-react-app), [Vue](./examples/rails-vue-app), [D3](./examples/rails-d3-app) and a [multi-app](./examples/rails-multi-app) setup
|
19
|
+
|
20
|
+
## How to
|
21
|
+
|
22
|
+
### Plain JavaScript
|
23
|
+
|
24
|
+
```js
|
25
|
+
// module.js
|
26
|
+
export function say(word) {
|
27
|
+
return word;
|
28
|
+
}
|
29
|
+
```
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
context = Isorun::Context.new
|
33
|
+
|
34
|
+
# import `export function say` from module
|
35
|
+
say = context.import(:say).from("./module.js")
|
36
|
+
say.call("Hello!") # "Hello!"
|
37
|
+
```
|
38
|
+
|
39
|
+
### Simple React app
|
40
|
+
|
41
|
+
```bash
|
42
|
+
rails new myproject --javascript esbuild
|
43
|
+
cd myproject
|
44
|
+
```
|
45
|
+
|
46
|
+
```js
|
47
|
+
// package.json
|
48
|
+
{
|
49
|
+
"scripts": {
|
50
|
+
"build": "esbuild app/javascript/app.jsx --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
|
51
|
+
"build-server": "esbuild app/javascript/app-server.jsx --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --format=esm"
|
52
|
+
}
|
53
|
+
}
|
54
|
+
```
|
55
|
+
|
56
|
+
```bash
|
57
|
+
# Procfile.dev
|
58
|
+
web: bin/rails server -p 3000
|
59
|
+
js: yarn build --watch
|
60
|
+
ssr: yarn build-server --watch
|
61
|
+
```
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
# config/initializers/isorun.rb
|
65
|
+
Isorun.configure do
|
66
|
+
# …configure isorun
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
```jsx
|
71
|
+
// app/javascript/my_app.jsx
|
72
|
+
import * as React from "react";
|
73
|
+
import {hydrateRoot} from "react-dom/client";
|
74
|
+
|
75
|
+
import {App} from "./my_app/App.jsx";
|
76
|
+
|
77
|
+
const container = document.querySelector('#my_app');
|
78
|
+
hydrateRoot(container, <App/>);
|
79
|
+
|
80
|
+
```
|
81
|
+
|
82
|
+
```jsx
|
83
|
+
// app/javascript/my_app-server.jsx
|
84
|
+
import * as React from "react";
|
85
|
+
import * as Server from "react-dom/server";
|
86
|
+
|
87
|
+
import {App} from "./my_app/App.jsx";
|
88
|
+
|
89
|
+
export default async function() {
|
90
|
+
return Server.renderToString(<App/>);
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
```erb
|
95
|
+
<!--my_view.html.erb-->
|
96
|
+
<%= isorun_app("my_app") %>
|
97
|
+
```
|
98
|
+
|
99
|
+
## Ruby and platform support
|
100
|
+
|
101
|
+
Ruby versions:
|
102
|
+
- `2.7`
|
103
|
+
- `3.0`
|
104
|
+
- `3.1`.
|
105
|
+
|
106
|
+
Platforms and architectures:
|
107
|
+
- `x86_64-linux`
|
108
|
+
- `x86_64-apple`
|
109
|
+
- `arm64-apple`
|
110
|
+
|
111
|
+
## Demo
|
112
|
+
|
113
|
+
You can also check out this demo video on YouTube. It shows how you can utilize
|
114
|
+
*isorun* to render SVGs with Ruby on the server, utilizing JavaScript and the
|
115
|
+
D3 library.
|
116
|
+
|
117
|
+
[![How to use d3 in Ruby](./docs/assets/how-to-use-d3-in-ruby.png)](https://www.youtube.com/watch?v=EPHX4po4X4g)
|
118
|
+
|
119
|
+
## Why server-side rendering (SSR)?
|
120
|
+
|
121
|
+
The fastest way to deliver an application to the user is streaming HTML directly
|
122
|
+
to the browser. The slowest way to deliver an application, is downloading a
|
123
|
+
JavaScript file first, parse and execute it on the client side.
|
124
|
+
|
125
|
+
Server-side rendering is taking advantage of the fact that we can render a
|
126
|
+
JavaScript application directly on the server, and stream the resulting HTML
|
127
|
+
directly to the browser.
|
128
|
+
Then we fetch the JavaScript file and eventually the application will
|
129
|
+
(re-)hydrate the already rendered user interface.
|
130
|
+
|
131
|
+
You can take this concept even further and make your application work without
|
132
|
+
JavaScript at all, but still use React or Vue (or any other view-controller
|
133
|
+
library) to define your user interface.
|
134
|
+
|
135
|
+
Read
|
136
|
+
more: [Netflix functions without client-side React, and it's a good thing](https://jakearchibald.com/2017/netflix-and-react/).
|
137
|
+
|
138
|
+
Server-side rendering has a few challenges:
|
139
|
+
|
140
|
+
1. You need something that can compile and run JavaScript
|
141
|
+
1. You need to be able to integrate the app with your preferred framework
|
142
|
+
1. You need to deal with the reality of frontend clients making network requests and managing state
|
143
|
+
|
144
|
+
**isorun** aims to make it as simple as possible to integrate a
|
145
|
+
JavaScript application into your server-side development and deployment
|
146
|
+
workflow, without changing the development workflow for frontend engineers.
|
147
|
+
|
148
|
+
This gem provides a helper that can render a JavaScript application directly in
|
149
|
+
your Ruby process, embedding Google's *v8* library via [*deno_core*](https://crates.io/crates/deno_core).
|
150
|
+
You can think of it as running a headless JavaScript browser directly in your
|
151
|
+
Ruby process (threads). Using *v8* allows us to completely separate the
|
152
|
+
execution environments between individual renders and therefore prevent any
|
153
|
+
potential [Cross-Request State Pollution](https://vuejs.org/guide/scaling-up/ssr.html#cross-request-state-pollution).
|
154
|
+
It is essentiallly the same as opening many tabs in one browser.
|
155
|
+
|
156
|
+
## Why SSR for Ruby (on Rails)?
|
157
|
+
|
158
|
+
I personally enjoy and use *Ruby on Rails* a lot, but I like to use some
|
159
|
+
Vue and React for frontend work. The integration of frontend and backend always
|
160
|
+
felt a bit off, and I wanted something that "just works" for most of my use
|
161
|
+
cases.
|
162
|
+
|
163
|
+
One goal of **isorun** is that server-side rendering should feel naturally in
|
164
|
+
Ruby and Rails. A simple tag helper should be enough to render, deliver, and
|
165
|
+
hydrate your complex JavaScript application. And if we want to do something
|
166
|
+
nice with visualization libraries, it should be possible to run any JavaScript
|
167
|
+
program and return the result to the user without spinning up a separate
|
168
|
+
service.
|
169
|
+
|
170
|
+
### Alternatives
|
171
|
+
|
172
|
+
#### "No" JavaScript
|
173
|
+
|
174
|
+
If you want to go all-in on the server side, I highly recommend taking a look at
|
175
|
+
[HTML over the Wire](https://hotwired.dev/), and [StimulusReflex](https://docs.stimulusreflex.com/).
|
176
|
+
|
177
|
+
#### Run a Node.js, deno, or bun service
|
178
|
+
|
179
|
+
**isorun** does SSR a bit different from how you would do it in a regular
|
180
|
+
Node.js service. In addition to being able to render the application, it also
|
181
|
+
supports more powerful features like network intercepts. This means, that you
|
182
|
+
can directly call into the Ruby process from the JavaScript application and
|
183
|
+
e.g. fetch data from the database. This is helpful for applications that
|
184
|
+
utilize APIs to fetch their data.
|
185
|
+
Even when server-side rendered, these applications issue network requests
|
186
|
+
against the production API endpoints to get access to data. In a lot of cases,
|
187
|
+
we can accelerate this process by forwarding the network requests directly to
|
188
|
+
the target controller/action in Rails.Instead of fetching
|
189
|
+
|
190
|
+
**Example** A React applications queries a Rails GraphQL API
|
191
|
+
|
192
|
+
We can override the HttpLink `fetch` method and utilize the `@isorun/rails`
|
193
|
+
package to send the HTTP request for the GraphQL API directly to the Ruby
|
194
|
+
process, instead of sending it over the network.
|
195
|
+
|
196
|
+
```js
|
197
|
+
import {apollo} from "@isorun/rails";
|
198
|
+
|
199
|
+
import {App} from "../my_app/App.jsx";
|
200
|
+
|
201
|
+
const apolloClient = new ApolloClient({
|
202
|
+
ssrMode: true,
|
203
|
+
cache: new InMemoryCache(),
|
204
|
+
link: new HttpLink({
|
205
|
+
uri: 'http://localhost:3000/graphql',
|
206
|
+
fetch: apollo.fetch
|
207
|
+
})
|
208
|
+
});
|
209
|
+
```
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
Isorun.configure do
|
213
|
+
receiver do |request|
|
214
|
+
query, variables, context, operation_name = parse(request)
|
215
|
+
|
216
|
+
RailsAppSchema.execute(
|
217
|
+
query,
|
218
|
+
variables: variables,
|
219
|
+
context: context,
|
220
|
+
operation_name: operation_name
|
221
|
+
)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
## Installation
|
227
|
+
|
228
|
+
Install the gem and add to the application's Gemfile by executing:
|
229
|
+
|
230
|
+
$ bundle add isorun
|
231
|
+
|
232
|
+
If bundler is not being used to manage dependencies, install the gem by
|
233
|
+
executing:
|
234
|
+
|
235
|
+
$ gem install isorun
|
236
|
+
|
237
|
+
## Development
|
238
|
+
|
239
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
240
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
241
|
+
prompt that will allow you to experiment.
|
242
|
+
|
243
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
244
|
+
release a new version, update the version number in `version.rb`, and then run
|
245
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
246
|
+
git commits and the created tag, and push the `.gem` file to
|
247
|
+
[rubygems.org](https://rubygems.org).
|
248
|
+
|
249
|
+
## Contributing
|
250
|
+
|
251
|
+
Bug reports and pull requests are welcome on GitHub at
|
252
|
+
https://github.com/eliias/isorun.
|
253
|
+
|
254
|
+
## License
|
255
|
+
|
256
|
+
The gem is available as open source under the terms of the
|
257
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rubygems/package_task"
|
5
|
+
require "rake/testtask"
|
6
|
+
require "rake/extensiontask"
|
7
|
+
require "rb_sys"
|
8
|
+
|
9
|
+
cross_rubies = %w[3.1.0 3.0.0 2.7.0]
|
10
|
+
cross_platforms = %w[
|
11
|
+
arm64-darwin
|
12
|
+
x86_64-darwin
|
13
|
+
x86_64-linux
|
14
|
+
]
|
15
|
+
|
16
|
+
spec = Bundler.load_gemspec("isorun.gemspec")
|
17
|
+
|
18
|
+
Gem::PackageTask.new(spec).define
|
19
|
+
|
20
|
+
Rake::ExtensionTask.new("isorun", spec) do |ext|
|
21
|
+
ext.source_pattern = "*.{rs,toml}"
|
22
|
+
ext.lib_dir = "lib/isorun"
|
23
|
+
ext.cross_compile = true
|
24
|
+
ext.cross_platform = cross_platforms
|
25
|
+
ext.config_script = ENV["ALTERNATE_CONFIG_SCRIPT"] || "extconf.rb"
|
26
|
+
ext.cross_compiling do |c|
|
27
|
+
c.files.reject! { |file| File.fnmatch?("*.tar.gz", file) }
|
28
|
+
c.dependencies.reject! { |dep| dep.name == "rb-sys" }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
namespace "gem" do
|
33
|
+
task "prepare" do # rubocop:disable Rails/RakeEnvironment
|
34
|
+
sh "bundle"
|
35
|
+
end
|
36
|
+
|
37
|
+
cross_platforms.each do |plat|
|
38
|
+
desc "Build the native gem for #{plat}"
|
39
|
+
task plat => "prepare" do
|
40
|
+
require "rake_compiler_dock"
|
41
|
+
|
42
|
+
ENV["RCD_IMAGE"] = "rbsys/#{plat}:#{RbSys::VERSION}"
|
43
|
+
|
44
|
+
RakeCompilerDock.sh <<~SH, platform: plat
|
45
|
+
bundle && \
|
46
|
+
RUBY_CC_VERSION="#{cross_rubies.join(":")}" \
|
47
|
+
rake native:#{plat} pkg/#{spec.full_name}-#{plat}.gem
|
48
|
+
SH
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
require "rspec/core/rake_task"
|
55
|
+
RSpec::Core::RakeTask.new(:spec, [] => [:compile])
|
56
|
+
task test: :spec
|
57
|
+
task default: %i[test]
|
58
|
+
rescue LoadError
|
59
|
+
# Ok
|
60
|
+
end
|
61
|
+
|
62
|
+
begin
|
63
|
+
require "rubocop/rake_task"
|
64
|
+
|
65
|
+
RuboCop::RakeTask.new
|
66
|
+
rescue LoadError
|
67
|
+
# Ok
|
68
|
+
end
|
69
|
+
|
70
|
+
begin
|
71
|
+
require "yard"
|
72
|
+
|
73
|
+
YARD::Rake::YardocTask.new
|
74
|
+
|
75
|
+
task docs: :environment do
|
76
|
+
`yard server --reload`
|
77
|
+
end
|
78
|
+
rescue LoadError
|
79
|
+
# Ok
|
80
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Isorun
|
4
|
+
module AppHelper
|
5
|
+
def isorun_app(id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
6
|
+
module_path = Isorun.configuration.module_resolver.call(id)
|
7
|
+
|
8
|
+
ssr_html = Isorun::Context.create do |context|
|
9
|
+
render_context = { environment: Rails.env.to_s }
|
10
|
+
render_function = context.import.from(module_path)
|
11
|
+
|
12
|
+
if render_function.blank?
|
13
|
+
Rails.logger.warn("[ISORUN] the requested app does not exist or " \
|
14
|
+
"does not have a server entrypoint. Please " \
|
15
|
+
"check if an asset with filename " + "
|
16
|
+
`#{id}-server.js` exists.")
|
17
|
+
end
|
18
|
+
|
19
|
+
Isorun.with_receiver(Isorun.configuration.receiver) do
|
20
|
+
render_function.call_without_gvl(render_context)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
html = if ssr_html.present?
|
25
|
+
tag.div id: id do
|
26
|
+
ssr_html.html_safe # rubocop:disable Rails/OutputSafety
|
27
|
+
end
|
28
|
+
else
|
29
|
+
Rails.logger.warn("[ISORUN] The server-side rendered result is empty.")
|
30
|
+
""
|
31
|
+
end
|
32
|
+
|
33
|
+
html += "\n"
|
34
|
+
html += javascript_include_tag(id, defer: true)
|
35
|
+
html.html_safe # rubocop:disable Rails/OutputSafety
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|