yesod-vite (empty) → 0.1.0.0
raw patch · 6 files changed
+558/−0 lines, 6 filesdep +HUnitdep +aesondep +base
Dependencies added: HUnit, aeson, base, blaze-html, bytestring, containers, hspec, text, yesod-core, yesod-static, yesod-test, yesod-vite
Files
- CHANGELOG.md +5/−0
- LICENSE +25/−0
- src/Yesod/Vite.hs +257/−0
- test/Main.hs +128/−0
- test/TestApp.hs +13/−0
- yesod-vite.cabal +130/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for yesod-vite++## 0.1.0.0 -- 2026-03-30++* First version. Not ready enough to be a 1.0, but its full featured.
+ LICENSE view
@@ -0,0 +1,25 @@+Copyright (c) 2026, Ian Kollipara++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the+ distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ src/Yesod/Vite.hs view
@@ -0,0 +1,257 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Yesod.Vite+-- Description : Vite Integration for the Yesod Web Framework+-- Copyright : (c) Ian Kollipara, 2026+-- License : BSD-2-Clause+-- Maintainer : ian.kollipara@gmail.com+-- Stability : experimental+-- Portability : POSIX+--+-- This module provides an integration to @[vitejs](https://vite.dev)@, a modern frontend build tool,+-- to the "Yesod" web framework. This integration would allow the use of @vite@, setup in backend mode,+-- to work seamlessly with "Yesod".+--+-- = Quick Start+-- The assumption here is that your setup is similar to the default Yesod scaffold.+-- You will need to have installed both @yesod-static@ and @yesod-vite@ for this to work.+-- In addition, you will need the static subsite configured, as the route constructor is required.+-- - Run @npm init -y@ or the equivalent for your build tool.+--+-- - Run @npm i -D vite@ or the equivalent for your build tool.+--+-- - Create a @vite.config.js@ to include the following:+--+-- @+-- import { defineConfig } from "vite";+-- import * as path from "node:path";+--+-- export default defineConfig({+-- plugins: [],+-- base: "/static/",+-- build: {+-- outDir: path.resolve("./static"),+-- manifest: "manifest.json",+-- rollupOptions: {+-- input: path.resolve("./assets/src/app.js"),+-- },+-- },+-- });+-- @+--+-- - Install and configure "Yesod.Static" for your application.+--+-- - Setup 'YesodVite' as the following:+--+-- @+-- instance YesodVite App where+-- viteBuildDir :: app -> IO FilePath+-- vitebuildDir app = return $ appStaticDir app+--+-- viteInDev :: app -> IO Bool+-- viteinDev app = return . const True+--+-- viteRoute :: Route Static -> Route site+-- viteRoute = StaticR+-- @+--+-- - Add the @viteAsset \<asset-name\>@ to your default layout.+module Yesod.Vite+ ( YesodVite (..),+ decodeManifest,+ gatherAllCSS,+ gatherAllModules,+ )+where++import Data.Aeson+ ( FromJSON (parseJSON),+ eitherDecode,+ withObject,+ (.!=),+ (.:),+ (.:?),+ )+import qualified Data.ByteString.Lazy.Char8 as BS+import Data.Functor (($>))+import Data.List (nub)+import qualified Data.Map as M+import qualified Data.Text as T+import GHC.Generics (Generic)+import Yesod.Core+ ( MonadIO (liftIO),+ RenderRoute (Route),+ WidgetFor,+ Yesod,+ getYesod,+ whamlet,+ )+import Yesod.Static (Route (StaticRoute), Static)++-- * Types++-- | A manifest chunk as described by the vite configuration.+data ViteManifestChunk+ = ViteManifestChunk+ { src :: Maybe FilePath,+ file :: FilePath,+ css :: [FilePath],+ assets :: [FilePath],+ isEntry :: Bool,+ name :: Maybe T.Text,+ isDynamicEntry :: Bool,+ imports :: [T.Text],+ dynamicImports :: [T.Text]+ }+ deriving (Show, Generic)++instance FromJSON ViteManifestChunk where+ parseJSON = withObject "ViteManifestChunk" $ \o ->+ ViteManifestChunk+ <$> o .:? "src"+ <*> o .: "file"+ <*> o .:? "css" .!= mempty+ <*> o .:? "assets" .!= mempty+ <*> o .:? "isEntry" .!= False+ <*> o .:? "name"+ <*> o .:? "isDynamicEntry" .!= False+ <*> o .:? "imports" .!= mempty+ <*> o .:? "dynamicImports" .!= mempty++-- | A wrapper around a map to use for parsing the manifest file as described by vite.+type ViteManifest = M.Map T.Text ViteManifestChunk++-- | Attempt to decode a manifest+decodeManifest :: BS.ByteString -> Either String ViteManifest+decodeManifest = eitherDecode++-- | Recursively traverse the manifest, starting at the @assetName@ to find all unique css values.+gatherAllCSS ::+ -- | The entry in the manifest.json. This is specified by your @vite.config.js@ as your input+ T.Text ->+ -- | The actual manifest to traverse.+ ViteManifest ->+ -- | The paths to all the css.+ [FilePath]+gatherAllCSS+ entry+ man =+ let gatherAllCSS' :: T.Text -> [FilePath]+ gatherAllCSS' k' =+ case man M.!? k' of+ Nothing -> []+ Just e -> css e <> concat [gatherAllCSS' k | k <- imports e]+ in nub $ gatherAllCSS' entry++-- | Recursively traverse the manifest, starting at the @assetName@ to find all unique js modules.+gatherAllModules ::+ -- | The entry in the manifest.json. This is specified by your @vite.config.js@ as your input+ T.Text ->+ -- | The actual manifest to traverse.+ ViteManifest ->+ -- | The paths to all the js modules.+ [FilePath]+gatherAllModules entry man =+ let gatherAllModules' :: T.Text -> [FilePath]+ gatherAllModules' k' =+ case man M.!? k' of+ Nothing -> []+ Just e -> file e : concat [gatherAllModules' k | k <- imports e]+ in nub $ gatherAllModules' entry++-- | = YesodVite+-- This typeclass is used for configuring the uses of @vite@ in your "Yesod" application.+class (Yesod site) => YesodVite site where+ -- | The directory that vite builds to. This should match the output field in your @vite.config.js@.+ viteBuildDir :: site -> IO FilePath++ -- | Whether or not the site is in development. This is used to either serve production or development assets.+ viteInDev :: site -> IO Bool++ -- | How to create a static route. Typically you can set this to just `StaticR`.+ viteRoute :: Route Static -> Route site++ {-# MINIMAL viteBuildDir, viteInDev, viteRoute #-}++ -- | The url to serve development resources from.+ -- Defaults to @\http://127.0.0.1:5173\@+ viteDevUrl :: site -> IO String+ viteDevUrl = return . const "http://127.0.0.1:5173"++ -- | The path to the built manifest. Defaults to `buildDir` + @/.vite/manifest.json@+ viteManifest :: site -> IO FilePath+ viteManifest site_ = do+ basePath <- viteBuildDir site_+ return $ basePath <> "/.vite/manifest.json"++ -- | A helper used by @vite@ for @react@-based sites.+ -- This is entirely optional, unless you are using a @react@-based frontend site.+ -- This will be empty in production, but will enable hot reload in development.+ viteEnableReactRefresh :: WidgetFor site ()+ viteEnableReactRefresh = do+ site_ <- getYesod+ devUrl <- liftIO $ viteDevUrl site_+ inDev <- liftIO $ viteInDev site_+ if inDev+ then+ [whamlet|+ <script type="module">+ import RefreshRuntime from '#{devUrl}/@react-refresh';+ RefreshRuntime.injectIntoGlobalHook(window);+ window.$RefreshReg$ = () => {};+ window.$RefreshSig$ = () => (type) => type;+ window.__vite_plugin_react_preamble_installed__ = true;+ |]+ else+ return mempty++ -- | Insert a @vite@ asset into your site.+ -- In development this will point at the vite development server.+ -- In production, this will traverse the manifest and insert the correct tags+ -- to your static assets.+ viteAsset :: T.Text -> WidgetFor site ()+ viteAsset assetName = do+ site_ <- getYesod+ inDev <- liftIO $ viteInDev site_+ if inDev+ then viteDevAsset assetName site_+ else viteProductionAsset assetName site_++-- = Internal++-- | Handle the development asset+viteDevAsset :: (YesodVite site, Yesod site) => T.Text -> site -> WidgetFor site ()+viteDevAsset assetName s = do+ devUrl <- liftIO $ viteDevUrl s+ [whamlet|+ <script type="module" src="#{devUrl}/@vite/client">+ <script type="module" src="#{devUrl}/#{assetName}">+ |]++-- | Handle the production asset+viteProductionAsset :: (YesodVite site, Yesod site) => T.Text -> site -> WidgetFor site ()+viteProductionAsset assetName s = do+ manifestPath <- liftIO $ viteManifest s+ manifestContents <- liftIO $ BS.readFile manifestPath+ let manifestE = decodeManifest manifestContents+ case manifestE of+ Left err -> error err $> mempty+ Right manifest -> do+ let isJS = T.isSuffixOf ".js" assetName+ allCss = T.splitOn "/" . T.pack <$> gatherAllCSS assetName manifest+ allModules = T.splitOn "/" . T.pack <$> gatherAllModules assetName manifest+ assetUrl = T.splitOn "/" assetName+ [whamlet|+ $if isJS+ <script type="module" src="@{viteRoute $ StaticRoute assetUrl []}">+ $else+ <link ref="stylesheet" href="@{viteRoute $ StaticRoute assetUrl []}">+ $forall css <- allCss+ <link ref="stylesheet" href="@{viteRoute $ StaticRoute css []}">+ $forall module_ <- allModules+ <link ref="modulepreload" href="@{viteRoute $ StaticRoute module_ []}">+ |]
+ test/Main.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Main (main) where++import qualified Data.ByteString.Lazy as BS+import Data.Either (either, isRight)+import Debug.Trace+import Test.Hspec+import TestApp+import Yesod.Core+import Yesod.Static+import Yesod.Test+import Yesod.Vite++mkYesod+ "App"+ [parseRoutes|+ /static StaticR Static appStatic+ /react RrR GET+ / HomeR GET+|]++getRrR :: Handler Html+getRrR = defaultLayout viteEnableReactRefresh++getHomeR :: Handler Html+getHomeR = defaultLayout $ do+ [whamlet|+ ^{viteAsset "views/foo.js"}+ <main>Hello World+ |]++instance Yesod App++instance YesodVite App where+ viteBuildDir :: App -> IO FilePath+ viteBuildDir _ = return "static"++ viteRoute = StaticR++ viteInDev :: App -> IO Bool+ viteInDev = return . appInDev++ viteManifest :: App -> IO FilePath+ viteManifest s =+ if appInDev s+ then do+ basePath <- viteBuildDir s+ return $ basePath <> "/.vite/manifest.json"+ else+ return "test/exampleManifest.json"++manifestSpec :: Spec+manifestSpec = do+ describe "Manifest Parsing" $ do+ it "should parse the manifest" $ do+ manifest <- liftIO $ BS.readFile "test/exampleManifest.json"+ decodeManifest manifest `shouldSatisfy` isRight++ it "should extract all the CSS" $ do+ manifest <- liftIO $ BS.readFile "test/exampleManifest.json"+ let em = decodeManifest manifest+ either+ fail+ ( \m ->+ let css_ = gatherAllCSS "views/foo.js" m+ in css_ `shouldNotSatisfy` null+ )+ em++ it "should gather all the modules" $ do+ manifest <- liftIO $ BS.readFile "test/exampleManifest.json"+ let em = decodeManifest manifest+ either+ fail+ ( \m ->+ let mods_ = gatherAllModules "views/foo.js" m+ in mods_ `shouldNotSatisfy` null+ )+ em++yesodViteSpec :: Spec+yesodViteSpec = do+ describe "YesodVite" $ do+ it "should return the expected path" $ do+ app <- liftIO $ getApp True+ manifest <- liftIO $ viteManifest app+ manifest `shouldBe` ("static/.vite/manifest.json" :: FilePath)++yesodViteWithYesodSpecDev :: Spec+yesodViteWithYesodSpecDev =+ yesodSpecWithSiteGenerator (getApp True) $ do+ ydescribe "YesodVite (Widgets) [DEV]" $ do+ yit "should get a correct response for RrR" $ do+ get RrR+ statusIs 200+ bodyContains "@react-refresh"++ yit "should have development vite urls" $ do+ get HomeR+ statusIs 200+ bodyContains "@vite"++yesodViteWithYesodSpecProd :: Spec+yesodViteWithYesodSpecProd =+ yesodSpecWithSiteGenerator (getApp False) $ do+ ydescribe "YesodVite (Widgets) [PROD]" $ do+ yit "should contain an empty response" $ do+ get RrR+ statusIs 200+ bodyNotContains "@react-refresh"+ yit "should return the full vite assets" $ do+ get HomeR+ statusIs 200+ bodyNotContains "@vite"++main :: IO ()+main =+ hspec $+ manifestSpec+ *> yesodViteSpec+ *> yesodViteWithYesodSpecDev+ *> yesodViteWithYesodSpecProd
+ test/TestApp.hs view
@@ -0,0 +1,13 @@+module TestApp where+import Yesod.Static (Static, static)+++data App = App {+ appStatic :: Static,+ appInDev :: Bool+}++getApp :: Bool -> IO App+getApp indev = do+ s <- static "./"+ return $ App s indev
+ yesod-vite.cabal view
@@ -0,0 +1,130 @@+cabal-version: 3.0+-- The cabal-version field refers to the version of the .cabal specification,+-- and can be different from the cabal-install (the tool) version and the+-- Cabal (the library) version you are using. As such, the Cabal (the library)+-- version used must be equal or greater than the version stated in this field.+-- Starting from the specification version 2.2, the cabal-version field must be+-- the first thing in the cabal file.++-- Initial package description 'yesod-vite' generated by+-- 'cabal init'. For further documentation, see:+-- http://haskell.org/cabal/users-guide/+--+-- The name of the package.+name: yesod-vite++-- The package version.+-- See the Haskell package versioning policy (PVP) for standards+-- guiding when and how versions should be incremented.+-- https://pvp.haskell.org+-- PVP summary: +-+------- breaking API changes+-- | | +----- non-breaking API additions+-- | | | +--- code changes with no API change+version: 0.1.0.0++-- A short (one-line) description of the package.+synopsis: An integration of vitejs with Yesod++-- A longer description of the package.+description: An integration of vite and yesod. Provides a typeclass YesodVite which enables the viteAsset tag to function.++-- URL for the project homepage or repository.+homepage: https://github.com/ikollipara/yesod-vite++-- The license under which the package is released.+license: BSD-2-Clause++-- The file containing the license text.+license-file: LICENSE++-- The package author(s).+author: Ian Kollipara++-- An email address to which users can send suggestions, bug reports, and patches.+maintainer: ian.kollipara@gmail.com++-- A copyright notice.+copyright: Ian Kollipara, 2026+category: Web, Yesod+build-type: Simple++-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.+extra-doc-files: CHANGELOG.md++-- Extra source files to be distributed with the package, such as examples, or a tutorial module.+-- extra-source-files:++common warnings+ ghc-options: -Wall++source-repository head+ type: git+ location: https://github.com/ikollipara/yesod-vite+ ++library+ -- Import common warning flags.+ import: warnings++ -- Modules exported by the library.+ exposed-modules:+ Yesod.Vite++ -- Modules included in this library but not exported.+ -- other-modules:++ -- LANGUAGE extensions used by modules in this package.+ -- other-extensions:++ -- Other library packages from which modules are imported.+ build-depends:+ base >=4.18.3.0 && <4.19,+ yesod-core >=1.6.27.0 && <1.7,+ text >=2.0.2 && <2.1,+ yesod-static >=1.6.1.0 && <1.7,+ aeson >=2.1.2.1 && <2.2,+ containers >=0.6.7 && <0.7,+ bytestring >=0.11.5.4 && <0.12++ + -- Directories containing source files.+ hs-source-dirs: src++ -- Base language which the package is written in.+ default-language: Haskell2010++test-suite yesod-vite-test+ -- Import common warning flags.+ import: warnings++ -- Base language which the package is written in.+ default-language: Haskell2010++ -- Modules included in this executable, other than Main.+ other-modules:+ TestApp++ -- LANGUAGE extensions used by modules in this package.+ -- other-extensions:++ -- The interface type and version of the test suite.+ type: exitcode-stdio-1.0++ -- Directories containing source files.+ hs-source-dirs: test++ -- The entrypoint to the test suite.+ main-is: Main.hs++ -- Test dependencies.+ build-depends:+ base >=4.18.3.0 && <4.19,+ yesod-vite,+ hspec >=2.11.12 && <2.12,+ HUnit >=1.6.2.0 && <1.7,+ text >=2.0.2 && <2.1,+ yesod-test >=1.6.16 && <1.7,+ yesod-static >=1.6.1.0 && <1.7,+ yesod-core >=1.6.27.0 && <1.7,+ bytestring >=0.11.5.4 && <0.12,+ blaze-html >=0.9.2.0 && <0.10