├── .bs-config.js ├── .eleventy.js ├── .gitignore ├── README.md ├── lib ├── filters │ └── example.js ├── linters │ └── example.js ├── parcel │ ├── parcel-plugin-copy-unbundled │ │ ├── index.js │ │ └── package.json │ ├── parcel-plugin-eleventy-sync │ │ ├── index.js │ │ ├── lib │ │ │ ├── DynamicEntry.js │ │ │ └── Watcher.js │ │ └── package.json │ ├── parcel-plugin-nunjucks-precompile │ │ ├── index.js │ │ ├── lib │ │ │ └── NunjucksAsset.js │ │ └── package.json │ └── parcel-plugin-service-worker │ │ ├── index.js │ │ ├── lib │ │ └── ServiceWorker.js │ │ └── package.json ├── shortcodes │ └── example.js └── transforms │ └── example.js ├── package-lock.json ├── package.json └── src ├── assets ├── 404.njk ├── blog.njk ├── blog │ └── example-post.md ├── browserconfig.xml ├── css │ └── index.css ├── img │ ├── favicon.ico │ ├── icon.png │ ├── tile-wide.png │ └── tile.png ├── index.njk ├── js │ └── index.ts ├── site.webmanifest.njk └── sw.js ├── data └── site.json └── includes ├── extends └── html5boilerplate.njk ├── layouts ├── base.njk └── post.md └── partials ├── scripts.njk └── styles.njk /.bs-config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * The browser-sync configuration. 5 | * 6 | * For a full list of options, see: http://www.browsersync.io/docs/options/ 7 | */ 8 | module.exports = { 9 | serveStatic: ['./dist'], 10 | serveStaticOptions: { 11 | extensions: ['html'] 12 | }, 13 | https: false, 14 | open: false, 15 | watch: true, 16 | watchOptions: { 17 | ignoreInitial: true 18 | }, 19 | ignore: [], 20 | }; -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const glob = require('fast-glob'); 4 | const path = require('path'); 5 | 6 | /** 7 | * The @11ty/eleventy configuration. 8 | * 9 | * For a full list of options, see: https://www.11ty.io/docs/config/ 10 | */ 11 | module.exports = (eleventyConfig) => { 12 | const paths = { 13 | filters: path.join(process.cwd(), "src/filters/*.js"), 14 | shortcodes: path.join(process.cwd(), "src/shortcodes/*.js"), 15 | transforms: path.join(process.cwd(), "src/transforms/*.js") 16 | } 17 | const dirs = { 18 | input: "src/assets/", 19 | data: `../data/`, 20 | includes: `../includes/`, 21 | } 22 | const files = glob.sync(path.join(process.cwd(), dirs.input, "**/*")); 23 | const exts = files.map(file => path.extname(file).replace('.', '')); 24 | const filters = glob.sync(paths.filters); 25 | const shortcodes = glob.sync(paths.shortcodes); 26 | const transforms = glob.sync(paths.transforms); 27 | 28 | // Add all found filters 29 | filters.forEach(filter => eleventyConfig.addFilter(resolveNameFromPath(filter), filter)); 30 | 31 | // Add all found shortcodes 32 | shortcodes.forEach(shortcode => eleventyConfig.addShortcode(resolveNameFromPath(shortcode), shortcode)); 33 | 34 | // Add all found transforms 35 | transforms.forEach(transform => eleventyConfig.addTransform(resolveNameFromPath(transform), transform)); 36 | 37 | // Make all files pass through to cache 38 | eleventyConfig.setTemplateFormats(exts); 39 | 40 | return { 41 | // Set the path from the root of the deploy domain 42 | // i.e, example.com + "/" 43 | pathPrefix: "/", 44 | 45 | // Set the src and output directories 46 | dir: dirs, 47 | 48 | // Set the default template engine from `liquid` to `njk` 49 | htmlTemplateEngine: "njk", 50 | markdownTemplateEngine: "njk", 51 | dataTemplateEngine: "njk", 52 | 53 | // Set up eleventy to pass-through files to be compiled by Parcel 54 | passthroughFileCopy: true 55 | }; 56 | }; 57 | 58 | function resolveNameFromPath(path) { 59 | return path.basename(path); 60 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.tgz 3 | node_modules/ 4 | .tmp/ 5 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `parceleventy` 2 | 3 | > ⚠ **This project is not being actively maintained** 4 | > Parcel 1.0 and eleventy do not go well together due to how parcel watches files, causing sync issues between the 11ty development cycle and parcel dev server 5 | 6 | A basic @11ty/eleventy starter using parcel-bundler for asset processing, minification, and bundling. 7 | 8 | Makes it easy to write HTML, JS, and CSS in your flavour of choosing, and get a production-ready website bundle. 9 | 10 | ## Why? 11 | 12 | This starter allows anyone to get started with modern web languages simply by installing it and writing code. 13 | 14 | No configuration is necessary, with sensible defaults provided by both Eleventy and Parcel Bundler, but can be provided as needed. 15 | 16 | This starter allows you to quickly develop a modern website using JAMStack technology, and can be extended to handle many use-cases. 17 | 18 | ## Installation 19 | 20 | First, you must have Git, Nodejs and NPM installed. 21 | 22 | Then, clone the repository to your local machine: 23 | ``` 24 | git clone https://github.com/chrisdmacrae/eleventy-starter-parcel.git 25 | ``` 26 | 27 | Then install the project dependencies: 28 | 29 | ``` 30 | cd eleventy-starter-parcel 31 | npm install 32 | ``` 33 | 34 | ## Development 35 | 36 | To start the development environment run the `start` command: 37 | 38 | ``` 39 | npm start 40 | ``` 41 | 42 | This will start eleventy in watch mode, Parcel's HMR server, and a BrowserSync proxy server to give you a seamless development experience, and allowing you to do multi-device testing. 43 | 44 | ## Building 45 | 46 | To generate a production build with all assets minified, optimized, and hashed for cachebusting, run: 47 | 48 | ``` 49 | npm run build 50 | ``` 51 | 52 | The production build will be available in `dist/` for deployment. 53 | 54 | ## How it works 55 | 56 | This starter combines Eleventy and Parcel bundler to create a zero-config build of a modern website/web application, using a variety of modern programming practices. 57 | 58 | It does this by post-processing, pre-processing, and bundling a group of _**assets**_, or files, in 4 steps: 59 | 60 | ### 1. Pre-processing with Eleventy 61 | 62 | Eleventy is used as a text-preprocessor, allowing you to use Nunjucks, or a variety of other template formats, to generate text-based documents. 63 | 64 | In most cases, this is HTML. 65 | 66 | Any files found in `src/assets` whose extensions match [supported template formats](https://www.11ty.io/docs/languages/) will be post-processed, and then copied to `.tmp/11ty`. 67 | 68 | _Any file extensions not supported by Eleventy's templates will be copied over to `.tmp/11ty` as-is._ 69 | 70 | ### 2. Parsing with Parcel Bundler 71 | 72 | Then, all output HTML assets in `.tmp/11ty` are parsed by Parcel Bundler. 73 | 74 | From this, a list of all dependencies referenced in post-processed HTML assets are assembled, and prepared for processing. 75 | 76 | _Any assets in `.tmp/11ty` that are not referenced in your HTML output are copied over to the `dist/` folder as-is._ 77 | 78 | ### 3. Pre-processing with Parcel Bundler 79 | 80 | Parcel is then used to pre-process supported assets, including CSS and JS. 81 | 82 | - [Sass is pre-processed into CSS](https://parceljs.org/scss.html) 83 | - [Less is pre-processed into CSS](https://parceljs.org/less.html) 84 | - [Stylus is pre-processed into CSS](https://parceljs.org/stylus.html) 85 | - Inline style tags of type `text/sass`, `text/scss`, `text/less`, `text/stylus` are transformed into CSS 86 | - [Typescript is pre-processed into JS](https://parceljs.org/typeScript.html) 87 | - [Coffeescript is pre-processed into JS](https://parceljs.org/coffeeScript.html) 88 | - [ReasonML is pre-procesed into JS](https://parceljs.org/reasonML.html) 89 | 90 | ### 4. Post-processing with Parcel Bundler 91 | 92 | Parcel is then used on all of the pre-processed assets to do final post-processing: 93 | 94 | - [HTML is post-processed with PostHTML](https://parceljs.org/html.html) 95 | - [CSS is post-processed with PostCSS](https://parceljs.org/css.html) 96 | - [JS is post-processed with Babel](https://parceljs.org/javascript.html) 97 | - Inline script tags of the type type `application/javascript` and `text/javascript` are post-processed with Babel 98 | - [Images are post-processed with Imagemin](#) 99 | 100 | ## Advanced Features 101 | 102 | This setup allows you to do a lot of cool things, without any configuration. This highlights some of them until I write better documentation. 103 | 104 | ### No 404-builds 105 | 106 | Parcel does not _**allow**_ 404s, let alone like them. If Parcel ecounters a relative file path in your assets that don't exist, it will throw an error and exit a build with a non-zero exit code. 107 | 108 | This means it's a _lot_ harder to deploy a broken site. Cool, eh? 109 | 110 | ### Asset resolution 111 | 112 | Parcel modifies the module resolution for JavaScript files, as well as the URL resolution algorithm for HTML and CSS. 113 | 114 | **For JavaScript:** 115 | - Support for requiring/importing [all supported asset types](https://parceljs.org/assets.html) is added. 116 | - Relative paths beginning with `/` are resolved from `src/assets/`. 117 | - Relative paths beginning with `~` will be resolve from the root of a matching package, allowing you to require files from outside your assets folder. 118 | - Glob support is added, allowing you to do things like: `require('./**/*.js')`; 119 | - [Aliases](https://parceljs.org/module_resolution.html#aliases) can be created, allowing you to: 120 | - Swap out one module for another, e.g. `react` => `preact` 121 | - Create a shortcut to a local module, e.g. `./src/assets/` => `assets/`. 122 | 123 | **For HTML and CSS:** 124 | - Relative paths beginning with `./` are resolved from the current file's directory. 125 | - Relative paths beginning with `../` are resolved up one directory from the current file, and `../` may be repeated. 126 | - Relative paths beginning with `/` are resolved from `src/assets/`. 127 | - Relative paths beginning with `~` will be resolve from the root of a matching package, allowing you to require files from outside your assets folder. 128 | 129 | ### Isomorphic Nunjucks 130 | 131 | Any Nunjucks templates found in `src/assets/` or `src/includes/` can be `require()`'d or `import`'d into your JavaScript assets, and be used to render HTML client-side. 132 | 133 | E.g, if you wanted to access the `html5boilerplate` extend in your JavaScript: 134 | 135 | In `src/assets/js/index.js`: 136 | 137 | ``` 138 | const nunjucks = require('nunjucks/browser/nunjucks-slim'); 139 | const html5boilerplate = require('includes/extends/html5boilerplate.njk'); 140 | const html = nunjucks.render('/src/includes/extends/html5boilerplate.njk', { 141 | title: "My new title" 142 | }); 143 | 144 | console.log(html); 145 | ``` 146 | 147 | > ## Want to create an Nunjucks template for client-use only? 148 | > 149 | > That's easy! Simply set `permalink` to `false` in the template's front matter, 150 | > and it will not be output as a file, but will stil be available to your JS! 151 | 152 | ### Isomorphic Data Files 153 | 154 | Supported data file formats can be `require()`'d or `import`'d in your JavaScript, allowing you to access the data as JavaScript types. 155 | 156 | Currently you can import: 157 | 158 | - [YAML](https://parceljs.org/yaml.html) 159 | - [TOML](https://parceljs.org/toml.html) 160 | - [JSON](https://parceljs.org/json.html) 161 | - JS 162 | 163 | E.g, if you wanted to use the data in `src/data/site.yml`: 164 | 165 | ``` 166 | const siteData = require('data/site.yml`); 167 | 168 | console.log(siteData); 169 | ``` 170 | 171 | ### Custom output formats 172 | 173 | Eleventy allows you to output _any_ type of text-based file. You can use a Nunjucks or other supported template format to generate HTML, JSON, YAML, TOML, CSS, or whatever other text-based files you'd like to generate. 174 | 175 | To do so, you simply change the `permalink` setting in the frontmatter of the file. 176 | 177 | E.g, to have `src/assets/index.njk` output to valid JSON to `src/assets/index.json` instead of outputting HTML to `src/assets/index.html`, simply do: 178 | 179 | ``` 180 | --- 181 | permalink: index.json 182 | data: [ 183 | "value1", 184 | "value2", 185 | "value3" 186 | ] 187 | --- 188 | {{ data | dump }} 189 | ``` 190 | 191 | ### Generating multiple files from a single template 192 | 193 | Eleventy also allows you to generate multiple output files from a single template, using it's [pagination feature](https://www.11ty.io/docs/pagination/). 194 | 195 | The pagination feature works by looping an array of data over the same template multiple times, in order to generate different pages or assets. 196 | 197 | To do so, add a `pagination` object to your template's front matter, with the properties: 198 | 199 | - `data`: is the key for any variable accessible to the template (e.g, from frontmatter, data files, or collections) 200 | - `size`: is the number of items to pass to the template in each loop 201 | - `alias`: the key you wish to access the pagination data for each set of the loop 202 | 203 | E.g, in `src/assets/blog.njk` 204 | 205 | ``` 206 | --- 207 | pagination: 208 | data: collections.posts 209 | size: 6 210 | alias: posts 211 | --- 212 | {{ posts | dump }} 213 | ``` 214 | 215 | #### Generating an archive 216 | 217 | Pagination can be used to generate multiple pages using the same template, but with different data, allowing you to get similar functionality to server-side rendered pages. 218 | 219 | The `permalink` key is processed using the template's parser, allowing you to use any data available to the template to change the permalink of the page during pagination, allowing you to generate many unique pages. 220 | 221 | E.g, in `src/assets/blog.njk`: 222 | 223 | ``` 224 | --- 225 | pagination: 226 | data: collections.posts 227 | size: 6 228 | alias: posts 229 | permalink: blog/{{ pagination.pageNumber + 1 }}/index.html 230 | --- 231 | {{ posts | dump }} 232 | ``` 233 | 234 | #### Generating multiple file types from a single template 235 | 236 | Pagination can also be used to output multiple file formats for a given page, such as outputting JSON representations for all pages. 237 | 238 | To output a different file type for each iteration of the loop, you can do something like: 239 | 240 | In `src/assets/index.njk`: 241 | ``` 242 | --- 243 | outputTypes: 244 | - html 245 | - json 246 | pagination: 247 | data: outputTypes 248 | size: 1 249 | alias: ext 250 | permalink: {{ permalink | replace('html', ext) }} 251 | --- 252 | {% if ext === "html %} 253 | {# Output HTML here #} 254 | {% elif ext === "json" %} 255 | {# Output JSON here #} 256 | {% endif %} 257 | ``` 258 | 259 | ### Service worker 260 | 261 | A service worker as generated at the end of every build using Google's [Workbox](https://developers.google.com/web/tools/workbox/). By default it precaches all HTML, CSS, and image assets in your `dist/` directory, and makes them available when offline. 262 | 263 | The generation supports two strategies for generating a service worker: 264 | 265 | | Key | Name | Description | 266 | | `0` | Generate | Automatically generates a fully-functioning service worker with precaching at `dist/sw.js` | 267 | | `1` | Inject | Automatically inject a service worker configuration into an existing service worker file at `src/assets/sw.js` | 268 | 269 | The strategy can be configured by: 270 | 271 | - Creating a `package` key in `package.json` with the following... 272 | - Creating a `.workboxrc` with a JSON configuration with the following.. 273 | - Creating a `.workbox.js` that exports a valid configuration object with the following... 274 | 275 | ``` 276 | { 277 | "strategy": 0 278 | "generate": {} 279 | "inject": {} 280 | } 281 | ``` 282 | 283 | ### Generate 284 | 285 | Generating gives you the most basic service worker, allowing your site/app to be available offline when network connection is not available. 286 | 287 | This method always overwrites the content of `src/assets/sw.js`. 288 | 289 | The options available to generation can be [found here](https://developers.google.com/web/tools/workbox/modules/workbox-build#generatesw_mode). 290 | 291 | ### Inject 292 | 293 | Injection allows you to fully customize your service worker, and enable custom functionality like push notifications and preloading. 294 | 295 | This method uses the asset found at `src/assets/sw.js`, which is already pre-configured for injection. 296 | 297 | APIs for configuring and adding features to your custom `sw.js` can be [found here](https://developers.google.com/web/tools/workbox/modules/workbox-sw). 298 | 299 | The options available to inject can be [found here](https://developers.google.com/web/tools/workbox/modules/workbox-build#injectmanifest_mode). 300 | 301 | ### Code splitting 302 | 303 | Parcel enables you to do code-splitting easily by [using dynamic imports](https://parceljs.org/code_splitting.html). 304 | 305 | Code splitting allows you to break up your Javascript into small bundles, or files, that are loaded on-demand as they are needed in the browser. 306 | 307 | This dramatically reduces the amount of JavaScript a user has to download, by ensuring they only download what the browser needs to use. 308 | 309 | ### Linters 310 | 311 | You can add [linters](https://www.11ty.io/docs/config/#linters) to Eleventy's template pre-processing process to add checks to your build process. 312 | 313 | To do so, add Javascript files to `lib/linters`: 314 | 315 | ``` 316 | module.exports = (content, inputPath, outputPath) => { 317 | // Review content and log console output if necessary... 318 | // console.warn(`warning message`); 319 | // 320 | // Or throw an error to stop builds! 321 | // throw new Error(`error message`); 322 | } 323 | ``` 324 | 325 | E.g, add a natural language linter to markdown files, to ensure off-brand langauge isn't used! 326 | 327 | ``` 328 | module.exports = (content, inputPath, outputPath) => { 329 | const isMarkdownFile = inputPath.endsWith(".md"); 330 | let words = ["the","seven","words","you","can't","say","on","television"] 331 | if (inputPath.endsWith(".md")) { 332 | for (let word of words) { 333 | let regexp = new RegExp("\\b(" + word + ")\\b", "gi"); 334 | if (content.match(regexp)) { 335 | console.warn( 336 | `Inclusive Language Linter (${inputPath}) Found: ${word}` 337 | ); 338 | } 339 | } 340 | } 341 | }; 342 | ``` 343 | 344 | ### Transforms 345 | 346 | Custom [transforms](https://www.11ty.io/docs/config/#transforms) can be applied at the end of Eleventy's pre-processing step, before the templates are processed by Parcel. 347 | 348 | To do so, add Javascript files to `lib/transforms`: 349 | 350 | ``` 351 | module.exports = (content, outputPath) => { 352 | // Do something to content... 353 | 354 | return content; 355 | } 356 | ``` 357 | 358 | E.g, pretty print HTML output before sending it to Parcel: 359 | 360 | ``` 361 | const pretty = require('pretty'); 362 | 363 | module.exports = (content, outputPath) => { 364 | const isHTML = outputPath.endsWith('.html'); 365 | 366 | if (isHTML) { 367 | return pretty(content); 368 | } 369 | 370 | return content; 371 | } 372 | ``` 373 | 374 | ## Filters & Shortcodes 375 | 376 | Custom filters and shortcodes can be added for use in your templates: 377 | 378 | - **Filters** allow you to modify a value, such as changing a string or converting an array to a delimited list 379 | - **Shortcodes** return content (a JavaScript string or template literal) that is injected into the template. They can also take paramaters to customize their output. 380 | 381 | ### Adding Filters 382 | 383 | To add a filter, add a JS file to `lib/filters` with the desired name. I.e, `lib/filters/example.js` becomes `example` in your templates: 384 | 385 | ``` 386 | module.exports = (value) => { 387 | // Do something with value 388 | 389 | return value; 390 | } 391 | ``` 392 | 393 | ``` 394 | {# Use as a function #} 395 | {{ example exampleValue }} 396 | 397 | {# Or use as a pipe #} 398 | {{ exampleValue | example }} 399 | ``` 400 | 401 | ### Adding shortcodes 402 | 403 | To add a shortcode, add a JS file to `lib/shortcodes` with the desired name. I.e, `lib/shortcodes/example.js` becomes `{% example %}` in your templates. 404 | 405 | ``` 406 | module.exports = (slot, value) => { 407 | // Do something with paramaters, and return output 408 | 409 | return value; 410 | } 411 | ``` 412 | 413 | ``` 414 | {% example "exampleForValue1" %} 415 | {# Outputs: "exampleForValue1" #} 416 | ``` 417 | 418 | Shortcodes can take as many paramaters as you like, just add additional arguments to your function: 419 | 420 | ``` 421 | module.exports = (slot, value1, value2) => { 422 | // Do something with values, and return output 423 | 424 | return value1 + " " + value2; 425 | } 426 | ``` 427 | 428 | ``` 429 | {% example "Hello", "World!" %} 430 | {# Outputs: "Hello World!" %} 431 | ``` 432 | 433 | Lastly, shortcodes can tag pairs, allowing you to slot variable content into the shortcode, accessible through the first paramater of the function: 434 | 435 | ``` 436 | module.exports = (slot, value1, value2) { 437 | return ` 438 |
{{ slot }}
440 | ` 441 | } 442 | ``` 443 | 444 | ``` 445 | {% example "Hello", "world!" %} 446 | I'm alive! 447 | {% endexample %} 448 | {# Outputs: 449 |I'm alive!
451 | #} 452 | ``` 453 | 454 | ## Common pitfalls 455 | 456 | ## Getting 404s for files in `src/assets/` 457 | 458 | #### Problems with HTML 459 | 460 | Ensure when referencing assets in your HTML, you're using the permalink of the file, not the original extension! 461 | 462 | E.g, `src/assets/index.njk` becomes `/index.html`. 463 | 464 | #### Changes to assets aren't showing up on my dev server? 465 | 466 | This is likely caused by either: 467 | 468 | - If your having issues with pre-processed CSS or JS assets like Sass or Typescript, your assets are likely not being referenced in your HTML documents. Assets that require any pre or post-processing must be referenced in an HTML document, or a supported asset format that _is_ referenced in an HTML document. 469 | - The service worker is caching your assets. You can: 470 | - [Disable the service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Developer_tools) in your browser's developer tools 471 | - Force a hard refresh of the page (this is usually done by pressing `ctrl` + `shift` + `R`). 472 | -------------------------------------------------------------------------------- /lib/filters/example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = (value) => { 4 | // Do something with value 5 | 6 | return value; 7 | } -------------------------------------------------------------------------------- /lib/linters/example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = (content, inputPath, outputPath) => { 4 | // Review content and log console output if necessary... 5 | // console.warn(`warning message`); 6 | // 7 | // Or throw an error to stop builds! 8 | // throw new Error(`error message`); 9 | } -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-copy-unbundled/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const glob = require('globby'); 4 | const path = require('path'); 5 | const fs = require('fs-extra'); 6 | 7 | module.exports = bundler => { 8 | const {rootDir, outDir } = bundler.options; 9 | let files = glob.sync(path.join(rootDir, "**/*")); 10 | let assets = []; 11 | let assetExts = []; 12 | let toCopy = []; 13 | 14 | Object.keys(bundler.parser.extensions).forEach(ext => assetExts = addExt(ext, assetExts)); 15 | bundler.packagers.packagers.forEach((value, ext) => assetExts = addExt(ext, assetExts)); 16 | 17 | bundler.on('bundled', (bundle) => { 18 | assets = resolveAssetsFromBundle(bundle, assets); 19 | toCopy = resolveFilesToCopy(files, assets, assetExts); 20 | 21 | for (let file of toCopy) { 22 | const outputPath = path.resolve(path.normalize(outDir), path.normalize(file).replace(path.normalize(rootDir), "./")); 23 | const data = fs.readFileSync(file); 24 | 25 | fs.outputFileSync(outputPath, data); 26 | } 27 | }); 28 | } 29 | 30 | function resolveAssetsFromBundle(bundle, assets) { 31 | let bundles = [bundle]; 32 | 33 | if (bundle.childBundles && bundle.childBundles.size > 0) { 34 | bundles = resolveChildBundlesFromBundle(bundle, bundles); 35 | } 36 | 37 | for (let bundle of bundles) { 38 | if (bundle.assets && bundle.assets.size > 0) { 39 | for (let [key, asset] of bundle.assets.entries()) { 40 | assets = resolveAssetsFromAsset(asset, assets); 41 | } 42 | } 43 | } 44 | 45 | return assets; 46 | } 47 | 48 | function resolveChildBundlesFromBundle(bundle, bundles) { 49 | if (bundle.childBundles && bundle.childBundles.size > 0) { 50 | for (let [key, childBundle] of bundle.childBundles.entries()) { 51 | bundles.push(childBundle); 52 | 53 | if (childBundle.childBundles) { 54 | bundles = resolveChildBundlesFromBundle(childBundle, bundles); 55 | } 56 | } 57 | } 58 | 59 | return bundles; 60 | } 61 | 62 | function resolveAssetsFromAsset(asset, assets) { 63 | const name = asset.name; 64 | const exists = assets.indexOf(name) > -1; 65 | 66 | if (!exists) { 67 | assets.push(name); 68 | } 69 | 70 | if (asset.depAssets.size > 0) { 71 | for (let [key, depAsset] of asset.depAssets.entries()) { 72 | assets = resolveAssetsFromAsset(depAsset, assets); 73 | } 74 | } 75 | 76 | return assets; 77 | } 78 | 79 | function resolveFilesToCopy(files, assets, exts) { 80 | files = files 81 | .filter(file => assets.indexOf(file) === -1) 82 | .filter(file => { 83 | const ext = path.extname(file); 84 | 85 | return exts.indexOf(ext) === -1; 86 | }); 87 | 88 | return files; 89 | } 90 | 91 | function addExt(ext, exts) { 92 | const trueExt = ext.startsWith('.') ? ext : `.${ext}`; 93 | const exists = exts.indexOf(trueExt) > -1; 94 | 95 | return exists ? exts : exts.concat(trueExt); 96 | } -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-copy-unbundled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-copy-unbundled", 3 | "version": "1.0.0", 4 | "description": "Copies any files that are not part of the bundle(s) to the output directory", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "@chrisdmacrae", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-eleventy-sync/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const EleventyWatcher = require('./lib/Watcher'); 4 | const DynamicEntry = require('./lib/DynamicEntry'); 5 | 6 | module.exports = bundler => { 7 | const dynamicEntry = "_11ty.html"; 8 | 9 | new DynamicEntry(dynamicEntry, bundler); 10 | 11 | if (!bundler.options.production) { 12 | new EleventyWatcher(dynamicEntry, bundler); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-eleventy-sync/lib/DynamicEntry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const glob = require('globby'); 4 | const path = require('path'); 5 | const fs = require('fs-extra'); 6 | 7 | class DynamicEntry { 8 | constructor(entryName, bundler) { 9 | this.options = bundler.options; 10 | this.name = entryName; 11 | this.path = path.join(this.options.rootDir, this.name); 12 | 13 | bundler.entryFiles.push(this.path); 14 | bundler.options.entryFiles.push(this.path); 15 | this._write(this.path); 16 | 17 | bundler.on('buildStart', () => this.handleBuildStart()); 18 | bundler.on('buildEnd', () => this.handleBuildEnd()); 19 | } 20 | 21 | handleBuildStart() { 22 | this._write(this.path); 23 | } 24 | 25 | handleBuildEnd() { 26 | const exists = fs.existsSync(this.destination); 27 | 28 | if (this.options.production && exists) { 29 | fs.unlinkSync(this.destination); 30 | 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | _write(p) { 38 | let output = ""; 39 | let existing = ""; 40 | const entryFilesPattern = path.join(this.options.rootDir, "**/*.html"); 41 | const entryFiles = glob.sync(entryFilesPattern); 42 | const exists = fs.existsSync(p); 43 | 44 | if (exists) { 45 | existing = fs.readFileSync(p, {encoding: "utf-8"}); 46 | } 47 | 48 | for (var file of entryFiles) { 49 | if (!file.endsWith(this.name)) { 50 | const rootPath = path.normalize(file).replace(path.normalize(this.options.rootDir), ""); 51 | const anchor = `\n`; 52 | output += anchor; 53 | } 54 | } 55 | 56 | if (existing.trim() !== output.trim()) { 57 | fs.writeFileSync(p, output.trim()); 58 | } 59 | } 60 | } 61 | 62 | module.exports = DynamicEntry; -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-eleventy-sync/lib/Watcher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Asset = require('parcel-bundler/lib/Asset'); 4 | const fs = require('fs-extra'); 5 | const glob = require('fast-glob'); 6 | const path = require('path'); 7 | const Watcher = require('@parcel/watcher'); 8 | const logger = require('@parcel/logger'); 9 | 10 | class EleventyWatcher { 11 | constructor(entryName, bundler) { 12 | this.options = bundler.options; 13 | this.name = entryName; 14 | this.config; 15 | this.configFiles = [".eleventy.js"]; 16 | this.packageKey = "eleventy"; 17 | this.unlinked = new Set(); 18 | this.tracked = new Set(); 19 | this.watcher = new Watcher(); 20 | 21 | // todo: get eleventy output dynamically 22 | // this.loadConfig(); 23 | this.options.eleventyDist = path.resolve(this.options.rootDir, "../11ty/"); 24 | 25 | this.watcher.add(this.options.eleventyDist); 26 | this.watcher.on('add', path => this.handleAdd(path)); 27 | this.watcher.on('change', path => this.handleChange(path)); 28 | this.watcher.on('unlink', path => this.handleUnlink(path)); 29 | bundler.on('buildStart', () => this.handleBuildStart()); 30 | } 31 | 32 | async handleAdd(path) { 33 | try { 34 | const isUnlinked = this.unlinked.has(path); 35 | 36 | if (isUnlinked) { 37 | await this._add(path); 38 | this.unlinked.delete(path); 39 | } else { 40 | await this._add(path); 41 | await this._forceUpdate() 42 | } 43 | } catch (error) { 44 | logger.error(error); 45 | } 46 | } 47 | 48 | async handleChange(path) { 49 | try { 50 | const isUnlinked = this.unlinked.has(path); 51 | 52 | if (isUnlinked) { 53 | this.unlinked.delete(path); 54 | } 55 | 56 | await this._add(path); 57 | } catch (error) { 58 | logger.error(error); 59 | } 60 | } 61 | 62 | async handleUnlink(path) { 63 | try { 64 | const isUnlinked = this.unlinked.has(path); 65 | 66 | if (!isUnlinked) { 67 | this.unlinked.add(path); 68 | } 69 | } catch (error) { 70 | logger.error(error); 71 | } 72 | } 73 | 74 | handleBuildStart() { 75 | try { 76 | const sourcePattern = path.join(this.options.eleventyDist, "**/*"); 77 | const sourceFiles = glob.sync(sourcePattern); 78 | 79 | for (let file of this.unlinked) { 80 | const shouldDelete = sourceFiles.indexOf(file) === -1; 81 | const exists = fs.existsSync(file); 82 | 83 | if (shouldDelete && exists) { 84 | this._delete(file); 85 | } 86 | } 87 | } catch (error) { 88 | logger.error(error); 89 | } 90 | } 91 | 92 | async loadConfig() { 93 | try { 94 | const asset = new Asset(path.resolve(this.options.rootDir, this.name), this.options); 95 | const config = await asset.getConfig(this.configFiles, { 96 | packageKey: this.packageKey 97 | }); 98 | 99 | if (config) { 100 | return typeof config === "function" ? config() : config; 101 | } 102 | 103 | return {}; 104 | } catch (error) { 105 | logger.error(error); 106 | } 107 | } 108 | 109 | async _add(p) { 110 | try { 111 | // todo: replace with config-based 11ty path check 112 | const relPath = path.normalize(p).slice(p.indexOf('11ty') + 4); 113 | const outputPath = path.join(this.options.rootDir, relPath); 114 | const exists = await this._ensureFileExists(p, 200); 115 | const isDirectory = fs.lstatSync(p).isDirectory(); 116 | 117 | if (exists && !isDirectory) { 118 | let oldData = ""; 119 | const oldExists = fs.existsSync(outputPath); 120 | const newData = fs.readFileSync(p); 121 | 122 | if (oldExists) { 123 | oldData = fs.readFileSync(outputPath); 124 | } 125 | 126 | if (!oldExists || oldExists && oldData != newData) { 127 | fs.outputFileSync(outputPath, newData); 128 | this.tracked.add(p); 129 | } 130 | } 131 | } catch (error) { 132 | throw error; 133 | } 134 | } 135 | 136 | _delete(p) { 137 | const exists = fs.existsSync(p); 138 | const isTracked = this.tracked.has(p); 139 | 140 | if (exists) { 141 | fs.unlinkSync(p); 142 | 143 | if (isTracked) { 144 | this.tracked.delete(p); 145 | } 146 | } 147 | } 148 | 149 | async _forceUpdate() { 150 | try { 151 | const eleventyManifestPath = path.resolve(this.options.rootDir, this.name); 152 | const exists = await this._ensureFileExists(eleventyManifestPath); 153 | 154 | if (exists) { 155 | fs.outputFileSync(eleventyManifestPath, ""); 156 | } 157 | } catch (error) { 158 | throw error; 159 | } 160 | } 161 | 162 | _ensureFileExists(path, timeout) { 163 | return new Promise((resolve, reject) => { 164 | let tries = 0; 165 | const timer = setInterval(() => { 166 | const exists = fs.existsSync(path); 167 | 168 | if (exists) { 169 | clearInterval(timer); 170 | resolve(true); 171 | } 172 | 173 | if (tries >= 5) { 174 | clearInterval(timer); 175 | resolve(true); 176 | } 177 | 178 | tries++; 179 | }, timeout); 180 | }); 181 | } 182 | } 183 | 184 | module.exports = EleventyWatcher; -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-eleventy-sync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-eleventy-sync", 3 | "version": "1.0.0", 4 | "description": "A plugin that writes out a persistent HTML file between rebuilds to track HTML entrypoints", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-nunjucks-precompile/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = bundler => { 4 | bundler.addAssetType('.njk', require.resolve('./lib/NunjucksAsset')); 5 | } -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-nunjucks-precompile/lib/NunjucksAsset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const JSAsset = require('parcel-bundler/lib/assets/JSAsset'); 4 | const nunjucks = require('nunjucks'); 5 | const path = require('path'); 6 | 7 | class NunjucksAsset extends JSAsset { 8 | constructor(name, opts) { 9 | super(name, opts); 10 | 11 | this.configFiles = ['.nunjucksrc', '.nunjucks.js']; 12 | this.packageName = "nunjucks"; 13 | } 14 | 15 | async pretransform() { 16 | const configOpts = { 17 | packageName: this.packageName 18 | } 19 | const config = await this.getConfig(this.configFiles, configOpts) || {}; 20 | const nunjucksOpts = Object.assign({}, config, { 21 | name: this.name.replace(path.normalize(process.cwd()), "") 22 | }); 23 | const precompiled = nunjucks.precompileString(this.contents, nunjucksOpts); 24 | 25 | if (nunjucksOpts.asFunction) { 26 | this.contents = 27 | ` 28 | const nunjucks = require('nunjucks'); 29 | module.exports = ${precompiled} 30 | ` 31 | } else { 32 | this.contents = 33 | ` 34 | module.exports = ${precompiled} 35 | ` 36 | } 37 | 38 | return await super.pretransform(); 39 | } 40 | } 41 | 42 | module.exports = NunjucksAsset -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-nunjucks-precompile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-nunjucks-precompile", 3 | "version": "1.0.0", 4 | "description": "A Parcel plugin to precompile Nunjucks assets referenced in JS assets", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "@chrisdmacrae", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-service-worker/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ServiceWorker = require('./lib/ServiceWorker'); 4 | 5 | module.exports = bundler => { 6 | new ServiceWorker(bundler); 7 | } -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-service-worker/lib/ServiceWorker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Asset = require('parcel-bundler/lib/Asset'); 4 | const fs = require('fs-extra'); 5 | const glob = require('fast-glob'); 6 | const logger = require('@parcel/logger'); 7 | const path = require('path'); 8 | const workbox = require('workbox-build'); 9 | const STRATEGIES = [ 10 | "GENERATE", 11 | "INJECT" 12 | ]; 13 | 14 | class ParcelServiceWorker { 15 | constructor(bundler) { 16 | this.options = bundler.options; 17 | this.name = "sw.js"; 18 | this.configFiles = ['.workboxrc', '.workbox-config.js']; 19 | this.packageKey = "workbox"; 20 | this.strategy = STRATEGIES[0]; 21 | this.importScripts = []; 22 | this.hasCopiedLibraries = false; 23 | 24 | bundler.on('buildEnd', () => this.handleBuildEnd()); 25 | } 26 | 27 | async handleBuildEnd() { 28 | try { 29 | const destination = path.resolve(this.options.outDir, this.name); 30 | const config = await this.loadConfig(); 31 | 32 | if (config && config.strategy) { 33 | this.strategy = STRATEGIES[config.strategy]; 34 | } 35 | 36 | if (!this.hasCopiedLibraries) { 37 | await this.copyLibraries(); 38 | } 39 | 40 | if (this.strategy == STRATEGIES[0]) { 41 | await this.generateSW(destination, config.generate); 42 | } else if (this.strategy == STRATEGIES[1]) { 43 | await this.injectSW(destination, config.inject); 44 | } else { 45 | throw new Error('Invalid strategy'); 46 | } 47 | 48 | } catch (error) { 49 | logger.error(error); 50 | } 51 | } 52 | 53 | async copyLibraries() { 54 | try { 55 | const libraryPath = ".workbox"; 56 | const destination = path.resolve(this.options.outDir, libraryPath); 57 | 58 | await workbox.copyWorkboxLibraries(destination); 59 | 60 | const vendorImports = glob.sync(path.join(destination, "**/workbox-sw.js"), { 61 | dot: true 62 | }).map(p => path.normalize(p).replace(path.normalize(this.options.outDir), "")); 63 | 64 | this.importScripts = this.importScripts.concat(vendorImports); 65 | this.hasCopiedLibraries = true; 66 | } catch (error) { 67 | throw error; 68 | } 69 | } 70 | 71 | async generateSW(destination, config = {}) { 72 | try { 73 | const defaultConfig = { 74 | globDirectory: this.options.outDir, 75 | globPatterns: [ 76 | '**/*.{js,css,html,png,jpg,jpeg,gif,tiff}' 77 | ], 78 | importWorkboxFrom: 'disabled', 79 | importScripts: [], 80 | swDest: destination 81 | } 82 | const cfg = Object.assign(defaultConfig, config); 83 | 84 | cfg.importScripts = cfg.importScripts.concat(this.importScripts); 85 | 86 | logger.progress('Creating service worker...'); 87 | 88 | await workbox.generateSW(cfg); 89 | 90 | logger.log('Service worker generated at: ' + cfg.swDest); 91 | } catch (error) { 92 | throw error; 93 | } 94 | } 95 | 96 | async injectSW(destination, config = {}) { 97 | try { 98 | const defaultConfig = { 99 | globDirectory: this.options.outDir, 100 | globPatterns: [ 101 | '**/*.{js,css,html,png,jpg,jpeg,gif,tiff}' 102 | ], 103 | swSrc: destination + ".tmp", 104 | swDest: destination 105 | } 106 | const cfg = Object.assign(defaultConfig, config); 107 | 108 | logger.progress('Injecting service worker manifest...'); 109 | 110 | fs.copySync(cfg.swDest, cfg.swDest + ".tmp"); 111 | 112 | let { count, size } = await workbox.injectManifest(cfg); 113 | fs.unlinkSync(cfg.swDest + ".tmp"); 114 | 115 | logger.log('Service worker manifest injected at: ' + cfg.swDest); 116 | logger.log(`Will precache ${count} files, totaling ${size} bytes.`); 117 | } catch (error) { 118 | throw error; 119 | } 120 | } 121 | 122 | async loadConfig() { 123 | try { 124 | const asset = new Asset(path.resolve(this.options.rootDir, this.name), this.options); 125 | const config = await asset.getConfig(this.configFiles, { 126 | packageKey: this.packageKey 127 | }); 128 | 129 | if (config) { 130 | return typeof config === "function" ? config() : config; 131 | } 132 | 133 | return {}; 134 | } catch (error) { 135 | logger.error(error); 136 | } 137 | } 138 | } 139 | 140 | module.exports = ParcelServiceWorker; 141 | -------------------------------------------------------------------------------- /lib/parcel/parcel-plugin-service-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-service-worker", 3 | "version": "1.0.0", 4 | "description": "Parcel plugin that generates service worker using workbox", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "@chrisdmacrae", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /lib/shortcodes/example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = (slot, value) => { 4 | // Do something with value 5 | // Slot is the contents between the shortcode tags when using tag pairs 6 | 7 | if (slot) { 8 | return value + slot; 9 | } 10 | 11 | return value; 12 | } 13 | -------------------------------------------------------------------------------- /lib/transforms/example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = (content, outputPath) => { 4 | // Do something to content... 5 | 6 | return content; 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-starter-parcel", 3 | "version": "1.0.0", 4 | "description": "An @11ty/eleventy starter using parcel-bundler for production bundling", 5 | "author": "chrisdmacrae", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": { 9 | "prestart": "run-s clean 11ty:build", 10 | "start": "run-p develop:*", 11 | "prebuild": "run-p clean", 12 | "build": "run-s build:*", 13 | "build:generate": "run-s 11ty:build parcel:build", 14 | "develop:generate": "run-p 11ty:watch parcel:watch", 15 | "develop:server": "browser-sync start --config .bs-config.js --proxy localhost:1234", 16 | "clean": "run-p clean:*", 17 | "clean:cache": "rimraf .tmp", 18 | "clean:dist": "rimraf dist", 19 | "11ty:build": "eleventy --quiet --output .tmp/parcel", 20 | "11ty:watch": "eleventy --quiet --watch --output .tmp/11ty", 21 | "parcel:build": "parcel build .tmp/parcel/*.html --out-dir dist --cache-dir .tmp/.cache", 22 | "parcel:watch": "parcel serve .tmp/parcel/*.html --out-dir dist --cache-dir .tmp/.cache", 23 | "test": "echo \"Error: no test specified\" && exit 1" 24 | }, 25 | "alias": { 26 | "assets": "./src/assets", 27 | "includes": "./src/includes", 28 | "data": "./src/data" 29 | }, 30 | "dependencies": { 31 | "@11ty/eleventy": "^0.7.1", 32 | "cash-cp": "^0.2.0", 33 | "chokidar-cli": "^1.2.1", 34 | "fs-extra": "^7.0.1", 35 | "globby": "^9.0.0", 36 | "npm-run-all": "^4.1.5", 37 | "nunjucks": "^3.1.7", 38 | "parcel-bundler": "^1.11.0", 39 | "rimraf": "^2.6.3", 40 | "workbox-build": "^3.6.3" 41 | }, 42 | "devDependencies": { 43 | "browser-sync": "^2.26.3", 44 | "parcel-plugin-copy-unbundled": "file:lib/parcel/parcel-plugin-copy-unbundled", 45 | "parcel-plugin-eleventy-sync": "file:lib/parcel/parcel-plugin-eleventy-sync", 46 | "parcel-plugin-nunjucks-precompile": "file:lib/parcel/parcel-plugin-nunjucks-precompile", 47 | "parcel-plugin-service-worker": "file:lib/parcel/parcel-plugin-service-worker" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/404.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | title: Page not found 4 | --- 5 |This page could not be found
7 |8 | Return home 9 |
-------------------------------------------------------------------------------- /src/assets/blog.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | title: Blog 4 | --- 5 |