Skip to content

Commit f5d20fb

Browse files
committed
Implement development adapter for ViteJS as frontend bundler
This code allows to use ViteJS as an alternative to Esbuild for developing and bundling Inertia apps on Phoenix framework. The motivation for this change is to have a great HMR experince on both client and ssr while maintaing full compatibility with current approach using Esbuild as JS bundler.
1 parent 5116eec commit f5d20fb

File tree

18 files changed

+1378
-55
lines changed

18 files changed

+1378
-55
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# The directory Mix will write compiled artifacts to.
22
/_build/
33

4+
# nvim ELP plugin output
5+
.elixir_ls/
6+
47
# If you run "mix test --cover", coverage assets end up here.
58
/cover/
69

@@ -24,3 +27,8 @@ inertia-*.tar
2427

2528
# Temporary files, for example, from tests.
2629
/tmp/
30+
31+
# ViteJS plugin compiled assets
32+
/assets/node_modules/
33+
package.json
34+
/priv/vitejs

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ config :inertia,
5555
# conventional in JavaScript. Defaults to `false`.
5656
camelize_props: false,
5757

58-
# Instruct the client side whether to encrypt the page object in the window history
58+
# Instruct the client side whether to encrypt the page object in the window history
5959
# state. This can also be set/overridden on a per-request basis, using the `encrypt_history`
6060
# controller helper. Defaults to `false`.
6161
history: [encrypt: false],
@@ -64,6 +64,11 @@ config :inertia,
6464
# see instructions below). Defaults to `false`.
6565
ssr: false,
6666

67+
# By default the server side rendering is done by executing nodejs in an
68+
# Elixir process. If you want to use vitejs set the ssr_adapter
69+
# to "vitejs". This will use the dev vitejs server to do the SSR and HMR.
70+
ssr_adapter: if config_env() == :dev, do: "vitejs", else: nil,
71+
6772
# Whether to raise an exception when server-side rendering fails (only applies
6873
# when SSR is enabled). Defaults to `true`.
6974
#
@@ -313,7 +318,7 @@ conn
313318

314319
## Deferred props
315320

316-
**Requires Inertia v2.x on the client-side**.
321+
**Requires Inertia v2.x on the client-side**.
317322

318323
If you have expensive data that you'd like to automatically fetch (from the client-side via an async background request) after the page is initially rendered, you can mark the prop as deferred:
319324

@@ -361,7 +366,7 @@ defmodule MyApp.UserAuth do
361366

362367
def authenticate_user(conn, _opts) do
363368
user = get_user_from_session(conn)
364-
369+
365370
# Here we are storing the user in the conn assigns (so
366371
# we can use it for things like checking permissions later on),
367372
# AND we are assigning a serialized represention of the user
@@ -405,7 +410,7 @@ The `assign_errors` function will automatically convert the changeset errors int
405410
{
406411
"name" => "can't be blank",
407412

408-
// Nested errors keys are flattened with a dot separator (`.`)
413+
// Nested errors keys are flattened with a dot separator (`.`)
409414
"team.name" => "must be at least 3 characters long",
410415

411416
// Nested arrays are zero-based and indexed using bracket notation (`[0]`)
@@ -714,7 +719,7 @@ Then, update your config to enable SSR (if you'd like to enable it globally).
714719
# assets using the `static_paths` config). Defaults to "1".
715720
default_version: "1",
716721

717-
# Enable server-side rendering for page responses (requires some additional setup,
722+
# Enable server-side rendering for page responses (requires some additional setup,
718723
# see instructions below). Defaults to `false`.
719724
- ssr: false
720725
+ ssr: true

assets/.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
priv/
2+
deps/
3+
doc/

assets/.prettierrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"printWidth": 120,
3+
"semi": false,
4+
"singleQuote": true,
5+
"tabWidth": 2
6+
}

assets/js/vitePlugin.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { IncomingMessage, ServerResponse } from 'http'
2+
import { Connect, Plugin } from 'vite'
3+
4+
interface ExtendedRequest extends Connect.IncomingMessage {
5+
body?: Record<string, unknown>
6+
}
7+
8+
const jsonResponse = (res: ServerResponse<IncomingMessage>, statusCode: number, data: unknown) => {
9+
res.statusCode = statusCode
10+
res.setHeader('Content-Type', 'application/json')
11+
res.end(JSON.stringify(data))
12+
}
13+
14+
const jsonMiddleware = (req: ExtendedRequest, res: ServerResponse<IncomingMessage>, next: () => Promise<void>) => {
15+
let data = ''
16+
req.on('data', (chunk) => (data += chunk))
17+
req.on('end', () => {
18+
try {
19+
req.body = JSON.parse(data)
20+
next()
21+
} catch {
22+
jsonResponse(res, 400, { error: 'Invalid JSON' })
23+
}
24+
})
25+
req.on('error', () => {
26+
jsonResponse(res, 500, { error: 'Internal Server Error' })
27+
})
28+
}
29+
30+
export default function inertiaPhoenixPlugin({ entrypoint }: { entrypoint: string }): Plugin {
31+
return {
32+
name: 'inertia-phoenix',
33+
configureServer(server) {
34+
if (!entrypoint) {
35+
throw new Error(
36+
`[inertia-phoenix] Missing required \`entrypoint\` in plugin options.
37+
38+
Please pass the path to your SSR entry file.
39+
40+
Example:
41+
inertiaPhoenixPlugin({ entrypoint: './js/ssr.{jsx|tsx}' })`
42+
)
43+
}
44+
45+
// exit cleanly with Phoenix (dev only)
46+
process.stdin.on('close', () => process.exit(0))
47+
process.stdin.resume()
48+
49+
server.middlewares.use((req: ExtendedRequest, res, next) => {
50+
const path = req.url?.split('?', 1)[0]
51+
const isInertiaRequest = req.method === 'POST' && path === '/ssr_render'
52+
if (!isInertiaRequest) return next()
53+
54+
jsonMiddleware(req, res, async () => {
55+
try {
56+
const { render } = await server.ssrLoadModule(entrypoint)
57+
const html = await render(req.body)
58+
59+
jsonResponse(res, 200, {
60+
head: [],
61+
body: html,
62+
})
63+
} catch (e) {
64+
if (e instanceof Error) {
65+
server.ssrFixStacktrace(e)
66+
67+
jsonResponse(res, 500, {
68+
error: {
69+
message: e.message,
70+
stack: e.stack,
71+
},
72+
})
73+
} else {
74+
jsonResponse(res, 500, {
75+
error: {
76+
message: 'Unknown error',
77+
detail: e,
78+
},
79+
})
80+
}
81+
}
82+
})
83+
})
84+
},
85+
}
86+
}

0 commit comments

Comments
 (0)