├── .git-blame-ignore-revs ├── example ├── public │ └── favicon.ico ├── package.json ├── src │ ├── assets │ │ └── main.css │ └── index.njk └── .eleventy.js ├── .prettierrc ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── package.json ├── .eleventy.js ├── README.md └── EleventyVite.js /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # chore: add prettier 2 | d76b34ffaf428752558b2d99f0e4a4163edda760 3 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11ty/eleventy-plugin-vite/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-plugin-vite-example", 3 | "private": true, 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /example/src/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": false, 4 | "semi": true, 5 | "endOfLine": "lf", 6 | "arrowParens": "always", 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | _site 3 | 4 | # IDEs 5 | /.idea/ 6 | /.vscode/ 7 | 8 | # Package managers 9 | node_modules 10 | .npm 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Artefacts 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /example/.eleventy.js: -------------------------------------------------------------------------------- 1 | import EleventyVitePlugin from "../.eleventy.js"; 2 | 3 | export default function (eleventyConfig) { 4 | eleventyConfig.addPassthroughCopy("src/assets"); 5 | 6 | eleventyConfig.addPlugin(EleventyVitePlugin); 7 | } 8 | 9 | export const config = { 10 | dir: { 11 | input: "src", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | assignees: [KiwiKilian] 8 | 9 | - package-ecosystem: github-actions 10 | directories: [/, ".github/workflows/**"] 11 | schedule: 12 | interval: weekly 13 | assignees: [KiwiKilian] 14 | -------------------------------------------------------------------------------- /example/src/index.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eleventy Plugin Vite 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Eleventy Plugin Vite

