├── .eleventy.js ├── .eleventyignore ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── EleventyVue.js ├── README.md ├── package.json └── test ├── sample ├── _includes │ ├── anothersubdir │ │ └── hi.vue │ ├── child.vue │ ├── layout.njk │ ├── myHeader.vue │ ├── noDiv.vue │ ├── sibling.vue │ └── subdir │ │ └── hi.vue ├── another-page.vue └── index.vue ├── stubs-a ├── _includes │ ├── child.vue │ └── grandchild.vue └── data.vue ├── stubs-b ├── _includes │ ├── child.vue │ └── grandchild.vue └── data.vue ├── stubs-c ├── _includes │ ├── child.vue │ └── grandchild.vue └── data.vue ├── stubs-d ├── _includes │ ├── child.vue │ └── grandchild.vue └── data.vue ├── stubs-data-leak └── index.vue ├── stubs-e └── data.vue ├── stubs-layout ├── _includes │ └── layout.vue └── page.vue ├── stubs-postcss └── data.vue └── test.js /.eleventy.js: -------------------------------------------------------------------------------- 1 | const lodashMerge = require("lodash.merge"); 2 | const debug = require("debug")("EleventyVue"); 3 | 4 | const { InlineCodeManager } = require("@11ty/eleventy-assets"); 5 | 6 | const EleventyVue = require("./EleventyVue"); 7 | 8 | const pkg = require("./package.json"); 9 | 10 | const globalOptions = { 11 | input: [], // point to a specific list of Vue files (defaults to **/*.vue) 12 | 13 | readOnly: false, 14 | 15 | cacheDirectory: ".cache/vue/", 16 | 17 | // See https://www.rollupjs.org/guide/en/#big-list-of-options 18 | rollupOptions: {}, 19 | 20 | // See https://rollup-plugin-vue.vuejs.org/options.html 21 | rollupPluginVueOptions: {}, 22 | 23 | assets: { 24 | css: null 25 | } // optional `eleventy-assets` instances 26 | }; 27 | 28 | module.exports = function(eleventyConfig, configGlobalOptions = {}) { 29 | try { 30 | eleventyConfig.versionCheck(pkg["11ty"].compatibility); 31 | } catch(e) { 32 | console.log( `WARN: Eleventy Plugin (${pkg.name}) Compatibility: ${e.message}` ); 33 | } 34 | 35 | let options = lodashMerge({}, globalOptions, configGlobalOptions); 36 | 37 | let eleventyVue = new EleventyVue(); 38 | eleventyVue.setCacheDir(options.cacheDirectory); 39 | eleventyVue.setReadOnly(options.readOnly); 40 | 41 | let cssManager = options.assets.css || new InlineCodeManager(); 42 | eleventyVue.setCssManager(cssManager); 43 | 44 | let changedVueFilesOnWatch = []; 45 | let skipVueBuild = false; 46 | 47 | // Only add this filter if you’re not re-using your own asset manager. 48 | // TODO Add warnings to readme 49 | // * This will probably only work in a layout template. 50 | // * Probably complications with components that are only used in a layout template. 51 | eleventyConfig.addFilter("getVueComponentCssForPage", (url) => { 52 | let components = cssManager.getComponentListForUrl(url); 53 | let css = cssManager.getCodeForUrl(url); 54 | debug("Component CSS for %o component count: %o, CSS size: %o: %O", url, components.length, css.length, components); 55 | return css; 56 | }); 57 | 58 | let eleventyIgnores; 59 | eleventyConfig.on("eleventy.ignores", ignores => { 60 | eleventyIgnores = ignores; 61 | }); 62 | 63 | // Default output 64 | let isVerboseMode = true; 65 | eleventyConfig.on("eleventy.config", config => { 66 | // Available in 1.0.0-beta.6+ 67 | if(config.verbose !== undefined) { 68 | isVerboseMode = config.verbose; 69 | } 70 | }); 71 | 72 | eleventyConfig.on("afterBuild", () => { 73 | let count = eleventyVue.componentsWriteCount; 74 | if(isVerboseMode && count > 0) { 75 | console.log( `Built ${count} component${count !== 1 ? "s" : ""} (eleventy-plugin-vue v${pkg.version}${version ? ` with Vue ${version}` : ""})` ); 76 | } 77 | }); 78 | 79 | // `beforeWatch` is available on Eleventy 0.11.0 and newer 80 | eleventyConfig.on("beforeWatch", (changedFiles) => { 81 | let hasChangedFiles = changedFiles && changedFiles.length > 0; 82 | 83 | // `changedFiles` array argument is available on Eleventy 0.11.1+ 84 | changedVueFilesOnWatch = (changedFiles || []).filter(file => file.endsWith(".vue")); 85 | 86 | // Only reset what changed! (Partial builds for Vue rollup files) 87 | if(changedVueFilesOnWatch.length > 0) { 88 | skipVueBuild = false; 89 | } else { 90 | if(hasChangedFiles) { 91 | skipVueBuild = true; 92 | } 93 | } 94 | eleventyVue.clearRequireCache(); 95 | }); 96 | 97 | eleventyConfig.addTemplateFormats("vue"); 98 | 99 | eleventyConfig.addExtension("vue", { 100 | read: false, // We use rollup to read the files 101 | getData: [ // get data from both the data function and serverPrefetch 102 | "data", 103 | // including this by default is bad because a lot of async data fetching happens here! 104 | // "serverPrefetch" 105 | ], 106 | getInstanceFromInputPath: function(inputPath) { 107 | return eleventyVue.getComponent(inputPath); 108 | }, 109 | init: async function() { 110 | eleventyVue.setInputDir(this.config.inputDir); 111 | eleventyVue.setIncludesDir(this.config.dir.includes); 112 | eleventyVue.setLayoutsDir(this.config.dir.layouts); 113 | eleventyVue.resetIgnores(eleventyIgnores); 114 | 115 | eleventyVue.setRollupOptions(options.rollupOptions); 116 | eleventyVue.setRollupPluginVueOptions(options.rollupPluginVueOptions); 117 | 118 | if(skipVueBuild) { 119 | // we only call this to set the write count for the build 120 | eleventyVue.createVueComponents([]); 121 | } else if(options.readOnly && eleventyVue.hasRollupOutputCache()) { 122 | await eleventyVue.loadRollupOutputCache(); 123 | } else { 124 | let files = changedVueFilesOnWatch; 125 | let isSubset = false; 126 | 127 | if(files && files.length) { 128 | isSubset = true; 129 | } else { 130 | // input passed in via config 131 | if(options.input && options.input.length) { 132 | files = options.input; 133 | isSubset = true; 134 | } else { 135 | files = await eleventyVue.findFiles(); 136 | } 137 | } 138 | 139 | // quit early 140 | if(!files || !files.length) { 141 | return; 142 | } 143 | 144 | try { 145 | let bundle = await eleventyVue.getBundle(files, isSubset); 146 | let output = await eleventyVue.write(bundle); 147 | 148 | eleventyVue.createVueComponents(output); 149 | } catch(e) { 150 | if(e.loc) { 151 | e.message = `Error in Vue file ${e.loc.file} on Line ${e.loc.line} Column ${e.loc.column}: ${e.message}` 152 | } 153 | throw e; 154 | } 155 | 156 | if(!options.readOnly && !isSubset) { // implied eleventyVue.hasRollupOutputCache() was false 157 | await eleventyVue.writeRollupOutputCache(); 158 | } 159 | } 160 | }, 161 | 162 | // Caching 163 | compileOptions: { 164 | cache: true, 165 | permalink: false, 166 | getCacheKey: function(str, inputPath) { 167 | return inputPath; 168 | }, 169 | }, 170 | 171 | compile: function(str, inputPath) { 172 | return async (data) => { 173 | // since `read: false` is set 11ty doesn't read file contents 174 | // so if str has a value, it's a permalink (which can be a string or a function) 175 | // currently Vue template syntax in permalink string is not supported. 176 | let vueMixin = { 177 | methods: this.config.javascriptFunctions, 178 | }; 179 | 180 | if (str) { 181 | if(typeof str === "function") { 182 | return str(data); 183 | } 184 | if(typeof str === "string" && str.trim().charAt("0") === "<") { 185 | return eleventyVue.renderString(str, data, vueMixin); 186 | } 187 | return str; 188 | } 189 | 190 | let vueComponent = eleventyVue.getComponent(inputPath); 191 | let componentName = eleventyVue.getJavaScriptComponentFile(inputPath); 192 | 193 | // if user attempts to render a Vue template in `serverPrefetch` or `data` to add to the data cascade 194 | // this will fail because data.page does not exist yet! 195 | if(data.page) { 196 | debug("Vue CSS: Adding component %o to %o", componentName, data.page.url); 197 | cssManager.addComponentForUrl(componentName, data.page.url); 198 | } 199 | 200 | return eleventyVue.renderComponent(vueComponent, data, vueMixin); 201 | }; 202 | } 203 | }); 204 | }; 205 | 206 | module.exports.EleventyVue = EleventyVue; 207 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | .cache/ -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches-ignore: 4 | - "gh-pages" 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 11 | node: ["12", "14", "16"] 12 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: npm install 20 | - run: npm run testci 21 | env: 22 | DEBUG: EleventyVue* 23 | env: 24 | YARN_GPG: no -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules 3 | _site 4 | package-lock.json -------------------------------------------------------------------------------- /EleventyVue.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const fsp = fs.promises; 4 | const fastglob = require("fast-glob"); 5 | const lodashMerge = require("lodash.merge"); 6 | 7 | const rollup = require("rollup"); 8 | const rollupPluginVue = require("rollup-plugin-vue"); 9 | const rollupPluginCssOnly = require("rollup-plugin-css-only"); 10 | 11 | const { createSSRApp } = require("vue"); 12 | const { renderToString } = require("@vue/server-renderer"); 13 | 14 | const debug = require("debug")("EleventyVue"); 15 | const debugDev = require("debug")("Dev:EleventyVue"); 16 | 17 | function addLeadingDotSlash(pathArg) { 18 | if (pathArg === "." || pathArg === "..") { 19 | return pathArg + path.sep; 20 | } 21 | 22 | if ( 23 | path.isAbsolute(pathArg) || 24 | pathArg.startsWith("." + path.sep) || 25 | pathArg.startsWith(".." + + path.sep) 26 | ) { 27 | return pathArg; 28 | } 29 | 30 | return "." + path.sep + pathArg; 31 | } 32 | 33 | class EleventyVue { 34 | constructor(cacheDirectory) { 35 | this.workingDir = path.resolve("."); 36 | this.ignores = new Set(); 37 | 38 | this.vueFileToCSSMap = {}; 39 | this.vueFileToJavaScriptFilenameMap = {}; 40 | this.componentRelationships = []; 41 | 42 | this.rollupBundleOptions = { 43 | format: "cjs", // because we’re consuming these in node. See also "esm" 44 | exports: "auto", 45 | preserveModules: true, // keeps separate files on the file system 46 | // dir: this.cacheDir // set via setCacheDir 47 | entryFileNames: (chunkInfo) => { 48 | debugDev("Rollup chunk %o", chunkInfo.facadeModuleId); 49 | return "[name].js"; 50 | } 51 | }; 52 | 53 | this.setCacheDir(cacheDirectory); 54 | 55 | this.componentsWriteCount = 0; 56 | this.readOnly = false; 57 | } 58 | 59 | setReadOnly(readOnly) { 60 | this.readOnly = !!readOnly; 61 | } 62 | 63 | getEntryFileName(localpath) { 64 | if(localpath.endsWith(".vue")) { 65 | localpath = localpath.substr(0, localpath.length - 4) + ".js"; 66 | } 67 | if(localpath.startsWith(this.workingDir)) { 68 | localpath = localpath.substr(this.workingDir.length); 69 | } 70 | let split = localpath.split(path.sep); 71 | if(!split[0]) { 72 | split.shift(); 73 | } 74 | return split.join("__"); 75 | } 76 | 77 | reset() { 78 | this.vueFileToCSSMap = {}; 79 | this.componentRelationships = []; 80 | } 81 | 82 | // Deprecated, use resetCSSFor above 83 | resetFor(localVuePath) { 84 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 85 | 86 | debug("Clearing CSS styleNodes in Vue for %o", localVuePath); 87 | this.vueFileToCSSMap[localVuePath] = []; 88 | } 89 | 90 | setCssManager(cssManager) { 91 | this.cssManager = cssManager; 92 | } 93 | 94 | setRollupOptions(options) { 95 | this.rollupOptions = options; 96 | } 97 | 98 | getMergedRollupOptions(input, isSubsetOfFiles) { 99 | let options = { 100 | input, 101 | onwarn (warning, warn) { 102 | if(warning.code === "UNUSED_EXTERNAL_IMPORT") { 103 | debug("Unused external import: %O", warning); 104 | } else { 105 | warn(warning); 106 | } 107 | }, 108 | external: [ 109 | "vue", 110 | "vue/server-renderer", 111 | "@vue/server-renderer", 112 | ], 113 | plugins: [ 114 | rollupPluginVue(this.getRollupPluginVueOptions()), 115 | rollupPluginCssOnly({ 116 | output: async (styles, styleNodes) => { 117 | this.resetCSSFor(styleNodes); 118 | this.addRawCSS(styleNodes); 119 | 120 | if(!this.readOnly && !isSubsetOfFiles) { 121 | await this.writeRollupOutputCacheCss(styleNodes); 122 | } 123 | } 124 | }), 125 | ] 126 | }; 127 | 128 | for(let key in this.rollupOptions) { 129 | if(key === "external" || key === "plugins") { 130 | // merge the Array 131 | options[key] = options[key].concat(this.rollupOptions[key]); 132 | } else { 133 | options[key] = this.rollupOptions[key]; 134 | } 135 | } 136 | 137 | return options; 138 | } 139 | 140 | setRollupPluginVueOptions(rollupPluginVueOptions) { 141 | this.rollupPluginVueOptions = rollupPluginVueOptions; 142 | } 143 | 144 | getRollupPluginVueOptions() { 145 | return lodashMerge({ 146 | target: "node", 147 | exposeFilename: true, 148 | // preprocessStyles: false, // false is default 149 | // compilerOptions: {} // https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#options 150 | }, this.rollupPluginVueOptions); 151 | } 152 | 153 | resetIgnores(extraIgnores = []) { 154 | this.ignores = new Set(); 155 | 156 | // These need to be forced to forward slashes for comparison 157 | let relativeIncludesDir = this.rawIncludesDir ? EleventyVue.forceForwardSlashOnFilePath(addLeadingDotSlash(path.join(this.relativeInputDir, this.rawIncludesDir))) : undefined; 158 | let relativeLayoutsDir = this.rawLayoutsDir ? EleventyVue.forceForwardSlashOnFilePath(addLeadingDotSlash(path.join(this.relativeInputDir, this.rawLayoutsDir))) : undefined; 159 | 160 | // don’t add ignores that match includes or layouts dirs 161 | for(let ignore of extraIgnores) { 162 | if(relativeIncludesDir && ignore.startsWith(relativeIncludesDir)) { 163 | // do nothing 164 | debug( "Skipping ignore from eleventy.ignores event: %o, matched includes dir", ignore); 165 | } else if(relativeLayoutsDir && ignore.startsWith(relativeLayoutsDir)) { 166 | // do nothing 167 | debug( "Skipping ignore from eleventy.ignores event: %o, matched layouts dir", ignore); 168 | } else { 169 | debug( "Adding ignore from eleventy.ignores event: %o %O %O", ignore, { relativeIncludesDir }, { relativeLayoutsDir } ); 170 | this.ignores.add(ignore); 171 | } 172 | } 173 | } 174 | 175 | setInputDir(inputDir) { 176 | this.relativeInputDir = inputDir; 177 | this.inputDir = path.join(this.workingDir, inputDir); 178 | } 179 | 180 | setIncludesDir(includesDir) { 181 | if(includesDir) { 182 | // Was: path.join(this.workingDir, includesDir); 183 | // Which seems wrong? per https://www.11ty.dev/docs/config/#directory-for-includes 184 | this.rawIncludesDir = includesDir; 185 | this.includesDir = path.join(this.inputDir, includesDir); 186 | } 187 | } 188 | 189 | setLayoutsDir(layoutsDir) { 190 | if(layoutsDir) { 191 | this.rawLayoutsDir = layoutsDir; 192 | this.layoutsDir = path.join(this.inputDir, layoutsDir); 193 | } 194 | } 195 | 196 | // adds leading ./ 197 | _createRequirePath(...paths) { 198 | let joined = path.join(...paths); 199 | if(joined.startsWith(path.sep)) { 200 | return joined; 201 | } 202 | return `.${path.sep}${joined}`; 203 | } 204 | 205 | setCacheDir(cacheDir) { 206 | this.cacheDir = cacheDir; 207 | this.rollupBundleOptions.dir = cacheDir; 208 | 209 | this.bypassRollupCacheCssFile = this._createRequirePath(this.cacheDir || "", "eleventy-vue-rollup-css.json"); 210 | this.bypassRollupCacheFile = this._createRequirePath(this.cacheDir || "", "eleventy-vue-rollup.json"); 211 | } 212 | 213 | getFullCacheDir() { 214 | if(this.cacheDir.startsWith("/")) { 215 | return this.cacheDir; 216 | } 217 | return path.join(this.workingDir, this.cacheDir); 218 | } 219 | 220 | isIncludeFile(filepath) { 221 | return filepath.startsWith(this.includesDir); 222 | } 223 | 224 | // TODO pass in a filename and only clear the appropriate files 225 | clearRequireCache() { 226 | let fullCacheDir = this.getFullCacheDir(); 227 | let deleteCount = 0; 228 | for(let fullPath in require.cache) { 229 | if(fullPath.startsWith(fullCacheDir)) { 230 | deleteCount++; 231 | debugDev( "Deleting from require cache: %o", fullPath ); 232 | delete require.cache[fullPath]; 233 | } 234 | } 235 | debug( "Deleted %o vue components from require.cache.", deleteCount ); 236 | } 237 | 238 | async findFiles(glob = "**/*.vue") { 239 | let globPaths = [ 240 | addLeadingDotSlash(path.join(this.relativeInputDir, glob)) 241 | ]; 242 | 243 | if(this.includesDir) { 244 | if(!this.includesDir.startsWith(this.inputDir)) { 245 | globPaths.push( 246 | addLeadingDotSlash(path.join(this.relativeIncludesDir, glob)) 247 | ); 248 | } 249 | } 250 | 251 | if(this.layoutsDir) { 252 | if(!this.layoutsDir.startsWith(this.inputDir)) { 253 | globPaths.push( 254 | addLeadingDotSlash(path.join(this.relativeLayoutsDir, glob)) 255 | ); 256 | } 257 | } 258 | 259 | // ignores should not include layouts or includes directories, filtered out above. 260 | let ignores = Array.from(this.ignores).map(ignore => EleventyVue.forceForwardSlashOnFilePath(ignore)); 261 | globPaths = globPaths.map(path => EleventyVue.forceForwardSlashOnFilePath(path)); 262 | debug("Looking for %O and ignoring %O", globPaths, ignores); 263 | 264 | // MUST use forward slashes here (even in Windows), per fast-glob requirements 265 | return fastglob(globPaths, { 266 | caseSensitiveMatch: false, 267 | // dot: true, 268 | ignore: ignores, 269 | }); 270 | } 271 | 272 | // Glob is optional 273 | async getBundle(input, isSubsetOfFiles = false) { 274 | if(!input) { 275 | input = await this.findFiles(); 276 | } 277 | 278 | debug("Processing %o Vue files", input.length); 279 | 280 | if(!this.readOnly) { 281 | await fsp.mkdir(this.cacheDir, { 282 | recursive: true 283 | }); 284 | } 285 | 286 | debug("Found these input files: %O", input); 287 | let options = this.getMergedRollupOptions(input, isSubsetOfFiles); 288 | let bundle = await rollup.rollup(options); 289 | 290 | return bundle; 291 | } 292 | 293 | async _operateOnBundle(bundle, rollupMethod = "write") { 294 | if(!bundle) { 295 | throw new Error("Eleventy Vue Plugin: write(bundle) needs a bundle argument."); 296 | } 297 | 298 | let { output } = await bundle[rollupMethod](this.rollupBundleOptions); 299 | 300 | output = output.filter(entry => !!entry.facadeModuleId); 301 | 302 | return output; 303 | } 304 | 305 | async write(bundle) { 306 | return this._operateOnBundle(bundle, "write"); 307 | } 308 | 309 | async generate(bundle) { 310 | return this._operateOnBundle(bundle, "generate"); 311 | } 312 | 313 | hasRollupOutputCache() { 314 | return fs.existsSync(this.bypassRollupCacheFile) && fs.existsSync(this.bypassRollupCacheCssFile); 315 | } 316 | 317 | async writeRollupOutputCache() { 318 | if(this.readOnly) { 319 | return; 320 | } 321 | 322 | debug("Writing rollup cache to file system %o", this.bypassRollupCacheFile); 323 | return fsp.writeFile(this.bypassRollupCacheFile, JSON.stringify({ 324 | vueToJs: this.vueFileToJavaScriptFilenameMap, 325 | relationships: this.componentRelationships 326 | }, null, 2)); 327 | } 328 | 329 | async writeRollupOutputCacheCss(styleNodes) { 330 | if(this.readOnly) { 331 | return; 332 | } 333 | // convert to local paths 334 | let localPathStyleNodes = {}; 335 | for(let fullVuePath in styleNodes) { 336 | let localVuePath = this.getLocalVueFilePath(fullVuePath); 337 | // one file can have multiple CSS blocks 338 | if(!localPathStyleNodes[localVuePath]) { 339 | localPathStyleNodes[localVuePath] = []; 340 | } 341 | localPathStyleNodes[localVuePath].push(styleNodes[fullVuePath]); 342 | } 343 | debug("Writing rollup cache CSS to file system %o", this.bypassRollupCacheCssFile); 344 | return fsp.writeFile(this.bypassRollupCacheCssFile, JSON.stringify(localPathStyleNodes, null, 2)); 345 | } 346 | 347 | async loadRollupOutputCache() { 348 | debugDev("Using rollup file system cache to bypass rollup."); 349 | let styleNodes = JSON.parse(await fsp.readFile(this.bypassRollupCacheCssFile, "utf8")); 350 | for(let localVuePath in styleNodes) { 351 | for(let css of styleNodes[localVuePath]) { 352 | this.addCSSViaLocalPath(localVuePath, css); 353 | } 354 | } 355 | 356 | let { vueToJs, relationships } = JSON.parse(await fsp.readFile(this.bypassRollupCacheFile, "utf8")); 357 | this.vueFileToJavaScriptFilenameMap = vueToJs; 358 | this.componentRelationships = relationships; 359 | 360 | if(this.cssManager) { 361 | // Re-insert CSS code in the CSS manager 362 | for(let localVuePath in vueToJs) { 363 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 364 | 365 | let css = this.getCSSForComponent(localVuePath); 366 | if(css) { 367 | let jsFilename = vueToJs[localVuePath]; 368 | this.cssManager.addComponentCode(jsFilename, css); 369 | } 370 | } 371 | 372 | // Re-establish both component relationships 373 | for(let relation of this.componentRelationships) { 374 | this.cssManager.addComponentRelationship(relation.from, relation.to); 375 | } 376 | } 377 | } 378 | 379 | // output is returned from .write() or .generate() 380 | createVueComponents(output) { 381 | this.componentsWriteCount = 0; 382 | for(let entry of output) { 383 | let fullVuePath = entry.facadeModuleId; 384 | let inputPath = this.getLocalVueFilePath(fullVuePath); 385 | let jsFilename = entry.fileName; 386 | let intermediateComponent = false; 387 | let css; 388 | 389 | if(fullVuePath.endsWith("&lang.js")) { 390 | intermediateComponent = true; 391 | css = false; 392 | } else { 393 | debugDev("Adding Vue file to JS component file name mapping: %o to %o (via %o)", inputPath, entry.fileName, fullVuePath); 394 | this.addVueToJavaScriptMapping(inputPath, jsFilename); 395 | this.componentsWriteCount++; 396 | 397 | css = this.getCSSForComponent(inputPath); 398 | if(css && this.cssManager) { 399 | this.cssManager.addComponentCode(jsFilename, css); 400 | } 401 | } 402 | 403 | if(this.cssManager) { 404 | // If you import it, it will roll up the imported CSS in the CSS manager 405 | let importList = entry.imports || []; 406 | // debugDev("filename: %o importedBindings:", entry.fileName, Object.keys(entry.importedBindings)); 407 | debugDev("filename: %o imports:", entry.fileName, entry.imports); 408 | // debugDev("modules: %O", Object.keys(entry.modules)); 409 | 410 | for(let importFilename of importList) { 411 | // TODO is this necessary? 412 | if(importFilename.endsWith(path.join("vue-runtime-helpers/dist/normalize-component.js"))) { 413 | continue; 414 | } 415 | 416 | this.componentRelationships.push({ from: jsFilename, to: importFilename }); 417 | this.cssManager.addComponentRelationship(jsFilename, importFilename); 418 | } 419 | } 420 | 421 | debugDev("Created %o from %o" + (css ? " w/ CSS" : " without CSS") + (intermediateComponent ? " (intermediate/connector component)" : ""), jsFilename, inputPath); 422 | } 423 | 424 | debug("Created %o Vue components", this.componentsWriteCount); 425 | } 426 | 427 | getLocalVueFilePath(fullPath) { 428 | let filePath = fullPath; 429 | if(fullPath.startsWith(this.workingDir)) { 430 | filePath = `.${fullPath.substr(this.workingDir.length)}`; 431 | } 432 | let extension = ".vue"; 433 | let localVuePath = filePath.substr(0, filePath.lastIndexOf(extension) + extension.length); 434 | return EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 435 | } 436 | 437 | /* CSS */ 438 | resetCSSFor(styleNodes) { 439 | for(let fullVuePath in styleNodes) { 440 | let localVuePath = this.getLocalVueFilePath(fullVuePath); 441 | delete this.vueFileToCSSMap[localVuePath]; 442 | 443 | if(this.cssManager) { 444 | let jsFilename = this.getJavaScriptComponentFile(localVuePath); 445 | this.cssManager.resetComponentCodeFor(jsFilename); 446 | } 447 | } 448 | } 449 | 450 | addRawCSS(styleNodes) { 451 | for(let fullVuePath in styleNodes) { 452 | this.addCSS(fullVuePath, styleNodes[fullVuePath]); 453 | } 454 | } 455 | 456 | addCSS(fullVuePath, cssText) { 457 | let localVuePath = this.getLocalVueFilePath(fullVuePath); 458 | this.addCSSViaLocalPath(localVuePath, cssText); 459 | } 460 | 461 | addCSSViaLocalPath(localVuePath, cssText) { 462 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 463 | 464 | if(!this.vueFileToCSSMap[localVuePath]) { 465 | this.vueFileToCSSMap[localVuePath] = []; 466 | } 467 | let css = cssText.trim(); 468 | if(css) { 469 | debugDev("Adding CSS to %o, length: %o", localVuePath, css.length); 470 | 471 | this.vueFileToCSSMap[localVuePath].push(css); 472 | } 473 | } 474 | 475 | getCSSForComponent(localVuePath) { 476 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 477 | 478 | let css = (this.vueFileToCSSMap[localVuePath] || []).join("\n"); 479 | debugDev("Getting CSS for component: %o, length: %o", localVuePath, css.length); 480 | return css; 481 | } 482 | 483 | /* Map from vue files to compiled JavaScript files */ 484 | addVueToJavaScriptMapping(localVuePath, jsFilename) { 485 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 486 | 487 | this.vueFileToJavaScriptFilenameMap[localVuePath] = jsFilename; 488 | } 489 | 490 | getJavaScriptComponentFile(localVuePath) { 491 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 492 | 493 | return this.vueFileToJavaScriptFilenameMap[localVuePath]; 494 | } 495 | 496 | // localVuePath is already normalized to local OS directory separator at this point 497 | getFullJavaScriptComponentFilePath(localVuePath) { 498 | localVuePath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 499 | 500 | let jsFilename = this.getJavaScriptComponentFile(localVuePath); 501 | if(!jsFilename) { 502 | throw new Error("Could not find compiled JavaScript file for Vue component: " + localVuePath); 503 | } 504 | 505 | debugDev("Map vue path to JS component file: %o to %o", localVuePath, jsFilename); 506 | let fullComponentPath = path.join(this.getFullCacheDir(), jsFilename); 507 | return fullComponentPath; 508 | } 509 | 510 | getComponent(localVuePath) { 511 | let filepath = EleventyVue.normalizeOperatingSystemFilePath(localVuePath); 512 | let fullComponentPath = this.getFullJavaScriptComponentFilePath(filepath); 513 | let component = require(fullComponentPath); 514 | return component; 515 | } 516 | 517 | async renderString(str, data, mixin = {}) { 518 | return this.renderComponent({ 519 | template: str 520 | }, data, mixin); 521 | } 522 | 523 | async renderComponent(vueComponent, pageData, mixin = {}) { 524 | // console.log( pageData ); 525 | const app = createSSRApp(vueComponent); 526 | // Allow `page` to be accessed inside any Vue component 527 | // https://v3.vuejs.org/api/application-config.html#globalproperties 528 | app.config.globalProperties.page = pageData.page; 529 | 530 | // TODO hook for app modifications 531 | // app.config.warnHandler = function(msg, vm, trace) { 532 | // console.log( "[Vue 11ty] Warning", msg, vm, trace ); 533 | // }; 534 | // app.config.errorHandler = function(msg, vm, info) { 535 | // console.log( "[Vue 11ty] Error", msg, vm, info ); 536 | // }; 537 | 538 | app.mixin(mixin); 539 | 540 | // Full data cascade is available to the root template component 541 | app.mixin({ 542 | data: function() { 543 | return pageData; 544 | }, 545 | }); 546 | 547 | // returns a promise 548 | return renderToString(app); 549 | } 550 | } 551 | 552 | EleventyVue.normalizeOperatingSystemFilePath = function(filePath, sep = "/") { 553 | return filePath.split(sep).join(path.sep); 554 | } 555 | 556 | EleventyVue.forceForwardSlashOnFilePath = function(filePath) { 557 | return filePath.split(path.sep).join("/"); 558 | } 559 | 560 | module.exports = EleventyVue; 561 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

eleventy Logo

2 | 3 | # eleventy-plugin-vue for Vue 3 🕚⚡️🎈🐀 4 | 5 | Adds Vue Single File Component (SFC) support to Eleventy. Eleventy processes `.vue` SFC files as Eleventy templates and outputs zero-bundle size server rendered components. 6 | 7 | Read more about the goals of this plugin (and a full tutorial!) at [Eleventy and Vue, a match made to power Netlify.com](https://www.netlify.com/blog/2020/09/18/eleventy-and-vue-a-match-made-to-power-netlify.com/) 8 | 9 | ## Installation 10 | 11 | ```sh 12 | npm install @11ty/eleventy-plugin-vue 13 | ``` 14 | 15 | * `1.x` requires Eleventy `1.0.0` or newer 16 | * `0.2.x` encouraged to use Eleventy `0.11.1` or newer (for incremental Vue component builds) 17 | * `0.1.x` requires Eleventy `0.11.0` or newer 18 | 21 | 22 | ### Changelog 23 | 24 | * `1.0.0` ([Milestone](https://github.com/11ty/eleventy-plugin-vue/milestone/6?closed=1)) Works with Vue 3. Adds Windows support. 25 | * `0.6.x` ([Milestone](https://github.com/11ty/eleventy-plugin-vue/milestone/5?closed=1)) 26 | * `0.3.1` ([Milestone](https://github.com/11ty/eleventy-plugin-vue/milestone/4?closed=1)) 27 | * `0.3.0` ([Milestone](https://github.com/11ty/eleventy-plugin-vue/milestone/3?closed=1)) More consistent incremental builds. Performance improvements. 28 | * `0.2.1` ([Milestone](https://github.com/11ty/eleventy-plugin-vue/milestone/2?closed=1)) adds incremental builds for Eleventy Vue components to avoid unnecessary repeat work. Fixes bug with `permalink` strings returned from Vue Single File Component data. 29 | * `0.1.x` converted to use a Single File Components for everything (templates, components, etc), instead of `0.0.x`’s string templates with front matter. 30 | 31 | ## Features 32 | 33 | * Builds `*.vue`’s Single File Components, both in the input directory and in Eleventy’s includes directory. `.vue` files in the includes directory are available for import. Same as any Eleventy template syntax, includes do not write files to your output directory. 34 | * Works with Vue’s Single File Components, including with `scoped` CSS. 35 | * Data from SFC files feeds into the data cascade (similar to front matter) 36 | * All JavaScript Template Functions (see https://www.11ty.dev/docs/languages/javascript/#javascript-template-functions), Universal Filters, Universal Shortcodes, Universal Paired Shortcodes are available as Vue `methods` (global functions to use in templates and child components). 37 | * For example, you can use the [`url` Universal Filter](https://www.11ty.dev/docs/filters/url/) like `url("/my-url/")` in your Vue templates. 38 | * `page` Eleventy supplied data is also available globally in all components. 39 | 40 | ### Not Yet Available 41 | 42 | * Using `.vue` templates as Eleventy layouts is not yet supported. Subscribe to this issue at [#5](https://github.com/11ty/eleventy-plugin-vue/issues/5). 43 | * Does not yet embed any client-side JavaScript from inside single file components into the output for use on the client. 44 | * `lang` on `