Repository: 11ty/eleventy-plugin-vite Branch: main Commit: 411034f94c71 Files: 16 Total size: 21.4 KB Directory structure: gitextract_g145p6_b/ ├── .eleventy.js ├── .git-blame-ignore-revs ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── EleventyVite.js ├── README.md ├── example/ │ ├── .eleventy.js │ ├── package.json │ └── src/ │ ├── assets/ │ │ └── main.css │ └── index.njk ├── package.json └── test/ ├── .eleventy.test.js └── EleventyVite.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eleventy.js ================================================ import EleventyVite from "./EleventyVite.js"; import path from "node:path"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const pkg = require("./package.json"); /** * Options which can be passed to eleventy-plugin-vite * @typedef {Object} EleventyViteOptions * @property {string} tempFolderName * @property {import("vite").InlineConfig} [viteOptions] * @property {Object} [serverOptions] */ /** * @param {import('@11ty/eleventy/src/UserConfig').default} eleventyConfig * @param {EleventyViteOptions} options */ export default function (eleventyConfig, options = {}) { try { eleventyConfig.versionCheck(pkg["11ty"].compatibility); } catch (error) { eleventyConfig.logger.warn( `Warning: Eleventy Plugin (${pkg.name}) Compatibility: ${error.message}`, ); } const eleventyVite = new EleventyVite(eleventyConfig, options); const publicDir = eleventyVite.options.viteOptions?.publicDir || "public"; if (!path.relative(eleventyConfig.directories.output, publicDir)) { throw new Error( `${EleventyVite.LOGGER_PREFIX} Misconfiguration: Can't use the same directory for 11ty output and vite public directory`, ); } // Add publicDir to passthrough copy eleventyConfig.addPassthroughCopy(publicDir); // Add tempFolder to ignores eleventyConfig.ignores.add(eleventyVite.getIgnoreDirectory()); const serverOptions = Object.assign( { module: "@11ty/eleventy-dev-server", domDiff: false, }, options.serverOptions, ); serverOptions.setup = async () => { // Use Vite as Middleware const viteDevServer = await eleventyVite.getServer(); process.on("SIGINT", async () => { await viteDevServer.close(); }); return { middleware: [viteDevServer.middlewares], }; }; eleventyConfig.setServerOptions(serverOptions); // Run Vite build // TODO use `build.write` option to work with json or ndjson outputs eleventyConfig.on("eleventy.after", async ({ dir, runMode, outputMode, results }) => { // Skips the Vite build if: // --serve // --to=json // --to=ndjson // or 0 output files from Eleventy build // Notably, this *does* run Vite build in --watch mode if ( runMode === "serve" || outputMode === "json" || outputMode === "ndjson" || results.length === 0 ) { return; } await eleventyVite.runBuild(results); }); } ================================================ FILE: .git-blame-ignore-revs ================================================ # chore: add prettier d76b34ffaf428752558b2d99f0e4a4163edda760 ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: weekly assignees: [KiwiKilian] - package-ecosystem: github-actions directories: [/, ".github/workflows/**"] schedule: interval: weekly assignees: [KiwiKilian] ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish Release to npm on: release: types: [published] permissions: read-all jobs: build: runs-on: ubuntu-latest environment: GitHub Publish permissions: contents: read id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" - run: npm install -g npm@latest - run: npm ci - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} run: npm publish --provenance --access=public --tag=${{ env.NPM_PUBLISH_TAG }} env: NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-alpha.') && 'alpha' || 'latest' }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Run Tests on: push: branches: [main, alpha] pull_request: branches: [main, alpha] merge_group: branches: [main, alpha] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 4.4.0 with: node-version: "24" - run: npm ci - run: npm test ================================================ FILE: .gitignore ================================================ # Build results _site # IDEs /.idea/ /.vscode/ # Package managers node_modules .npm # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Artefacts .DS_Store ================================================ FILE: .prettierrc ================================================ { "useTabs": true, "singleQuote": false, "semi": true, "endOfLine": "lf", "arrowParens": "always", "printWidth": 100 } ================================================ FILE: EleventyVite.js ================================================ import { promises as fsp } from "node:fs"; import path from "node:path"; import { DeepCopy, Merge } from "@11ty/eleventy-utils"; import { build, createServer } from "vite"; /** @type {Required} */ const DEFAULT_OPTIONS = { tempFolderName: ".11ty-vite", viteOptions: { clearScreen: false, appType: "mpa", server: { middlewareMode: true, }, build: { emptyOutDir: true, rolldownOptions: { // HTML files will be injected and merged into `input` for MPA }, }, resolve: { alias: { // Allow references to `node_modules` directly for bundling. "/node_modules": path.resolve(".", "node_modules"), // Note that bare module specifiers are also supported }, }, }, }; export default class EleventyVite { static LOGGER_PREFIX = "[11ty/eleventy-plugin-vite]"; /** @type {import("@11ty/eleventy/src/Util/ProjectDirectories.js").default} */ directories; /** @type {import("@11ty/eleventy/src/Util/ConsoleLogger.js").default} */ logger; /** @type {Required} */ options; constructor(eleventyConfig, pluginOptions = {}) { this.directories = eleventyConfig.directories; this.logger = eleventyConfig.logger; // Relevant until `rollupOptions` are removed pluginOptions = DeepCopy({}, pluginOptions); const userBuildOptions = pluginOptions.viteOptions?.build; if (userBuildOptions?.rollupOptions && !userBuildOptions?.rolldownOptions) { this.logger.logWithOptions({ prefix: EleventyVite.LOGGER_PREFIX, message: "Usage of `rollupOptions` is deprecated, use `rolldownOptions` instead", type: "info", }); userBuildOptions.rolldownOptions = userBuildOptions.rollupOptions; delete userBuildOptions.rollupOptions; } this.options = Merge({}, DEFAULT_OPTIONS, pluginOptions); } getServer() { /** @type {import("vite").InlineConfig} */ const viteOptions = DeepCopy({}, this.options.viteOptions); viteOptions.root = this.directories.output; return createServer(viteOptions); } getIgnoreDirectory() { return path.join(this.options.tempFolderName, "**"); } get tempFolderPath() { return path.resolve(this.options.tempFolderName); } static getEntryPointName(filePath) { return path .join(path.dirname(filePath), path.basename(filePath, path.extname(filePath))) .replace(/^\/+/, ""); } getEleventyRolldownOptionsInput(input) { return ( input // Filter out `false` serverless routes .filter((entry) => !!entry.outputPath) // Only HTML output .filter((entry) => (entry.outputPath ?? "").endsWith(".html")) .reduce((result, entry) => { if (!entry.outputPath.startsWith(this.directories.output)) { throw new Error( `Unexpected output path (was not in output directory ${this.directories.output}): ${entry.outputPath}`, ); } const filePath = entry.outputPath.substring(this.directories.output.length); result[EleventyVite.getEntryPointName(filePath)] = path.resolve( this.tempFolderPath, filePath, ); return result; }, {}) ); } getUserRolldownOptionsInput(input) { let userInput = {}; if (input) { if (Array.isArray(input)) { input.forEach((file) => { userInput[EleventyVite.getEntryPointName(file)] = file; }); } else if (typeof input === "object") { userInput = input; } else if (typeof input === "string") { userInput[EleventyVite.getEntryPointName(input)] = input; } } return userInput; } async runBuild(input) { await fsp.rename(this.directories.output, this.tempFolderPath); try { /** @type {import("vite").InlineConfig} */ const viteOptions = DeepCopy({}, this.options.viteOptions); viteOptions.root = this.tempFolderPath; viteOptions.build.outDir = path.resolve(".", this.directories.output); viteOptions.build.rolldownOptions.input = { ...this.getEleventyRolldownOptionsInput(input), ...this.getUserRolldownOptionsInput(viteOptions.build.rolldownOptions.input), }; this.logger.logWithOptions({ prefix: EleventyVite.LOGGER_PREFIX, message: "Starting Vite build", type: "info", }); await build(viteOptions); this.logger.logWithOptions({ prefix: EleventyVite.LOGGER_PREFIX, message: "Finished Vite build", type: "info", }); } catch (error) { this.logger.logWithOptions({ prefix: EleventyVite.LOGGER_PREFIX, message: `Encountered a Vite build error, restoring original Eleventy output to ${this.directories.output}`, type: "error", color: "red", }); this.logger.logWithOptions({ prefix: EleventyVite.LOGGER_PREFIX, message: "Vite error:", type: "error", }); this.logger.logWithOptions({ prefix: EleventyVite.LOGGER_PREFIX, message: JSON.stringify(error, null, 2), type: "error", color: "cyan", }); await fsp.rename(this.tempFolderPath, this.directories.output); throw error; } finally { await fsp.rm(this.tempFolderPath, { force: true, recursive: true }); } } } ================================================ FILE: README.md ================================================

11ty Logo  Vite logo

# eleventy-plugin-vite 🕚⚡️🎈🐀 A plugin to use [Vite](https://vitejs.dev/) with Eleventy. This plugin: - Runs Vite as Middleware in Eleventy Dev Server (try with Eleventy’s `--incremental`) - Runs Vite build to postprocess your Eleventy build output ## Eleventy Housekeeping - Please star [Eleventy on GitHub](https://github.com/11ty/eleventy/)! - Follow us on Mastodon [@eleventy@fosstodon.org](https://fosstodon.org/@eleventy) or Twitter [@eleven_ty](https://twitter.com/eleven_ty) - Join us on [Discord](https://www.11ty.dev/blog/discord/) - Support [11ty on Open Collective](https://opencollective.com/11ty) - [11ty on npm](https://www.npmjs.com/org/11ty) - [11ty on GitHub](https://github.com/11ty) [![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) ## Installation ``` npm install @11ty/eleventy-plugin-vite@alpha --save-dev ``` ### ESM `.eleventy.js` Config ```js import EleventyVitePlugin from "@11ty/eleventy-plugin-vite"; export default function (eleventyConfig) { eleventyConfig.addPlugin(EleventyVitePlugin); } ``` ### CommonJS `.eleventy.js` Config > [!NOTE] > 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. ```js module.exports = async function (eleventyConfig) { const EleventyPluginVite = (await import("@11ty/eleventy-plugin-vite")).default; eleventyConfig.addPlugin(EleventyPluginVite); }; ``` Read more about ESM vs CommonJS on the [Eleventy documentation](https://www.11ty.dev/docs/cjs-esm/). ### Options These are the default options of the plugin. There's no need to specify them unless you want to change them. ```js import EleventyVitePlugin from "@11ty/eleventy-plugin-vite"; export default function (eleventyConfig) { eleventyConfig.addPlugin(EleventyVitePlugin, { // Default name of the temp folder tempFolderName: ".11ty-vite", // Eleventy Dev Server Options serverOptions: { module: "@11ty/eleventy-dev-server", domDiff: false, }, // Vite Config viteOptions: { clearScreen: false, appType: "mpa", server: { middlewareMode: true, }, build: { emptyOutDir: true, rolldownOptions: { input: { // HTML entry points will be injected automatically // Custom input will be merged }, }, }, resolve: { alias: { // Allow references to `node_modules` directly for bundling. "/node_modules": path.resolve(".", "node_modules"), // Note that bare module specifiers are also supported }, }, }, }); } ``` View the [full list of Vite configuration options](https://vitejs.dev/config/). Custom viteOptions will be deeply merged with the defaults. ## Related Projects - [`eleventy-plus-vite`](https://github.com/matthiasott/eleventy-plus-vite) by @matthiasott: A starter template using this plugin - Currently unmaintained: - [`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! - [`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) ================================================ FILE: example/.eleventy.js ================================================ import EleventyVitePlugin from "../.eleventy.js"; export default function (eleventyConfig) { eleventyConfig.addPassthroughCopy("src/assets"); eleventyConfig.addPlugin(EleventyVitePlugin); } export const config = { dir: { input: "src", }, }; ================================================ FILE: example/package.json ================================================ { "name": "eleventy-plugin-vite-example", "private": true, "type": "module" } ================================================ FILE: example/src/assets/main.css ================================================ body { background-color: black; color: white; font-family: sans-serif; } ================================================ FILE: example/src/index.njk ================================================ Eleventy Plugin Vite

Eleventy Plugin Vite

================================================ FILE: package.json ================================================ { "name": "@11ty/eleventy-plugin-vite", "version": "8.0.0", "description": "A plugin to use Vite as a development server and run Vite to postprocess your Eleventy build.", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/11ty" }, "keywords": [ "eleventy", "server" ], "11ty": { "compatibility": ">=3.0.0" }, "publishConfig": { "access": "public" }, "contributors": [ { "name": "Zach Leatherman", "email": "zachleatherman@gmail.com", "url": "https://zachleat.com/" }, { "name": "Kilian Finger", "email": "npm@kilianfinger.com", "url": "https://www.kilianfinger.com/" } ], "repository": { "type": "git", "url": "git://github.com/11ty/eleventy-plugin-vite.git" }, "bugs": "https://github.com/11ty/eleventy-plugin-vite/issues", "homepage": "https://github.com/11ty/eleventy-plugin-vite/", "main": "./.eleventy.js", "type": "module", "exports": { ".": "./.eleventy.js", "./EleventyVite": "./EleventyVite.js", "./package.json": "./package.json" }, "files": [ ".eleventy.js", "EleventyVite.js" ], "scripts": { "test": "vitest", "format": "prettier . --write", "example": "cd example && eleventy", "example:start": "npm run example -- --serve", "example:build": "npm run example", "example:clean": "rimraf ./example/_site" }, "dependencies": { "@11ty/eleventy-utils": "^2.0.7", "vite": "^8.0.9" }, "devDependencies": { "@11ty/eleventy": "3.1.5", "prettier": "3.8.3", "vitest": "3.2.4" } } ================================================ FILE: test/.eleventy.test.js ================================================ import { describe, it, expect, vi } from "vitest"; import eleventyPlugin from "../.eleventy.js"; describe(".eleventy.js", () => { it("sets up passthrough, ignores, and server options", () => { const ignores = new Set(); const eleventyConfig = { directories: { output: "dist" }, addPassthroughCopy: vi.fn(), ignores, setServerOptions: vi.fn(), on: vi.fn(), logger: { warn: vi.fn() }, versionCheck: vi.fn(), }; eleventyPlugin(eleventyConfig, { tempFolderName: ".test-11ty-vite" }); expect(eleventyConfig.addPassthroughCopy).toHaveBeenCalled(); expect([...ignores].some((i) => i.includes(".test-11ty-vite"))).toBe(true); expect(eleventyConfig.setServerOptions).toHaveBeenCalled(); expect(typeof eleventyConfig.setServerOptions.mock.calls[0][0].setup).toBe("function"); expect(eleventyConfig.on).toHaveBeenCalled(); }); }); ================================================ FILE: test/EleventyVite.test.js ================================================ import { describe, it, beforeEach, afterEach, expect } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import EleventyVite from "../EleventyVite.js"; const outputPath = ".test-output"; const htmlFilePath = path.join(outputPath, "index.html"); const tempFolderName = ".test-11ty-vite"; const mockEleventyConfig = { directories: { output: outputPath }, logger: { logWithOptions: () => {}, }, }; const pluginOptions = { tempFolderName, viteOptions: { logLevel: "silent", }, }; describe("EleventyVite", () => { beforeEach(async () => { await fs.rm(outputPath, { recursive: true, force: true }).catch(() => {}); await fs.mkdir(outputPath, { recursive: true }); await fs.writeFile(path.join(outputPath, "style.css"), "body { color: red; }"); await fs.writeFile( htmlFilePath, ``, ); }); afterEach(async () => { await fs.rm(outputPath, { recursive: true, force: true }).catch(() => {}); await fs.rm(tempFolderName, { recursive: true, force: true }).catch(() => {}); }); it("constructor merges options", () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); expect(plugin.options.tempFolderName).toBe(tempFolderName); expect(plugin.options.viteOptions).toBeTruthy(); expect(plugin.options.viteOptions.appType).toBe("mpa"); }); it("getServer returns a Vite dev server", async () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const server = await plugin.getServer(); expect(server.middlewares).toBeTruthy(); await server.close(); }); it("getIgnoreDirectory returns correct path", () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); expect(plugin.getIgnoreDirectory()).toBe(path.join(tempFolderName, "**")); }); it("outputs HTML file to output path during build", async () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const input = [{ outputPath: htmlFilePath }]; await plugin.runBuild(input); const stat = await fs.stat(htmlFilePath); expect(stat.isFile()).toBe(true); }); it("runBuild runs Vite build and cleans up", async () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const input = [{ outputPath: htmlFilePath }]; await expect(plugin.runBuild(input)).resolves.not.toThrow(); const exists = await fs.stat(tempFolderName).then( () => true, () => false, ); expect(exists).toBe(false); }); it("references CSS file with hash in HTML after build", async () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const input = [{ outputPath: htmlFilePath }]; await plugin.runBuild(input); const html = await fs.readFile(htmlFilePath, "utf8"); const match = html.match( /]+href=["']([^"']*assets\/index-[a-zA-Z0-9]+\.css)["'][^>]*>/, ); expect(match).toBeTruthy(); const cssPath = match[1]; const cssFile = path.join(outputPath, cssPath); const stat = await fs.stat(cssFile); expect(stat.isFile()).toBe(true); }); it("getEleventyRolldownOptionsInput returns correct input object", () => { const entries = [ { outputPath: `${outputPath}index.html` }, { outputPath: `${outputPath}posts/index.html` }, { outputPath: `${outputPath}posts/hello/index.html` }, { outputPath: `${outputPath}robots.txt` }, { outputPath: false }, ]; const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const result = plugin.getEleventyRolldownOptionsInput(entries); expect(result).toEqual({ index: path.resolve(`${tempFolderName}/index.html`), "posts/index": path.resolve(`${tempFolderName}/posts/index.html`), "posts/hello/index": path.resolve(`${tempFolderName}/posts/hello/index.html`), }); }); it("getUserRolldownOptionsInput handles array input", () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const result = plugin.getUserRolldownOptionsInput(["script.js", "styles/main.css"]); expect(result).toEqual({ script: "script.js", "styles/main": "styles/main.css", }); }); it("getUserRolldownOptionsInput handles object input", () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const result = plugin.getUserRolldownOptionsInput({ script: "script.js", "styles/main": "styles/main.css", }); expect(result).toEqual({ script: "script.js", "styles/main": "styles/main.css", }); }); it("maps rollupOptions to rolldownOptions and logs a deprecation warning", () => { const logCalls = []; const mockConfigWithLogger = { ...mockEleventyConfig, logger: { logWithOptions: (opts) => logCalls.push(opts), }, }; const rollupInput = { main: "src/main.js" }; const options = { tempFolderName, viteOptions: { build: { rollupOptions: { input: rollupInput }, }, }, }; const plugin = new EleventyVite(mockConfigWithLogger, options); expect(plugin.options.viteOptions.build.rolldownOptions.input).toEqual(rollupInput); expect(plugin.options.viteOptions.build.rollupOptions).toBeUndefined(); const warning = logCalls.find((c) => c.message.includes("rollupOptions")); expect(warning).toBeTruthy(); expect(warning.type).toBe("info"); // original pluginOptions object must not be mutated expect(options.viteOptions.build.rollupOptions).toEqual({ input: rollupInput }); expect(options.viteOptions.build.rolldownOptions).toBeUndefined(); }); it("getUserRolldownOptionsInput handles string input", () => { const plugin = new EleventyVite(mockEleventyConfig, pluginOptions); const result = plugin.getUserRolldownOptionsInput("styles/main.css"); expect(result).toEqual({ "styles/main": "styles/main.css", }); }); });