├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── .eslintrc.js ├── LICENSE ├── package.json ├── README.md ├── .gitignore ├── lib └── Eleventy.js └── index.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "proseWrap": "preserve", 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | overrides: [ 4 | { 5 | files: ['*.js'], 6 | extends: ['eslint:recommended', 'google', 'plugin:prettier/recommended'], 7 | plugins: ['prettier'], 8 | }, 9 | ], 10 | env: { 11 | browser: false, 12 | node: true, 13 | es6: true, 14 | es2017: true, 15 | es2020: true, 16 | }, 17 | parserOptions: { 18 | parser: 'babel-eslint', 19 | ecmaVersion: 2020, 20 | sourceType: 'module', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sam Richard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-eleventy", 3 | "version": "0.3.1", 4 | "description": "A Vite plugin to build your site with 11ty", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "run-p *:lint", 8 | "fix": "run-p *:fix", 9 | "js:lint": "eslint .", 10 | "js:fix": "eslint . --fix", 11 | "prettier:lint": "prettier --check .", 12 | "prettier:fix": "prettier --write .", 13 | "prepare": "husky install", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/Snugug/vite-plugin-eleventy.git" 19 | }, 20 | "keywords": [ 21 | "vite-plugin", 22 | "eleventy" 23 | ], 24 | "author": "Sam Richard", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/Snugug/vite-plugin-eleventy/issues" 28 | }, 29 | "homepage": "https://github.com/Snugug/vite-plugin-eleventy#readme", 30 | "dependencies": { 31 | "@11ty/eleventy": "^1.0", 32 | "chalk": "^4.1.1" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^7.26.0", 36 | "eslint-config-google": "^0.14.0", 37 | "eslint-config-prettier": "^8.3.0", 38 | "eslint-plugin-prettier": "^3.4.0", 39 | "husky": "^6.0.0", 40 | "npm-run-all": "^4.1.5", 41 | "prettier": "^2.3.0" 42 | }, 43 | "volta": { 44 | "node": "14.17.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vite Plugin Eleventy 2 | 3 | Vite plugin to build out your site with [Eleventy](http://11ty.io/). Allows you to use the power of 11ty to build your HTML without needing to compile it to disk during development. 4 | 5 | ## Important Integration Caveats 6 | 7 | - Due to the nature of integrating Vite with Eleventy, not all Eleventy setups and plugins are guaranteed to work; when in doubt, consider moving to Vite plugins from Eleventy plugins (for instance, instead of using Eleventy [transforms](https://www.11ty.dev/docs/config/#transforms), consider writing/using a [PostHTML](https://github.com/posthtml/posthtml) plugin with [Vite Plugin PostHTML](https://www.npmjs.com/package/vite-plugin-posthtml)). 8 | - Because Vite has built-in handling for HTML files, it's recommended to _not_ use `.html` files with Eleventy. If you're writing Nunjucks, for instance, use `.njk` instead of `.html`. Don't rely on Eleventy's default template handling for HTML files is what I'm saying. 9 | 10 | ## Usage 11 | 12 | After installing, add it to your Vite config as follows: 13 | 14 | ```js 15 | const { eleventyPlugin } = require('vite-plugin-eleventy'); 16 | 17 | module.exports = { 18 | plugins: [eleventyPlugin()], 19 | }; 20 | ``` 21 | 22 | ## Config 23 | 24 | Use an [`.eleventy.js`](https://www.11ty.dev/docs/config/) configuration file to configure Eleventy. 25 | 26 | Additionally, the following options are available for configuration; pass them in as an object when instantiating the plugin: 27 | 28 | - `replace` - Array of arrays representing replacements to be made to a glob'd path to generate an input name for Rollup. Internal arrays are in the form of `[find, replace]`. Will be passed to `string.replace`. Defaults to `[[viteConfig.root, ''], ['/index.html', '']]` 29 | 30 | ## Important Eleventy differences 31 | 32 | - This plugin overrides Eleventy's input and output directories with Vite's root directory configuration. If you want to change where files live, you need to change Vite's root. This also means your 11ty template and include directories are relative to the Vite root. This also means you need to _not_ rely on Vite plugins to set your project root. Further testing will determine if this changes in the future. 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Package lock, cause this is a dependency 107 | package-lock.json -------------------------------------------------------------------------------- /lib/Eleventy.js: -------------------------------------------------------------------------------- 1 | const Eleventy = require('@11ty/eleventy'); 2 | const EleventyBaseError = require('@11ty/eleventy/src/EleventyBaseError'); 3 | const chalk = require('chalk'); 4 | 5 | /** 6 | * Extension of Eleventy to allow for JSON watching 7 | */ 8 | class EleventyJSONWatch extends Eleventy { 9 | /** 10 | * 11 | * @param {string} input Input directory 12 | * @param {string} output Output directory 13 | * @param {object} options Options object 14 | * @param {object} eleventyConfig Eleventy config 15 | */ 16 | constructor(input, output, options = {}, eleventyConfig = null) { 17 | super(input, output, options, eleventyConfig); 18 | } 19 | 20 | /** 21 | * 22 | * @param {string} msg Message to log 23 | * @param {string} file File path 24 | * @param {string} type One of 'info', 'warn', or 'error' 25 | */ 26 | viteLog(msg, file, type) { 27 | const prefix = '[vite-plugin-eleventy]'; 28 | 29 | const tag = 30 | type === 'info' 31 | ? chalk.cyan.bold(prefix) 32 | : type === 'warn' 33 | ? chalk.yellow.bold(prefix) 34 | : chalk.red.bold(prefix); 35 | 36 | this.logger.forceLog( 37 | `${chalk.dim(new Date().toLocaleTimeString())} ${tag} ${chalk.green(msg)} ${chalk.dim(file)}`, 38 | ); 39 | } 40 | 41 | /** 42 | * Start the watching of files. 43 | * 44 | * @async 45 | * @method 46 | */ 47 | async watch() { 48 | this.watcherBench.setMinimumThresholdMs(500); 49 | this.watcherBench.reset(); 50 | 51 | // We use a string module name and try/catch here to hide this from the zisi and esbuild serverless bundlers 52 | let chokidar; 53 | // eslint-disable-next-line no-useless-catch 54 | try { 55 | const moduleName = 'chokidar'; 56 | chokidar = require(moduleName); 57 | } catch (e) { 58 | throw e; 59 | } 60 | 61 | // Note that watching indirectly depends on this for fetching dependencies from JS files 62 | // See: TemplateWriter:pathCache and EleventyWatchTargets 63 | const result = await this.toJSON(); 64 | if (result.error) { 65 | // initial build failed—quit watch early 66 | return Promise.reject(result.error); 67 | } 68 | 69 | const initWatchBench = this.watcherBench.get('Start up --watch'); 70 | initWatchBench.before(); 71 | 72 | await this.initWatch(); 73 | 74 | // TODO improve unwatching if JS dependencies are removed (or files are deleted) 75 | const rawFiles = await this.getWatchedFiles(); 76 | if (process.env.DEBUG === 'Eleventy') { 77 | this.viteLog('Watching for changes to: %o', rawFiles); 78 | } 79 | 80 | const watcher = chokidar.watch(rawFiles, this.getChokidarConfig()); 81 | 82 | initWatchBench.after(); 83 | 84 | this.watcherBench.finish('Watch'); 85 | 86 | this.watcher = watcher; 87 | 88 | let watchDelay; 89 | const watchRun = async (path) => { 90 | try { 91 | this._addFileToWatchQueue(path); 92 | clearTimeout(watchDelay); 93 | 94 | await new Promise((resolve, reject) => { 95 | watchDelay = setTimeout(async () => { 96 | this._watch().then(resolve, reject); 97 | }, this.config.watchThrottleWaitTime); 98 | }); 99 | } catch (e) { 100 | if (e instanceof EleventyBaseError) { 101 | this.errorHandler.error(e, 'Eleventy watch error'); 102 | this.watchManager.setBuildFinished(); 103 | } else { 104 | this.errorHandler.fatal(e, 'Eleventy fatal watch error'); 105 | this.stopWatch(); 106 | } 107 | } 108 | }; 109 | 110 | watcher.on('change', async (path) => { 111 | this.viteLog('file changed', path, 'info'); 112 | this.viteLog('running 11ty', '', 'info'); 113 | await watchRun(path); 114 | }); 115 | 116 | watcher.on('add', async (path) => { 117 | this.viteLog('file added', path, 'info'); 118 | this.viteLog('running 11ty', '', 'info'); 119 | await watchRun(path); 120 | }); 121 | 122 | process.on('SIGINT', () => this.stopWatch()); 123 | } 124 | 125 | /** 126 | * tbd. 127 | * 128 | * @private 129 | * @method 130 | */ 131 | async _watch() { 132 | if (this.watchManager.isBuildRunning()) { 133 | return; 134 | } 135 | 136 | this.watchManager.setBuildRunning(); 137 | 138 | const queue = this.watchManager.getActiveQueue(); 139 | await this.config.events.emit('beforeWatch', queue); 140 | await this.config.events.emit('eleventy.beforeWatch', queue); 141 | 142 | // reset and reload global configuration :O 143 | if (this.watchManager.hasQueuedFile(this.eleventyConfig.getLocalProjectConfigFile())) { 144 | this.resetConfig(); 145 | } 146 | 147 | await this.restart(); 148 | 149 | this.watchTargets.clearDependencyRequireCache(); 150 | 151 | const incrementalFile = this.watchManager.getIncrementalFile(); 152 | if (incrementalFile) { 153 | this.writer.setIncrementalFile(incrementalFile); 154 | } 155 | 156 | await this.config.events.emit('watchChange', await this.toJSON()); 157 | // let writeResult = await this.write(); 158 | // let hasError = !!writeResult.error; 159 | 160 | this.writer.resetIncrementalFile(); 161 | 162 | this.watchTargets.reset(); 163 | 164 | await this._initWatchDependencies(); 165 | 166 | // Add new deps to chokidar 167 | this.watcher.add(this.watchTargets.getNewTargetsSinceLastReset()); 168 | 169 | this.watchManager.setBuildFinished(); 170 | 171 | if (this.watchManager.getPendingQueueSize() > 0) { 172 | this.viteLog( 173 | 'file saved while 11ty running', 174 | `${this.watchManager.getPendingQueueSize()} files in queue`, 175 | 'warn', 176 | ); 177 | await this._watch(); 178 | } 179 | } 180 | } 181 | 182 | module.exports = EleventyJSONWatch; 183 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EleventyJSONWatch = require('./lib/Eleventy'); 2 | const { extname } = require('path'); 3 | const path = require('path'); 4 | const os = require('os'); 5 | 6 | /** 7 | * Normalizes a path to remove any `.` and `..` segments to their POSIX equivalents. 8 | * Borrowed from Vite so this plugin doesn't need to depend on Vite 9 | * @param {string} id - The path to normalize. 10 | * @return {string} - The normalized path. 11 | **/ 12 | function normalizePath(id) { 13 | return path.posix.normalize(os.platform() === 'win32' ? id.replace(/\\/g, '/') : id); 14 | } 15 | 16 | /** 17 | * Vite plugin for Eleventy. 18 | * @param {Object} opts - The options for the plugin. 19 | * @param {Array.} opts.replace - An array of arrays of strings representing path portions to be replaced when building Rollup output. First item should be find, second should be replace, assuming POSIX paths. 20 | * @return {Object} - The plugin object. 21 | **/ 22 | const eleventyPlugin = (opts = {}) => { 23 | let eleventy; 24 | let files = []; 25 | let output = []; 26 | const outIDs = []; 27 | const outFiles = []; 28 | let base; 29 | 30 | // Set up user options 31 | const options = Object.assign( 32 | { 33 | replace: [['/index.html', '']], 34 | }, 35 | opts, 36 | ); 37 | 38 | const contentTypes = { 39 | js: 'application/javascript', 40 | css: 'text/css', 41 | html: 'text/html', 42 | json: 'application/json', 43 | }; 44 | 45 | return { 46 | name: 'eleventy', 47 | enforce: 'pre', 48 | 49 | // This _should_ be done in configResolved but we need to generate the HTML and the input files _before_ the config gets resolved. As a compromise, an error will be thrown in configResolved if the root changes. 50 | async config(config, { command }) { 51 | // Determine Vite's root. Because it can be an absolute or relative path, we're `path.resolve`ing it, then figuring out the relative path because that's what Eleventy needs. 52 | base = config.root ? path.resolve(config.root) : process.cwd(); 53 | base = path.relative(process.cwd(), base) || '.'; 54 | 55 | eleventy = new EleventyJSONWatch(base, base); 56 | await eleventy.init(); 57 | files = await eleventy.toJSON(); 58 | 59 | // On build, write files, glob the HTML, and add them to Build Rollup Options 60 | if (command === 'build') { 61 | // Add relative path to replacements for build files. 62 | if (base !== '.') { 63 | options.replace.unshift([base, '']); 64 | } 65 | 66 | // Determine output file object 67 | output = files.reduce((acc, cur) => { 68 | let name = cur.outputPath; 69 | // Removes all "replacements" from the output path to build name 70 | for (const r of options.replace) { 71 | name = name.replace(r[0], r[1]); 72 | } 73 | name = name.startsWith('/') ? name.substring(1) : name; 74 | 75 | cur.outId = path.join(process.cwd(), cur.outputPath); 76 | 77 | acc[name] = cur.outId; 78 | outIDs.push(cur.outId); 79 | outFiles.push(cur); 80 | return acc; 81 | }, {}); 82 | 83 | // Return 11ty rollup inputs 84 | return { 85 | build: { 86 | rollupOptions: { 87 | input: output, 88 | }, 89 | }, 90 | }; 91 | } 92 | }, 93 | 94 | configResolved(resolvedConfig) { 95 | // If the root changes, throw an error 96 | const baseRoot = normalizePath(path.resolve(base)); 97 | if (baseRoot !== normalizePath(resolvedConfig.root)) { 98 | throw new Error( 99 | 'A plugin has changed the Vite root after [vite-plugin-eleventy] has run. Please make sure any plugins that change the Vite root run before this one.', 100 | ); 101 | } 102 | }, 103 | 104 | // Resolves IDs of files built by 11ty 105 | resolveId(id) { 106 | if (outIDs.includes(id)) { 107 | return id; 108 | } 109 | 110 | return null; 111 | }, 112 | 113 | // Loads files built by 11ty 114 | load(id) { 115 | if (outIDs.includes(id)) { 116 | return outFiles.find((f) => f.outId === id).content; 117 | } 118 | return null; 119 | }, 120 | 121 | // Configures dev server to respond with virtual 11ty output 122 | configureServer(server) { 123 | // Set up 11ty watcher and reload. 124 | eleventy.watch(); 125 | eleventy.config.events.on('watchChange', (f) => { 126 | files = f; 127 | if (server.ws) { 128 | server.ws.send({ 129 | type: 'full-reload', 130 | event: 'eleventy-update', 131 | data: {}, 132 | }); 133 | } 134 | }); 135 | 136 | // Setup Vite dev server middlware to respond with virtual 11ty output 137 | server.middlewares.use(async (req, res, next) => { 138 | // Need to grab the pathname, not the request url, to match against 11ty output 139 | const { pathname } = req._parsedUrl; 140 | const url = pathname.endsWith('/') ? pathname : `${pathname}/`; 141 | 142 | // Find the file if it exists! 143 | const output = files.find((r) => r.url === url); 144 | if (output) { 145 | let ct = ''; 146 | let content = '' + output.content; 147 | 148 | // Manage transforms and content types 149 | if ((extname(url) === '' && url.endsWith('/')) || extname(url) === '.html') { 150 | // If it's an HTML file or a route, run it through transformIndexHtml 151 | content = await server.transformIndexHtml(url, content, req.originalUrl); 152 | ct = 'html'; 153 | } else { 154 | // Otherwise, run it through transformRequest 155 | content = await server.transformRequest(url, content, req.originalUrl); 156 | ct = extname(url).replace('.', ''); 157 | } 158 | 159 | return res 160 | .writeHead(200, { 161 | 'Content-Length': Buffer.byteLength(content), 162 | 'Content-Type': contentTypes[ct] || 'text/plain', 163 | }) 164 | .end(content); 165 | } 166 | 167 | return next(); 168 | }); 169 | }, 170 | }; 171 | }; 172 | 173 | module.exports = { 174 | eleventyPlugin, 175 | }; 176 | --------------------------------------------------------------------------------