13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release to npm 2 | on: 3 | release: 4 | types: [published] 5 | permissions: read-all 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | environment: GitHub Publish 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 15 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # 6.0.0 16 | with: 17 | node-version: "22" 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install -g npm@latest 20 | - run: npm ci 21 | - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} 22 | run: npm publish --provenance --access=public --tag=${{ env.NPM_PUBLISH_TAG }} 23 | env: 24 | NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-alpha.') && 'alpha' || 'latest' }} 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@11ty/eleventy-plugin-vite", 3 | "version": "7.0.0", 4 | "description": "A plugin to use Vite as a development server and run Vite to postprocess your Eleventy build.", 5 | "license": "MIT", 6 | "engines": { 7 | "node": ">=18" 8 | }, 9 | "funding": { 10 | "type": "opencollective", 11 | "url": "https://opencollective.com/11ty" 12 | }, 13 | "keywords": [ 14 | "eleventy", 15 | "server" 16 | ], 17 | "11ty": { 18 | "compatibility": ">=3.0.0" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "contributors": [ 24 | { 25 | "name": "Zach Leatherman", 26 | "email": "zachleatherman@gmail.com", 27 | "url": "https://zachleat.com/" 28 | }, 29 | { 30 | "name": "Kilian Finger", 31 | "email": "npm@kilianfinger.com", 32 | "url": "https://www.kilianfinger.com/" 33 | } 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/11ty/eleventy-plugin-vite.git" 38 | }, 39 | "bugs": "https://github.com/11ty/eleventy-plugin-vite/issues", 40 | "homepage": "https://github.com/11ty/eleventy-plugin-vite/", 41 | "main": "./.eleventy.js", 42 | "type": "module", 43 | "exports": { 44 | ".": "./.eleventy.js", 45 | "./EleventyVite": "./EleventyVite.js", 46 | "./package.json": "./package.json" 47 | }, 48 | "files": [ 49 | ".eleventy.js", 50 | "EleventyVite.js" 51 | ], 52 | "scripts": { 53 | "format": "prettier . --write", 54 | "example": "cd example && eleventy", 55 | "example:start": "npm run example -- --serve", 56 | "example:build": "npm run example", 57 | "example:clean": "rimraf ./example/_site" 58 | }, 59 | "dependencies": { 60 | "@11ty/eleventy-utils": "^2.0.7", 61 | "vite": "^7.0.0" 62 | }, 63 | "devDependencies": { 64 | "@11ty/eleventy": "3.1.2", 65 | "prettier": "3.7.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | import EleventyVite from "./EleventyVite.js"; 2 | 3 | import path from "node:path"; 4 | import { createRequire } from "module"; 5 | const require = createRequire(import.meta.url); 6 | const pkg = require("./package.json"); 7 | 8 | /** 9 | * Options which can be passed to eleventy-plugin-vite 10 | * @typedef {Object} EleventyViteOptions 11 | * @property {string} tempFolderName 12 | * @property {import("vite").InlineConfig} [viteOptions] 13 | * @property {Object} [serverOptions] 14 | */ 15 | 16 | /** 17 | * @param {import('@11ty/eleventy/src/UserConfig').default} eleventyConfig 18 | * @param {EleventyViteOptions} options 19 | */ 20 | export default function (eleventyConfig, options = {}) { 21 | try { 22 | eleventyConfig.versionCheck(pkg["11ty"].compatibility); 23 | } catch (error) { 24 | eleventyConfig.logger.warn( 25 | `Warning: Eleventy Plugin (${pkg.name}) Compatibility: ${error.message}`, 26 | ); 27 | } 28 | 29 | const eleventyVite = new EleventyVite(eleventyConfig, options); 30 | 31 | const publicDir = eleventyVite.options.viteOptions?.publicDir || "public"; 32 | 33 | if (!path.relative(eleventyConfig.directories.output, publicDir)) { 34 | throw new Error( 35 | `${EleventyVite.LOGGER_PREFIX} Misconfiguration: Can't use the same directory for 11ty output and vite public directory`, 36 | ); 37 | } 38 | 39 | // Add publicDir to passthrough copy 40 | eleventyConfig.addPassthroughCopy(publicDir); 41 | 42 | // Add tempFolder to ignores 43 | eleventyConfig.ignores.add(eleventyVite.getIgnoreDirectory()); 44 | 45 | const serverOptions = Object.assign( 46 | { 47 | module: "@11ty/eleventy-dev-server", 48 | domDiff: false, 49 | }, 50 | options.serverOptions, 51 | ); 52 | 53 | serverOptions.setup = async () => { 54 | // Use Vite as Middleware 55 | const viteDevServer = await eleventyVite.getServer(); 56 | 57 | process.on("SIGINT", async () => { 58 | await viteDevServer.close(); 59 | }); 60 | 61 | return { 62 | middleware: [viteDevServer.middlewares], 63 | }; 64 | }; 65 | 66 | eleventyConfig.setServerOptions(serverOptions); 67 | 68 | // Run Vite build 69 | // TODO use `build.write` option to work with json or ndjson outputs 70 | eleventyConfig.on("eleventy.after", async ({ dir, runMode, outputMode, results }) => { 71 | // Skips the Vite build if: 72 | // --serve 73 | // --to=json 74 | // --to=ndjson 75 | // or 0 output files from Eleventy build 76 | // Notably, this *does* run Vite build in --watch mode 77 | if ( 78 | runMode === "serve" || 79 | outputMode === "json" || 80 | outputMode === "ndjson" || 81 | results.length === 0 82 | ) { 83 | return; 84 | } 85 | 86 | await eleventyVite.runBuild(results); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

11ty Logo  Vite logo

