isorun 0.1.0.pre-arm64-darwin
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 +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: f40fd29fe6e6af7cea7300d59bfbc8a952e5057134f8572085fbb0d5b82714ea
|
4
|
+
data.tar.gz: 24a68c47f8da2a0b3bb1500bc7f9df0c52d3ec0593518dbccb1d78cc8ce8df34
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91cf98f13188aeb1dd2d702d44eb584e5d28ae21533c500387fe5348d9d4646b28f4eb6e8b7479968f1d944a911ed2b601d11a58eae9ba009f8a5064739d0add
|
7
|
+
data.tar.gz: ff48f42eee5c832796ee7da6a0a2928dbcdafa168b7a199cae2ffea26100da617c92fd3ecc6c435c73e6c49707b61990db6aa5291d14e83f775584b2e977b08d
|
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
|
+
[](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
|