Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ main
(`ember-cli >= 6.8`), without `ember-cli-rails-addon`
* Require `ember-cli-rails-assets >= 0.8.0`, which adds Vite support to
`include_ember_script_tags`
* Serve Vite-based applications from Vite's development server in
`development`, so that changes are hot-reloaded instead of requiring a
restart. Configure it with the `dev_server` option, or opt out with
`dev_server: false`

0.12.3
------
Expand Down
78 changes: 71 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ c.app :frontend, path: "~/projects/my-ember-app"

- `yarn` - enables the [yarn](https://github.com/yarnpkg/yarn) package manager when installing dependencies

- `dev_server` - configures [Vite's development server](#vite-based-applications)
for Vite-based applications in `development`. Pass `false` to opt out of it,
or a Hash of `host`, `port`, and `timeout` settings.

```ruby
EmberCli.configure do |c|
c.app :adminpanel # path defaults to `Rails.root.join("adminpanel")`
Expand Down Expand Up @@ -176,16 +180,63 @@ suites, configure the `default` task to depend on both `spec` and `ember:test`.
task default: [:spec, "ember:test"]
```

**Vite-based applications**
### Vite-based applications

When Rails is running in development mode, classic (Broccoli-based) Ember
applications are built with `ember build --watch`, so changes to the Ember
application are picked up automatically.

Vite-based Ember applications (generated with `ember-cli >= 6.8`) are instead
built once, synchronously, when they are first requested. To pick up changes
to the Ember application, restart the Rails server, or iterate on the Ember
application directly with its own development server (`npm start`).
Vite-based Ember applications (generated with `ember-cli >= 6.8`) are served by
Vite's own development server instead — the same one the application's
`npm start` script runs. `ember-cli-rails` starts it on the first request,
waits for it to accept connections, and shuts it down when Rails exits.

Rails still renders the application's `index.html`, but reads it from the
development server rather than from disk. The root-relative URLs in that
document are rewritten to point at the development server, so the browser
loads the application's modules — and Vite's HMR client — from it directly.
Changes to the Ember application are hot-reloaded without reloading the page,
and without restarting Rails.

Assets that the Ember application references relatively (an `<img>` in a
template, for instance) are still requested from Rails, which proxies them to
the development server.

**Configuring the development server**

By default the development server listens on an available port on `127.0.0.1`,
and `ember-cli-rails` waits up to 30 seconds for it to start:

```rb
EmberCli.configure do |c|
c.app :frontend, dev_server: { host: "127.0.0.1", port: 4200, timeout: 60 }
end
```

If something is already listening on the configured `host` and `port`,
`ember-cli-rails` uses it instead of starting a second server. That makes it
possible to run `npm start` yourself, on a port the initializer names, and have
Rails serve the application from it.

The development server's output is written to
`log/ember-<app>.<environment>.log`.

**Opting out**

To build the application once, synchronously, on the first request instead of
running a development server, disable it:

```rb
EmberCli.configure do |c|
c.app :frontend, dev_server: false
end
```

Changes to the Ember application are then only picked up by restarting the
Rails server.

The development server is only used in `development`. `test` and `production`
are served from the output of `ember build`, as they always have been.

## Deploy

Expand Down Expand Up @@ -472,6 +523,12 @@ and `modulepreload` links, and the ES module script tags — extracted from
the generated `index.html`. `include_ember_stylesheet_tags` only supports
classic (Broccoli-based) applications.

The asset helpers always read the output of `ember build`, so they do not use
[Vite's development server](#vite-based-applications). To serve a Vite-based
application from it, render the application with `render_ember_app`. Otherwise,
disable the development server with `dev_server: false` so that the assets the
helpers refer to are built.

Following the example above, configure the mounted EmberCLI application to be
served by a custom controller (`ApplicationController`, in this case).

Expand Down Expand Up @@ -627,6 +684,11 @@ make sure it's configured to run a single worker process.
Without restricting the server to a single process, [it is possible for multiple
EmberCLI runners to clobber each others' work][#94].

This does not apply to Vite-based applications served by [Vite's development
server](#vite-based-applications): nothing is written to a shared build
directory. Give the application a fixed `port` so that the workers share a
single development server rather than starting one each.

[Puma]: https://github.com/puma/puma
[Unicorn]: https://rubygems.org/gems/unicorn
[#94]: https://github.com/tricknotes/ember-cli-rails/issues/94#issuecomment-77627453
Expand Down Expand Up @@ -689,8 +751,10 @@ Note the following limitations for Vite-based applications:
* `include_ember_script_tags` emits the full set of tags the application
needs to boot, stylesheets included; `include_ember_stylesheet_tags` is
classic-only and must not be called for Vite-based applications
* in development, the application is built synchronously on first request
instead of being rebuilt on file changes
* in development, the application is served by [Vite's development
server](#vite-based-applications) rather than out of a build directory. The
asset helpers read that build directory, so they require the development
server to be disabled with `dev_server: false`

## Ruby and Rails support

Expand Down
27 changes: 23 additions & 4 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,29 @@ following differences in how `ember-cli-rails` treats it:
configuration meta tag, the stylesheet links, and the module script tags
all together, while `include_ember_stylesheet_tags` supports only classic
applications. This requires `ember-cli-rails-assets >= 0.8.0`.
* In development, the application is built synchronously on first request
instead of being rebuilt on file changes. Restart the Rails server to pick
up changes, or iterate with the Ember application's own development server
(`npm start`).
* In development, the application is served by Vite's development server —
the same one the application's `npm start` script runs. `ember-cli-rails`
starts it on the first request and shuts it down when Rails exits, and
rewrites the URLs in the `index.html` it serves so that the browser loads
the application's modules, and Vite's HMR client, from it directly. Changes
are hot-reloaded without restarting Rails.

Configure it, or opt out of it, with the `dev_server` option:

```rb
EmberCli.configure do |c|
# listen on a fixed port instead of an available one
c.app :frontend, dev_server: { port: 4200 }

# build once, synchronously, on the first request instead
c.app :admin, dev_server: false
end
```

* `include_ember_script_tags` reads the output of `ember build`, which the
development server does not produce. Render Vite-based applications with
`render_ember_app`, or disable the development server with
`dev_server: false` to keep using the asset helpers.

[Vite]: https://vitejs.dev

Expand Down
84 changes: 70 additions & 14 deletions lib/ember_cli/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
require "ember_cli/path_set"
require "ember_cli/shell"
require "ember_cli/build_monitor"
require "ember_cli/deploy/dev_server"
require "ember_cli/deploy/file"
require "ember_cli/dev_server"

module EmberCli
class App
Expand Down Expand Up @@ -49,20 +51,15 @@ def compile

def build
unless EmberCli.skip?
if development?
if paths.vite?
# The Vite-based blueprint (`ember-cli >= 6.8`) has no
# `ember-cli-rails-addon` to manage the build lock, so build
# synchronously instead of watching for changes.
compile
else
build_and_watch
end
elsif test?
compile
if dev_server?
# Vite's own development server rebuilds and hot-reloads the
# application, so there is nothing to build ahead of time.
dev_server.start
else
build_for_environment

@build.wait!
end

@build.wait!
end
end

Expand Down Expand Up @@ -106,6 +103,24 @@ def to_rack
deploy.to_rack
end

def dev_server
@dev_server ||= DevServer.new(
name: name,
paths: paths,
shell: @shell,
options: dev_server_options,
)
end

# Whether this application is served by Vite's development server rather
# than out of the directory `ember build` writes to.
def dev_server?
strategy = deploy_strategy

strategy.is_a?(Class) &&
strategy.ancestors.include?(EmberCli::Deploy::DevServer)
end

private

def development?
Expand All @@ -124,12 +139,38 @@ def deploy_strategy
strategy = options.fetch(:deploy, {})

if strategy.respond_to?(:fetch)
strategy.fetch(rails_env, EmberCli::Deploy::File)
strategy.fetch(rails_env) { default_deploy_strategy }
else
strategy
end
end

def default_deploy_strategy
if development? && paths.vite? && dev_server_enabled?
EmberCli::Deploy::DevServer
else
EmberCli::Deploy::File
end
end

def dev_server_option
options.fetch(:dev_server, true)
end

def dev_server_enabled?
dev_server_option != false
end

def dev_server_options
option = dev_server_option

if option.respond_to?(:fetch)
option
else
{}
end
end

def rails_env
Rails.env.to_s.to_sym
end
Expand All @@ -138,6 +179,21 @@ def env
EmberCli.env
end

def build_for_environment
if development?
if paths.vite?
# The Vite-based blueprint (`ember-cli >= 6.8`) has no
# `ember-cli-rails-addon` to manage the build lock, so build
# synchronously instead of watching for changes.
compile
else
build_and_watch
end
elsif test?
compile
end
end

def build_and_watch
prepare
@shell.build_and_watch
Expand Down
14 changes: 14 additions & 0 deletions lib/ember_cli/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ def build(watch: false)
ember_build(watch: watch)
end

# Boots Vite's development server for an application generated with the
# Vite-based blueprint (`ember-cli >= 6.8`). This is what the blueprint's
# own `npm start` script runs.
def dev_server(host:, port:)
line = Terrapin::CommandLine.new(paths.vite, [
"--host :host",
"--port :port",
"--strictPort",
"--clearScreen false",
].join(" "))

line.command(host: host.to_s, port: port.to_s)
end

private

attr_reader :options, :paths
Expand Down
47 changes: 47 additions & 0 deletions lib/ember_cli/deploy/dev_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "ember_cli/dev_server_proxy"
require "ember_cli/errors"

module EmberCli
module Deploy
# Serves an Ember application from its Vite development server instead of
# from the `dist` directory written by `ember build`.
#
# The `index.html` served by the development server refers to its assets
# (`/@vite/client` included) with root-relative URLs. Rails serves the
# document from its own origin, so those URLs are rewritten to absolute
# URLs pointing at the development server. Loading them from there means
# the Vite client opens its HMR WebSocket against the development server
# directly, without Rails proxying it.
class DevServer
ROOT_RELATIVE_URL = %r{(\s)(src|href)=(["'])/(?!/)}i

def initialize(app)
@app = app
end

def mountable?
true
end

def to_rack
DevServerProxy.new(app)
end

def index_html
rewrite_root_relative_urls(dev_server.index_html)
end

private

attr_reader :app

delegate :dev_server, to: :app

def rewrite_root_relative_urls(html)
html.gsub(ROOT_RELATIVE_URL) do
"#{$1}#{$2}=#{$3}#{dev_server.origin}/"
end
end
end
end
end
Loading