├── .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 |

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 | [](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 |
--------------------------------------------------------------------------------