packages feed

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 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