vite
Version, currently 0.1.01 version
- 0.1.0latestAug 17, 2026
github.com/codedByShoe/crystal-vite
Vite integration for Crystal. Resolves a Vite 5+ build manifest and emits the script, stylesheet, and modulepreload tags for your entrypoints, with a dev mode that targets the Vite dev server.
Installation
# Add this to your shard.yml
dependencies:
vite:
github: codedByShoe/crystal-vite
version: ~> 0.1.0Then run:
shards installshard.yml
- Crystal
>= 1.21.0- License
- MIT
- Author
- Andrew Shoemaker
Dependencies
This version declares no dependencies.
README
vite
Vite integration for Crystal. Reads a manifest.json produced by vite build
and emits the <script>, <link rel="stylesheet">, and
<link rel="modulepreload"> tags your entrypoints need — with a dev mode that
points at the Vite dev server instead, including React Fast Refresh.
Requirements
- Crystal
>= 1.21.0 - Vite 5 or newer
Installation
-
Add the dependency to your
shard.yml:dependencies: vite: github: codedByShoe/crystal-vite version: ~> 0.1.0 -
Run
shards install
Usage
require "vite"
config = Vite::Config.new
config.dev = ENV["APP_ENV"]? != "production"
client = Vite::Client.new(config)
client.tags("src/main.ts")
In production that resolves the manifest and emits stylesheets, then modulepreloads, then the entry script:
<link rel="stylesheet" href="/build/assets/shared-aKvd9S2.css" />
<link rel="stylesheet" href="/build/assets/main-r2MXTfSO.css" />
<link rel="modulepreload" href="/build/assets/shared-DkKv66kD.js" />
<script type="module" src="/build/assets/main-B86osw1_.js"></script>
In dev mode the manifest is ignored entirely — nothing needs to be built — and the tags point at the dev server:
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/src/main.ts"></script>
tags accepts multiple entries, and has an IO overload that avoids building an
intermediate string:
client.tags("src/main.ts", "src/admin.ts")
client.tags(io, "src/main.ts")
React
Setting react prepends the React Refresh preamble, which must run before the
Vite client loads:
config.dev = true
config.react = true
client.tags("src/main.tsx")
Individual assets
client.asset("src/main.ts") # => "/build/assets/main-DOZrsiw7.js"
client.css("src/main.ts") # => ["/build/assets/shared-aKvd9S2.css", ...]
css resolves stylesheets transitively through the import graph, so CSS carried
by a shared chunk is included. Stylesheets behind a dynamic import are
excluded, since Vite loads those at runtime with the chunk. Circular import
graphs are handled — Vite output does produce them.
Vite gives every emitted asset its own manifest entry, so asset works for
images and fonts too, keyed by source path:
client.asset("src/logo.svg") # => "/build/assets/logo-BWN8Kq6B.svg"
assets lists everything an entry pulls in, resolved transitively the same way
css is:
client.assets("src/main.ts")
# => ["/build/assets/logo-BWN8Kq6B.svg", "/build/assets/texture-BvTte26A.svg"]
Configuration
| Property | Default | Purpose |
|---|---|---|
dev | false | Emit dev-server tags and skip the manifest entirely |
react | false | Prepend the React Refresh preamble in dev |
build_dir | "build" | Public-facing URL prefix for built assets |
public_dir | "public" | Document root, used to locate the manifest on disk |
dev_server_url | "http://localhost:5173" | Vite dev server origin |
asset_host | nil | CDN origin; when set, replaces /<build_dir> in asset URLs |
manifest_path | nil | Explicit manifest path, overriding the default location |
The manifest defaults to public/build/.vite/manifest.json — i.e.
File.join(public_dir, build_dir, ".vite", "manifest.json"). Point your Vite
config's build.outDir at public/build to match, or set manifest_path
directly. See Kemal example below
With asset_host set, URLs are rewritten to that origin:
config.asset_host = "https://cdn.example.com"
client.asset("src/main.ts") # => "https://cdn.example.com/assets/main-B86osw1_.js"
With Kemal
Kemal serves static files from ./public by default, so there is nothing else to configure
for use of this package with Kemal. Just make sure to point Vite's build.outDir at public/build.
# src/app.cr
require "kemal"
require "vite"
VITE = Vite::Client.new(Vite::Config.new.tap { |c|
c.dev = Kemal.config.env != "production"
c.react = true
})
get "/" do |env|
render "views/index.ecr", "views/layout.ecr"
end
Kemal.run
<%# views/layout.ecr %>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<%= VITE.tags("src/main.tsx") %>
</head>
<body><%= content %></body>
</html>
Example Vite config:
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
// Make sure to set publicDir to false. Let Kemal control public/ instead of Vite
publicDir: false,
build: {
manifest: true,
outDir: "public/build",
rollupOptions: { input: "src/main.tsx" },
},
});
Run vite dev alongside Kemal in development, and vite build before deploying.
Errors
Vite::ManifestError— manifest missing, unparseable, or referencing a chunk that isn't in the manifest.Vite::EntryNotFoundError— the requested entry isn't in the manifest. The message lists the entries that are, which is usually enough to spot the typo.
Both inherit from Vite::Error.
Development
Run the specs:
crystal spec
Specs run against spec/fixtures/vite7-react.json, which is recorded from
vite build. Regenerate it from examples/react when bumping Vite support:
cd examples/react
npm install
npm run record
Commit the resulting diff — a change in that file, or in the VITE_VERSION
beside it, is the signal that Vite's manifest format moved.
The only manifests built inline in specs are malformed ones — dangling imports, bad JSON — which a build cannot produce.
The example app is never built by crystal spec and Node is not part of CI.
Contributing
- Fork it (https://github.com/codedByShoe/crystal-vite/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Andrew Shoemaker - creator and maintainer
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.1.0- Tagged
- Aug 17, 2026
- Commit
1093aa4912bd- Crystal
>= 1.21.0- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/codedByShoe/crystal-vite
Metadata
- Created
- Aug 19, 2026
- Updated
- Sep 17, 2026
- Synced
- Sep 17, 2026
- Versions
- 1