2 | 3 | # eleventy-plugin-vite 🕚⚡️🎈🐀 4 | 5 | A plugin to use [Vite](https://vitejs.dev/) with Eleventy. 6 | 7 | This plugin: 8 | 9 | - Runs Vite as Middleware in Eleventy Dev Server (try with Eleventy’s `--incremental`) 10 | - Runs Vite build to postprocess your Eleventy build output 11 | 12 | ## Eleventy Housekeeping 13 | 14 | - Please star [Eleventy on GitHub](https://github.com/11ty/eleventy/)! 15 | - Follow us on Mastodon [@eleventy@fosstodon.org](https://fosstodon.org/@eleventy) or Twitter [@eleven_ty](https://twitter.com/eleven_ty) 16 | - Join us on [Discord](https://www.11ty.dev/blog/discord/) 17 | - Support [11ty on Open Collective](https://opencollective.com/11ty) 18 | - [11ty on npm](https://www.npmjs.com/org/11ty) 19 | - [11ty on GitHub](https://github.com/11ty) 20 | 21 | [![npm Version](https://img.shields.io/npm/v/@11ty/eleventy-plugin-vite.svg?style=for-the-badge)](https://www.npmjs.com/package/@11ty/eleventy-plugin-vite) 22 | 23 | ## Installation 24 | 25 | ``` 26 | npm install @11ty/eleventy-plugin-vite@alpha --save-dev 27 | ``` 28 | 29 | ### ESM `.eleventy.js` Config 30 | 31 | ```js 32 | import EleventyVitePlugin from "@11ty/eleventy-plugin-vite"; 33 | 34 | export default function (eleventyConfig) { 35 | eleventyConfig.addPlugin(EleventyVitePlugin); 36 | } 37 | ``` 38 | 39 | ### CommonJS `.eleventy.js` Config 40 | 41 | > [!NOTE] 42 | > This plugin is written in ESM, therefore `require` is not possible. If your .eleventy.js config uses CommonJS, make it async and create a dynamic import as shown below. 43 | 44 | ```js 45 | module.exports = async function (eleventyConfig) { 46 | const EleventyPluginVite = (await import("@11ty/eleventy-plugin-vite")).default; 47 | 48 | eleventyConfig.addPlugin(EleventyPluginVite); 49 | }; 50 | ``` 51 | 52 | Read more about ESM vs CommonJS on the [Eleventy documentation](https://www.11ty.dev/docs/cjs-esm/). 53 | 54 | ### Options 55 | 56 | View the [full list of Vite Configuration options](https://vitejs.dev/config/). 57 | 58 | ```js 59 | import EleventyVitePlugin from "@11ty/eleventy-plugin-vite"; 60 | 61 | export default function (eleventyConfig) { 62 | eleventyConfig.addPlugin(EleventyVitePlugin, { 63 | tempFolderName: ".11ty-vite", // Default name of the temp folder 64 | 65 | // Options passed to the Eleventy Dev Server 66 | // Defaults 67 | serverOptions: { 68 | module: "@11ty/eleventy-dev-server", 69 | domDiff: false, 70 | }, 71 | 72 | // Defaults 73 | viteOptions: { 74 | clearScreen: false, 75 | appType: "mpa", 76 | 77 | server: { 78 | middlewareMode: true, 79 | }, 80 | 81 | build: { 82 | emptyOutDir: true, 83 | }, 84 | 85 | resolve: { 86 | alias: { 87 | // Allow references to `node_modules` folder directly 88 | "/node_modules": path.resolve(".", "node_modules"), 89 | }, 90 | }, 91 | }, 92 | }); 93 | } 94 | ``` 95 | 96 | ## Related Projects 97 | 98 | - [`eleventy-plus-vite`](https://github.com/matthiasott/eleventy-plus-vite) by @matthiasott: A starter template using this plugin 99 | - Currently unmaintained: 100 | - [`slinkity`](https://slinkity.dev/) by @Holben888: A much deeper and more comprehensive integration with Vite! Offers partial hydration and can use shortcodes to render framework components in Eleventy! 101 | - [`vite-plugin-eleventy`](https://www.npmjs.com/package/vite-plugin-eleventy) by @Snugug: Uses Eleventy as Middleware in Vite (instead of the post-processing approach used here) 102 | -------------------------------------------------------------------------------- /EleventyVite.js: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from "node:fs"; 2 | import path from "node:path"; 3 | import { DeepCopy, Merge } from "@11ty/eleventy-utils"; 4 | import { build, createServer } from "vite"; 5 | 6 | /** @type {Required} */ 7 | const DEFAULT_OPTIONS = { 8 | tempFolderName: ".11ty-vite", 9 | viteOptions: { 10 | clearScreen: false, 11 | appType: "mpa", 12 | server: { 13 | middlewareMode: true, 14 | }, 15 | build: { 16 | emptyOutDir: true, 17 | rollupOptions: {}, // we use this to inject input for MPA build below 18 | }, 19 | resolve: { 20 | alias: { 21 | // Allow references to `node_modules` directly for bundling. 22 | "/node_modules": path.resolve(".", "node_modules"), 23 | // Note that bare module specifiers are also supported 24 | }, 25 | }, 26 | }, 27 | }; 28 | 29 | export default class EleventyVite { 30 | static LOGGER_PREFIX = "[11ty/eleventy-plugin-vite]"; 31 | 32 | /** @type {import("@11ty/eleventy/src/Util/ProjectDirectories.js").default} */ 33 | directories; 34 | 35 | /** @type {import("@11ty/eleventy/src/Util/ConsoleLogger.js").default} */ 36 | logger; 37 | 38 | /** @type {Required} */ 39 | options; 40 | 41 | constructor(eleventyConfig, pluginOptions = {}) { 42 | this.directories = eleventyConfig.directories; 43 | this.logger = eleventyConfig.logger; 44 | this.options = Merge({}, DEFAULT_OPTIONS, pluginOptions); 45 | } 46 | 47 | getServer() { 48 | /** @type {import("vite").InlineConfig} */ 49 | const viteOptions = DeepCopy({}, this.options.viteOptions); 50 | viteOptions.root = this.directories.output; 51 | 52 | return createServer(viteOptions); 53 | } 54 | 55 | getIgnoreDirectory() { 56 | return path.join(this.options.tempFolderName, "**"); 57 | } 58 | 59 | async runBuild(input) { 60 | const tempFolderPath = path.resolve(this.options.tempFolderName); 61 | await fsp.rename(this.directories.output, tempFolderPath); 62 | 63 | try { 64 | /** @type {import("vite").InlineConfig} */ 65 | const viteOptions = DeepCopy({}, this.options.viteOptions); 66 | viteOptions.root = tempFolderPath; 67 | 68 | viteOptions.build.rollupOptions.input = input 69 | .filter((entry) => !!entry.outputPath) // filter out `false` serverless routes 70 | .filter((entry) => (entry.outputPath || "").endsWith(".html")) // only html output 71 | .map((entry) => { 72 | if (!entry.outputPath.startsWith(this.directories.output)) { 73 | throw new Error( 74 | `Unexpected output path (was not in output directory ${this.directories.output}): ${entry.outputPath}`, 75 | ); 76 | } 77 | 78 | return path.resolve( 79 | tempFolderPath, 80 | entry.outputPath.substring(this.directories.output.length), 81 | ); 82 | }); 83 | 84 | viteOptions.build.outDir = path.resolve(".", this.directories.output); 85 | 86 | this.logger.logWithOptions({ 87 | prefix: EleventyVite.LOGGER_PREFIX, 88 | message: "Starting Vite build", 89 | type: "info", 90 | }); 91 | await build(viteOptions); 92 | this.logger.logWithOptions({ 93 | prefix: EleventyVite.LOGGER_PREFIX, 94 | message: "Finished Vite build", 95 | type: "info", 96 | }); 97 | } catch (error) { 98 | this.logger.logWithOptions({ 99 | prefix: EleventyVite.LOGGER_PREFIX, 100 | message: `Encountered a Vite build error, restoring original Eleventy output to ${this.directories.output}`, 101 | type: "error", 102 | color: "red", 103 | }); 104 | this.logger.logWithOptions({ 105 | prefix: EleventyVite.LOGGER_PREFIX, 106 | message: "Vite error:", 107 | type: "error", 108 | }); 109 | this.logger.logWithOptions({ 110 | prefix: EleventyVite.LOGGER_PREFIX, 111 | message: JSON.stringify(error, null, 2), 112 | type: "error", 113 | color: "cyan", 114 | }); 115 | 116 | await fsp.rename(tempFolderPath, this.directories.output); 117 | 118 | throw error; 119 | } finally { 120 | await fsp.rm(tempFolderPath, { force: true, recursive: true }); 121 | } 122 | } 123 | } 124 | --------------------------------------------------------------------------------