├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docs ├── .firebaserc ├── .gitignore ├── README.md ├── content │ ├── api.md │ ├── design-decisions.md │ ├── faq.md │ ├── getting-started.md │ ├── guides.md │ ├── index.md │ ├── origin-story.md │ ├── platforms.md │ ├── plugins.md │ └── roadmap.md ├── firebase.json ├── package-lock.json ├── package.json ├── scripts │ └── build.js └── src │ ├── assets │ ├── favicon-16.png │ ├── favicon-180.png │ ├── favicon-32.png │ ├── favicon-48.png │ ├── favicon.png │ └── open-graph.png │ ├── components │ ├── favicon.html │ ├── meta.html │ ├── open-graph.html │ └── side-menu.html │ ├── index.css │ ├── index.html │ ├── scripts │ ├── contents.js │ ├── nav.js │ └── performance-marks.js │ └── styles │ ├── code.css │ ├── global.css │ └── main.css ├── examples ├── basic │ ├── README.md │ ├── content │ │ └── notes │ │ │ ├── hakuna-matata.md │ │ │ └── hello-world.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── index.css │ │ ├── index.html │ │ ├── notes.css │ │ ├── notes.html │ │ ├── notes │ │ ├── note.css │ │ └── note.html │ │ └── styles │ │ └── global.css ├── commonjs │ ├── README.md │ ├── content │ │ └── notes │ │ │ ├── hakuna-matata.md │ │ │ └── hello-world.md │ ├── package-lock.json │ ├── package.json │ ├── scripts │ │ └── build.js │ └── src │ │ ├── index.css │ │ ├── index.html │ │ ├── notes.css │ │ ├── notes.html │ │ ├── notes │ │ ├── note.css │ │ └── note.html │ │ └── styles │ │ └── global.css ├── deno │ ├── README.md │ ├── content │ │ └── notes │ │ │ ├── hakuna-matata.md │ │ │ └── hello-world.md │ ├── package-lock.json │ ├── package.json │ ├── scripts │ │ └── build.js │ └── src │ │ ├── index.css │ │ ├── index.html │ │ ├── notes.css │ │ ├── notes.html │ │ ├── notes │ │ ├── note.css │ │ └── note.html │ │ └── styles │ │ └── global.css └── esm │ ├── README.md │ ├── content │ └── notes │ │ ├── hakuna-matata.md │ │ └── hello-world.md │ ├── package-lock.json │ ├── package.json │ ├── scripts │ └── build.js │ └── src │ ├── index.css │ ├── index.html │ ├── notes.css │ ├── notes.html │ ├── notes │ ├── note.css │ └── note.html │ └── styles │ └── global.css ├── package-lock.json ├── package.json ├── packages ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ └── prpl.js │ ├── deno-import-map.json │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── client │ │ ├── prefetch-worker.ts │ │ ├── prefetch.ts │ │ └── router.ts │ │ ├── index.ts │ │ ├── interpolate │ │ ├── interpolate-html.ts │ │ ├── interpolate-list.ts │ │ ├── interpolate-page.ts │ │ ├── interpolate.ts │ │ ├── parse-prpl-attributes.ts │ │ ├── parse-prpl-metadata.ts │ │ └── transform-markdown.ts │ │ ├── lib │ │ ├── cache.ts │ │ ├── cwd.ts │ │ ├── ensure-dir.ts │ │ ├── ensure-file.ts │ │ ├── exists.ts │ │ ├── generate-fs-tree.ts │ │ ├── generate-or-retrieve-fs-tree.ts │ │ ├── log.ts │ │ └── read-dir-safe.ts │ │ └── types │ │ └── prpl.ts ├── create-prpl │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts ├── plugin-aws │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── fetch-from-s3.ts │ │ ├── index.ts │ │ ├── lib │ │ └── init-s3.ts │ │ └── upload-to-s3.ts ├── plugin-cache │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts ├── plugin-code-highlight │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts ├── plugin-css-imports │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts ├── plugin-html-imports │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts ├── plugin-rss │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts ├── plugin-sitemap │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.ts └── server │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ └── prpl-server.js │ ├── package-lock.json │ ├── package.json │ └── src │ ├── index.ts │ ├── server.ts │ └── socket.ts ├── process-development.md ├── process-release.md ├── rollup.config.js ├── tests ├── fixtures │ ├── core │ │ ├── content │ │ │ └── notes │ │ │ │ ├── a.md │ │ │ │ └── b.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── scripts │ │ │ ├── build.cjs │ │ │ └── build.mjs │ │ └── src │ │ │ ├── fragments │ │ │ ├── a.html │ │ │ └── b.html │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── notes.css │ │ │ ├── notes.html │ │ │ ├── notes │ │ │ ├── note.css │ │ │ └── note.html │ │ │ ├── plugin-code-highlight.css │ │ │ ├── plugin-code-highlight.html │ │ │ ├── plugin-css-imports-2.css │ │ │ ├── plugin-css-imports.css │ │ │ ├── plugin-css-imports.html │ │ │ ├── plugin-html-imports.css │ │ │ ├── plugin-html-imports.html │ │ │ ├── server-file.css │ │ │ ├── server-file.html │ │ │ └── styles │ │ │ └── global.css │ ├── plugins │ │ ├── content │ │ │ └── notes │ │ │ │ ├── a.md │ │ │ │ └── b.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── scripts │ │ │ ├── build.cjs │ │ │ └── build.mjs │ │ └── src │ │ │ ├── fragments │ │ │ ├── a.html │ │ │ └── b.html │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── notes.css │ │ │ ├── notes.html │ │ │ ├── notes │ │ │ ├── note.css │ │ │ └── note.html │ │ │ ├── plugin-code-highlight.css │ │ │ ├── plugin-code-highlight.html │ │ │ ├── plugin-css-imports-2.css │ │ │ ├── plugin-css-imports.css │ │ │ ├── plugin-css-imports.html │ │ │ ├── plugin-html-imports.css │ │ │ ├── plugin-html-imports.html │ │ │ ├── server-file.css │ │ │ ├── server-file.html │ │ │ └── styles │ │ │ └── global.css │ └── server │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── scripts │ │ ├── build.cjs │ │ ├── build.mjs │ │ ├── serve.cjs │ │ └── serve.mjs │ │ └── src │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── styles │ │ └── global.css ├── package-lock.json ├── package.json ├── tests │ ├── code-highlight.js │ ├── css-imports.js │ ├── html-imports.js │ ├── lists.js │ ├── pages.js │ ├── rss.js │ ├── server.js │ ├── sitemap.js │ └── tagless-files.js └── utils │ ├── build-site.js │ ├── construct-cssom.js │ ├── construct-dom.js │ ├── fetch.js │ ├── listen-for-change.js │ ├── wait.js │ └── write-site-file.js └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Description 10 | 11 | A clear and concise description of: 12 | 13 | - What behavior you saw 14 | - What behavior you expected to see 15 | 16 | ## Minimal reproduction 17 | 18 | 1. Create a new PRPL site with `npx -y create-prpl@latest` 19 | 2. Add the changes that cause the bug 20 | 3. Commit and push the changes to a public repo 21 | 4. Add the link to the repo here 22 | 23 | > Without a minimal reproduction this issue may be closed without any other action taken. 24 | 25 | ## Environment 26 | 27 | - Operating system (e.g. macOS v12.5.1) 28 | - Node version (e.g. Node v18.10.0) 29 | - PRPL package versions (e.g. `@prpl/core@0.4.0`) 30 | 31 | ## Screenshots 32 | 33 | If applicable, add screenshots to help explain your problem. 34 | 35 | ## Additional context 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | test: 9 | name: ${{ matrix.os }}-node-${{ matrix.node }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | node: [18, 19] 15 | 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | - name: Install monorepo dependencies 26 | run: npm install 27 | 28 | - name: Build PRPL modules 29 | run: npm run build 30 | 31 | - name: Run tests 32 | working-directory: tests 33 | run: npm run test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | node_modules 4 | dist 5 | *.log 6 | *.local 7 | .idea 8 | .vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | node_modules 4 | dist 5 | *.log 6 | *.local 7 | .idea 8 | .vscode 9 | .prettierrc 10 | CHANGELOG.md 11 | README.md 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "none", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always" 13 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | ## Etiquette 6 | 7 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 8 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 9 | extremely unfair for them to suffer abuse or anger for their hard work. 10 | 11 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 12 | world that developers are civilized and selfless people. 13 | 14 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 15 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 16 | 17 | ## Viability 18 | 19 | When requesting or submitting new features, first consider whether it might be useful to others. Open 20 | source projects are used by many developers, who may have entirely different needs to your own. Think about 21 | whether or not your feature is likely to be used by other users of the project. 22 | 23 | ## Procedure 24 | 25 | Before filing an issue: 26 | 27 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident 28 | - Check to make sure your feature suggestion isn't already present within the project 29 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress 30 | - Check the pull requests tab to ensure that the feature isn't already in progress 31 | 32 | Before submitting a pull request: 33 | 34 | - Check the codebase to ensure that your feature doesn't already exist. 35 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 36 | 37 | ## Requirements 38 | 39 | If the project maintainer has any additional requirements, you will find them listed here. 40 | 41 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date 42 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests 43 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting 44 | - **Use conventional commits** - To ensure automatically generated `CHANGELOG.md` files are generated, do use 45 | [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 46 | 47 | **Happy coding**! -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Ty Hopp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is no longer actively maintained.** 2 | 3 | # PRPL 4 | 5 | PRPL is a **lightweight** library for building **fast** static sites. It does two things: 6 | 7 | - Interpolate your content into HTML files 8 | - Maximize your site's runtime speed with the [PRPL pattern](https://web.dev/apply-instant-loading-with-prpl/) 9 | 10 | ## Features 11 | 12 | - Tiny HTML-based API 13 | - Zero configuration 14 | - Zero or near-zero module dependencies 15 | - CLI, CJS and ESM module interfaces 16 | - Define your own template syntax 17 | - Ship no client JavaScript 18 | - Works on Linux, MacOS and Windows 19 | 20 | ## Why? 21 | 22 | All the static site generators I have tried have one or more of these problems: 23 | 24 | - Built on an underlying framework like React, Vue, etc. 25 | - Relies on complex build tools like Webpack, Babel, etc. 26 | - Depends on a massive tree of modules that force constant maintenance 27 | - Has interfaces, source code and documentation that cannot be understood in one sitting 28 | - Requires that your site source be organized in a way that looks nothing like your output 29 | - Forces a huge leap from hello world to a real world implementation 30 | 31 | PRPL is my answer to these gripes. 32 | ## Usage 33 | 34 | PRPL requires [Node](https://nodejs.org/en/) [LTS or greater](https://nodejs.org/en/about/releases/). 35 | 36 | To clone the minimal starter and run it locally, run: 37 | 38 | ``` 39 | npx -y create-prpl@latest 40 | ``` 41 | 42 | Visit [docs](./docs/README.md) for full documentation, guides and design decisions. -------------------------------------------------------------------------------- /docs/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "prpl-docs-firebase" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .env 5 | .idea 6 | .firebase 7 | *.log -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # PRPL Docs 2 | 3 | Documentation for the [PRPL](https://github.com/tyhopp/prpl) project. 4 | -------------------------------------------------------------------------------- /docs/content/design-decisions.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Design decisions 9 | 10 | PRPL is built for **longevity**, a concept not a lot of open source projects have as an explicit goal. 11 | 12 | The idea is if you're building a website that will be around for 5 or 10 years, PRPL should make a compelling enough 13 | case to be a natural fit for the job. 14 | 15 | This page outlines some thinking around the design decisions made in pursuit of that target. 16 | 17 | ## Source-output alignment 18 | 19 | A major frustration in the JavaScript community is the complexity added to projects from tooling like frameworks, 20 | bundlers and compilers. One result of this trend is that **source code no longer looks anything like 21 | the output that the browser consumes**. 22 | 23 | As the gap between source and output widens, the greater the context shift is between writing source code and 24 | inspecting the DOM during development. The further we get in our thinking from how the browser sees our code, 25 | the more layers of abstraction there are to manage. 26 | 27 | PRPL aims to reduce the discrepancy between source code and output. This offers many benefits: 28 | 29 | - Projects are more maintainable over time 30 | - Development and debugging is faster 31 | - Less time and effort is spent on complex tooling 32 | - Easier to move away from PRPL and adopt other tools 33 | 34 | ## One-sitting source code 35 | 36 | Many popular open source projects have a codebase that can take days or weeks to understand. PRPL seeks to avoid this by keeping the overall scope small, and making sure all code is fully typed and 37 | explicitly commented. 38 | 39 | If you can understand PRPL's source code in one sitting (a few hours), you can more easily: 40 | 41 | - Make an informed decision to adopt PRPL or not 42 | - Have confidence that you can fix or adjust the code to your needs 43 | - Contribute to the project 44 | 45 | ## Functions, not configuration 46 | 47 | PRPL sidesteps the concept of a configuration file entirely, preferring to pass any optional parameters directly 48 | into each exported function instead. This constraint forces: 49 | 50 | - Optional parameters to stay as few as would be acceptable for a normal function 51 | - Functions to stay small enough that optional parameters can be easily kept track of 52 | - The scope of the project overall to stay smaller to avoid becoming unwieldy 53 | 54 | ## Web APIs, not framework APIs 55 | 56 | Wherever possible, PRPL leverages native web platform APIs over custom framework APIs. This is the ultimate move to 57 | support the goal of longevity: if the W3C and friends agree on a specification and the major browsers implement 58 | it, there is very little chance of that API going away. 59 | 60 | By betting on web APIs, you: 61 | 62 | - Do not have to waste time learning framework-specific concepts 63 | - Can be fairly confident the code you write today will still run in 5 or 10 years 64 | - Use the *lingua franca* of the web, providing the greatest opportunity for collaboration 65 | 66 | --- 67 | 68 | See [platform APIs](/platforms) next. -------------------------------------------------------------------------------- /docs/content/faq.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # FAQ 9 | 10 | A home for some frequently asked questions. 11 | 12 | ## Does PRPL work with X templating engine? 13 | 14 | No, PRPL is focused entirely on HTML to minimize scope, have fewer dependencies, and encourage use of web 15 | standards for longevity. 16 | 17 | You can however define your own template syntax via the [`templateRegex`](/api#options) option. 18 | 19 | ## Why does diffing happen in main instead of body? 20 | 21 | Diffing within `
` instead of `` allows part of the DOM to be persisted between page renders. 22 | This is useful if you want to persist elements like ` 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/src/scripts/contents.js: -------------------------------------------------------------------------------- 1 | function generateContents() { 2 | const contentsElement = document.querySelector('.side-menu-contents'); 3 | 4 | while (contentsElement.firstChild) { 5 | contentsElement.removeChild(contentsElement.lastChild); 6 | } 7 | 8 | const contents = document.querySelectorAll('h2[id]'); 9 | const fragment = document.createDocumentFragment(); 10 | 11 | contents.forEach((content) => { 12 | const listItem = document.createElement('li'); 13 | const item = document.createElement('a'); 14 | const slug = `${window.location.pathname}#${content.getAttribute('id')}`; 15 | listItem.dataset.slug = slug; 16 | item.textContent = content.textContent; 17 | item.setAttribute('href', slug); 18 | listItem.appendChild(item); 19 | fragment.appendChild(listItem); 20 | }); 21 | 22 | contentsElement.appendChild(fragment); 23 | 24 | const observer = new IntersectionObserver((entries) => { 25 | entries.forEach((entry) => { 26 | const id = entry.target.getAttribute('id'); 27 | const slug = `${window.location.pathname}#${id}`; 28 | const method = entry.intersectionRatio > 0 ? 'add' : 'remove'; 29 | const contentsItem = contentsElement.querySelector(`li[data-slug="${slug}"]`); 30 | if (contentsItem) { 31 | contentsItem.classList[method]('content-active'); 32 | } 33 | }); 34 | }); 35 | 36 | contents.forEach((item) => observer.observe(item)); 37 | } 38 | 39 | window.addEventListener('load', generateContents); 40 | window.addEventListener('prpl-render', generateContents); 41 | -------------------------------------------------------------------------------- /docs/src/scripts/nav.js: -------------------------------------------------------------------------------- 1 | const nav = document.querySelector('.nav'); 2 | const navToggle = document.querySelector('.nav-toggle'); 3 | const main = document.querySelector('.main'); 4 | 5 | navToggle.addEventListener('click', () => { 6 | nav.classList.toggle('nav-mobile-invisible'); 7 | }); 8 | 9 | nav.addEventListener('click', (event) => { 10 | if (event.target.closest('a[href]')) { 11 | nav.classList.toggle('nav-mobile-invisible'); 12 | } 13 | }); 14 | 15 | main.addEventListener('click', () => { 16 | if (!nav.classList.contains('nav-mobile-invisible')) { 17 | nav.classList.add('nav-mobile-invisible'); 18 | } 19 | }); 20 | 21 | const activeItem = document.querySelector(`[data-slug="${window.location.pathname}"]`); 22 | 23 | if (activeItem) { 24 | activeItem.classList.add('nav-active'); 25 | } 26 | 27 | window.addEventListener('prpl-render', () => { 28 | document 29 | .querySelectorAll('.nav-active') 30 | .forEach((element) => element.classList.remove('nav-active')); 31 | const newActiveNavItem = document.querySelector(`[data-slug="${window.location.pathname}"]`); 32 | if (newActiveNavItem) { 33 | newActiveNavItem.classList.add('nav-active'); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /docs/src/scripts/performance-marks.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', () => { 2 | document.querySelector('[data-stats="render"]').textContent = Math.round( 3 | performance.timing.domComplete - performance.timing.domLoading 4 | ); 5 | }); 6 | const performanceObserver = new PerformanceObserver((entries) => { 7 | const renderStart = entries.getEntriesByName('prpl-render-start'); 8 | const renderEnd = entries.getEntriesByName('prpl-render-end'); 9 | if (renderStart.length && renderEnd.length) { 10 | document.querySelector('[data-stats="render"]').textContent = Math.round( 11 | renderEnd[0].startTime - renderStart[0].startTime 12 | ); 13 | performance.clearMarks(); 14 | } 15 | }); 16 | performanceObserver.observe({ entryTypes: ['mark'] }); 17 | -------------------------------------------------------------------------------- /docs/src/styles/code.css: -------------------------------------------------------------------------------- 1 | 2 | pre, 3 | code { 4 | --code-text: var(--text); 5 | --code-slate-gray: slategray; 6 | --code-gray: #999; 7 | --code-purple: #8a47f5; 8 | --code-blue: rgb(16, 163, 207); 9 | --code-green: #34c982; 10 | --code-background: rgb(248, 248, 250); 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | pre, 15 | code { 16 | --code-text: var(--text); 17 | --code-slate-gray: slategray; 18 | --code-gray: #999; 19 | --code-purple: #9166ff; 20 | --code-blue: #24c1f0; 21 | --code-green: #5fe3b9; 22 | --code-background: rgb(12, 12, 26); 23 | } 24 | } 25 | 26 | pre, 27 | code { 28 | color: var(--code-text); 29 | background-color: var(--code-background); 30 | font-family: monospace; 31 | font-size: 14px; 32 | text-align: left; 33 | white-space: pre; 34 | line-height: 1.5; 35 | word-spacing: normal; 36 | word-break: normal; 37 | word-wrap: normal; 38 | border-radius: 0.3em; 39 | 40 | -moz-tab-size: 2; 41 | -o-tab-size: 2; 42 | tab-size: 2; 43 | 44 | -webkit-hyphens: none; 45 | -moz-hyphens: none; 46 | -ms-hyphens: none; 47 | hyphens: none; 48 | } 49 | 50 | code::-moz-selection { 51 | text-shadow: none; 52 | } 53 | 54 | code::selection { 55 | text-shadow: none; 56 | } 57 | 58 | @media print { 59 | code { 60 | text-shadow: none; 61 | } 62 | } 63 | 64 | pre { 65 | padding: 1em; 66 | margin: 0.5em 0; 67 | overflow: auto; 68 | } 69 | 70 | :not(pre) > code { 71 | color: var(--code-purple); 72 | white-space: normal; 73 | margin: 0 -1px; 74 | padding: 0.25em 0.4em; 75 | border: 1px solid transparent; 76 | } 77 | 78 | a:not(pre) > code:hover { 79 | border: 1px dotted var(--accent) 80 | } 81 | 82 | .hljs-comment, 83 | .hljs-string { 84 | color: var(--code-slate-gray); 85 | } 86 | 87 | .hljs-tag, 88 | .hljs-punctuation { 89 | color: var(--code-gray); 90 | } 91 | 92 | .hljs-title { 93 | color: var(--code-blue); 94 | } 95 | 96 | .hljs-name, 97 | .hljs-keyword { 98 | color: var(--code-purple); 99 | } 100 | 101 | .hljs-attr { 102 | color: var(--code-blue); 103 | } 104 | 105 | .hljs-tag > .hljs-string { 106 | color: var(--code-green); 107 | } 108 | 109 | .hljs-regexp, 110 | .hljs-symbol, 111 | .hljs-variable, 112 | .hljs-template-variable, 113 | .hljs-link, 114 | .hljs-selector-attr, 115 | .hljs-operator, 116 | .hljs-selector-pseudo { 117 | color: var(--code-green); 118 | } -------------------------------------------------------------------------------- /docs/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | --selection: #e6e6fa; 10 | --mark: #e6e6fa; 11 | --accent: #8869f6; 12 | --line: #e8e3f1; 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --text: #fff; 18 | --background: #000; 19 | --selection: #9494e3; 20 | --mark: #34216e; 21 | --accent: #8869f6; 22 | --line: #424464; 23 | } 24 | } 25 | 26 | ::selection { 27 | background-color: var(--selection); 28 | } 29 | 30 | ::-moz-selection { 31 | background-color: var(--selection); 32 | } 33 | 34 | ::-webkit-scrollbar { 35 | width: 8px; 36 | height: 8px; 37 | } 38 | 39 | ::-webkit-scrollbar-track { 40 | background: transparent; 41 | } 42 | 43 | ::-webkit-scrollbar-thumb { 44 | background: var(--line); 45 | border-radius: 2px; 46 | } 47 | 48 | html, 49 | body { 50 | margin: 0; 51 | padding: 0; 52 | } 53 | 54 | body { 55 | position: relative; 56 | font-family: 'system-ui', sans-serif; 57 | font-size: 16px; 58 | font-weight: normal; 59 | line-height: 26px; 60 | letter-spacing: 0.2px; 61 | color: var(--text); 62 | background-color: var(--background); 63 | overflow-x: hidden; 64 | -webkit-text-size-adjust: none; 65 | } 66 | 67 | article > h1 { 68 | font-size: 2em; 69 | } 70 | 71 | a { 72 | text-decoration: none; 73 | padding-bottom: 0.1em; 74 | border-bottom: 1px dotted var(--accent); 75 | } 76 | 77 | a:link, 78 | a:visited { 79 | color: var(--text); 80 | } 81 | 82 | blockquote { 83 | border-left: 2px solid var(--line); 84 | margin: 1em; 85 | } 86 | 87 | blockquote p { 88 | padding: 0 1em; 89 | } 90 | 91 | hr { 92 | width: 100%; 93 | border-top: 0; 94 | border-left: 0; 95 | border-right: 0; 96 | border-bottom: 1px dashed var(--line); 97 | } 98 | 99 | footer { 100 | border-top: 1px solid var(--line); 101 | font-size: small; 102 | text-align: center; 103 | padding: 1em 1em 1.75em; 104 | } 105 | 106 | @media (min-width: 990px) { 107 | footer { 108 | padding: 0.5em 0.5em 1em; 109 | } 110 | } 111 | 112 | footer > p { 113 | margin: 0 auto; 114 | } 115 | 116 | .button { 117 | border: 1px solid var(--line); 118 | width: fit-content; 119 | padding: 0.25em 1em; 120 | margin: 1em 1em 0.5em 1em; 121 | cursor: pointer; 122 | } 123 | 124 | .button:hover { 125 | filter: brightness(110%) saturate(110%); 126 | } 127 | 128 | details > summary::-webkit-details-marker { 129 | display: none; 130 | } 131 | 132 | details > summary { 133 | outline: none; 134 | } 135 | 136 | table, th, td { 137 | border: 1px solid var(--line); 138 | border-collapse: collapse; 139 | } 140 | 141 | th, td { 142 | padding: 0.25em 0.5em; 143 | } 144 | 145 | mark { 146 | color: inherit; 147 | background-color: var(--mark); 148 | padding: 0.2em; 149 | border-radius: 0.1em; 150 | } 151 | 152 | .hidden { 153 | display: none; 154 | } 155 | -------------------------------------------------------------------------------- /docs/src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import 'global.css'; 2 | @import 'code.css'; 3 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # PRPL Basic Example 2 | 3 | Basic site example used when running `npx -y create-prpl@latest`. 4 | -------------------------------------------------------------------------------- /examples/basic/content/notes/hakuna-matata.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | Hakuna Matata! 10 | 11 | What a wonderful phrase 12 | 13 | Hakuna Matata! 14 | 15 | Ain't no passing craze 16 | 17 | It means no worries 18 | 19 | For the rest of your days 20 | 21 | It's our problem-free philosophy 22 | 23 | Hakuna Matata! 24 | -------------------------------------------------------------------------------- /examples/basic/content/notes/hello-world.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | This is the body of the first note. 10 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prpl-example-basic", 3 | "version": "0.0.1", 4 | "description": "Basic PRPL example site", 5 | "scripts": { 6 | "clear": "rimraf dist", 7 | "dev": "npm run clear && prpl && prpl-server", 8 | "build": "npm run clear && prpl" 9 | }, 10 | "dependencies": { 11 | "@prpl/core": "^0.4.0" 12 | }, 13 | "devDependencies": { 14 | "@prpl/server": "^0.2.0", 15 | "rimraf": "^3.0.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/basic/src/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | margin: 0 auto; 3 | text-align: center; 4 | width: 500px; 5 | } 6 | 7 | p { 8 | margin: 2em; 9 | } -------------------------------------------------------------------------------- /examples/basic/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PRPL Basic Example 8 | 9 | 10 | 11 | 12 |
13 |

PRPL Basic Example

14 |

Have a peek at the notes page.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/basic/src/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .notes-title { 7 | text-align: center; 8 | } 9 | 10 | article { 11 | margin: 2em 0; 12 | } 13 | 14 | article > * { 15 | margin: 0 0 0.5em 0; 16 | } 17 | 18 | h3 { 19 | margin: 0 0 0.5em 0; 20 | } 21 | 22 | time { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /examples/basic/src/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Notes 8 | 9 | 10 | 11 | 12 |
13 |

Notes

14 | 15 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/basic/src/notes/note.css: -------------------------------------------------------------------------------- 1 | .note { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .note-body { 7 | margin: 2em 0; 8 | } 9 | -------------------------------------------------------------------------------- /examples/basic/src/notes/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | [title] 12 | 13 | 14 | 15 | 16 |
17 |

[title]

18 | 19 |
[body]
20 |

21 | Thanks for reading! Check out some other 22 | notes. 23 |

24 |
25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /examples/basic/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | -------------------------------------------------------------------------------- /examples/commonjs/README.md: -------------------------------------------------------------------------------- 1 | # PRPL CommonJS Example 2 | 3 | Example site [using PRPL in a CommonJS module](scripts/build.js). 4 | 5 | Other than that, it's the same as the [basic example](../basic/README.md). 6 | -------------------------------------------------------------------------------- /examples/commonjs/content/notes/hakuna-matata.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | Hakuna Matata! 10 | 11 | What a wonderful phrase 12 | 13 | Hakuna Matata! 14 | 15 | Ain't no passing craze 16 | 17 | It means no worries 18 | 19 | For the rest of your days 20 | 21 | It's our problem-free philosophy 22 | 23 | Hakuna Matata! 24 | -------------------------------------------------------------------------------- /examples/commonjs/content/notes/hello-world.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | This is the body of the first note. 10 | -------------------------------------------------------------------------------- /examples/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prpl-example-commonjs", 3 | "version": "0.0.1", 4 | "description": "Example site using PRPL in a CommonJS module", 5 | "scripts": { 6 | "clear": "rimraf dist", 7 | "dev": "npm run clear && node scripts/build.js && prpl-server", 8 | "build": "npm run clear && node scripts/build.js" 9 | }, 10 | "dependencies": { 11 | "@prpl/core": "^0.4.0" 12 | }, 13 | "devDependencies": { 14 | "@prpl/server": "^0.2.0", 15 | "rimraf": "^3.0.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/commonjs/scripts/build.js: -------------------------------------------------------------------------------- 1 | const { interpolate } = require('@prpl/core'); 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | async function build() { 11 | await interpolate({ options }); 12 | } 13 | 14 | build(); 15 | -------------------------------------------------------------------------------- /examples/commonjs/src/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | margin: 0 auto; 3 | text-align: center; 4 | width: 500px; 5 | } 6 | 7 | p { 8 | margin: 2em; 9 | } -------------------------------------------------------------------------------- /examples/commonjs/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PRPL CommonJS Example 8 | 9 | 10 | 11 | 12 |
13 |

PRPL CommonJS Example

14 |

Have a peek at the notes page.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/commonjs/src/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .notes-title { 7 | text-align: center; 8 | } 9 | 10 | article { 11 | margin: 2em 0; 12 | } 13 | 14 | article > * { 15 | margin: 0 0 0.5em 0; 16 | } 17 | 18 | h3 { 19 | margin: 0 0 0.5em 0; 20 | } 21 | 22 | time { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /examples/commonjs/src/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Notes 11 | 12 | 13 | 14 | 15 |
16 |

Notes

17 | 18 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/commonjs/src/notes/note.css: -------------------------------------------------------------------------------- 1 | .note { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .note-body { 7 | margin: 2em 0; 8 | } 9 | -------------------------------------------------------------------------------- /examples/commonjs/src/notes/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [title] 9 | 10 | 11 | 12 | 13 |
14 |

[title]

15 | 16 |
[body]
17 |

18 | Thanks for reading! Check out some other 19 | notes. 20 |

21 |
22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /examples/commonjs/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | -------------------------------------------------------------------------------- /examples/deno/README.md: -------------------------------------------------------------------------------- 1 | # PRPL Deno Example 2 | 3 | Example site using [Deno](https://deno.land) for the build step. 4 | 5 | This is possible via the [experimental Node.js compatibility mode](https://deno.land/manual@v1.17.1/npm_nodejs/compatibility_mode) provided in Deno since v1.15. 6 | 7 | Other than that, it's the same as the [basic example](../basic/README.md). 8 | -------------------------------------------------------------------------------- /examples/deno/content/notes/hakuna-matata.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | Hakuna Matata! 10 | 11 | What a wonderful phrase 12 | 13 | Hakuna Matata! 14 | 15 | Ain't no passing craze 16 | 17 | It means no worries 18 | 19 | For the rest of your days 20 | 21 | It's our problem-free philosophy 22 | 23 | Hakuna Matata! 24 | -------------------------------------------------------------------------------- /examples/deno/content/notes/hello-world.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | This is the body of the first note. 10 | -------------------------------------------------------------------------------- /examples/deno/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prpl-example-deno", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Example site using PRPL with Deno", 6 | "type": "module", 7 | "scripts": { 8 | "clear": "rimraf dist", 9 | "dev": "npm run clear && node scripts/build.js && prpl-server", 10 | "dev:deno": "npm run clear && npm run build && prpl-server", 11 | "build": "deno run --unstable --compat --allow-read --allow-write --allow-net --import-map=node_modules/@prpl/core/deno-import-map.json scripts/build.js" 12 | }, 13 | "dependencies": { 14 | "@prpl/core": "^0.4.0" 15 | }, 16 | "devDependencies": { 17 | "@prpl/server": "^0.2.0", 18 | "rimraf": "^3.0.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/deno/scripts/build.js: -------------------------------------------------------------------------------- 1 | import { interpolate } from '@prpl/core'; 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | async function build() { 11 | await interpolate({ options }); 12 | } 13 | 14 | build(); 15 | -------------------------------------------------------------------------------- /examples/deno/src/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | margin: 0 auto; 3 | text-align: center; 4 | width: 500px; 5 | } 6 | 7 | p { 8 | margin: 2em; 9 | } 10 | -------------------------------------------------------------------------------- /examples/deno/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PRPL Deno Example 8 | 9 | 10 | 11 | 12 |
13 |

PRPL Deno Example

14 |

Have a peek at the notes page.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/deno/src/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .notes-title { 7 | text-align: center; 8 | } 9 | 10 | article { 11 | margin: 2em 0; 12 | } 13 | 14 | article > * { 15 | margin: 0 0 0.5em 0; 16 | } 17 | 18 | h3 { 19 | margin: 0 0 0.5em 0; 20 | } 21 | 22 | time { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /examples/deno/src/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Notes 8 | 9 | 10 | 11 | 12 |
13 |

Notes

14 | 15 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/deno/src/notes/note.css: -------------------------------------------------------------------------------- 1 | .note { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .note-body { 7 | margin: 2em 0; 8 | } 9 | -------------------------------------------------------------------------------- /examples/deno/src/notes/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [title] 9 | 10 | 11 | 12 | 13 |
14 |

[title]

15 | 16 |
[body]
17 |

18 | Thanks for reading! Check out some other 19 | notes. 20 |

21 |
22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /examples/deno/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | -------------------------------------------------------------------------------- /examples/esm/README.md: -------------------------------------------------------------------------------- 1 | # PRPL ECMAScript Module Example 2 | 3 | Example site [using PRPL in an ECMAScript module](scripts/build.js). 4 | 5 | Other than that, it's the same as the [basic example](../basic/README.md). 6 | -------------------------------------------------------------------------------- /examples/esm/content/notes/hakuna-matata.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | Hakuna Matata! 10 | 11 | What a wonderful phrase 12 | 13 | Hakuna Matata! 14 | 15 | Ain't no passing craze 16 | 17 | It means no worries 18 | 19 | For the rest of your days 20 | 21 | It's our problem-free philosophy 22 | 23 | Hakuna Matata! 24 | -------------------------------------------------------------------------------- /examples/esm/content/notes/hello-world.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | This is the body of the first note. 10 | -------------------------------------------------------------------------------- /examples/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prpl-example-esm", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Example site using PRPL in an ECMAScript module", 6 | "type": "module", 7 | "scripts": { 8 | "clear": "rimraf dist", 9 | "dev": "npm run clear && node scripts/build.js && prpl-server", 10 | "build": "npm run clear && node scripts/build.js" 11 | }, 12 | "dependencies": { 13 | "@prpl/core": "^0.4.0" 14 | }, 15 | "devDependencies": { 16 | "@prpl/server": "^0.2.0", 17 | "rimraf": "^3.0.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/esm/scripts/build.js: -------------------------------------------------------------------------------- 1 | import { interpolate } from '@prpl/core'; 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | async function build() { 11 | await interpolate({ options }); 12 | } 13 | 14 | build(); 15 | -------------------------------------------------------------------------------- /examples/esm/src/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | margin: 0 auto; 3 | text-align: center; 4 | width: 500px; 5 | } 6 | 7 | p { 8 | margin: 2em; 9 | } 10 | -------------------------------------------------------------------------------- /examples/esm/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PRPL ESM Example 8 | 9 | 10 | 11 | 12 |
13 |

PRPL ESM Example

14 |

Have a peek at the notes page.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/esm/src/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .notes-title { 7 | text-align: center; 8 | } 9 | 10 | article { 11 | margin: 2em 0; 12 | } 13 | 14 | article > * { 15 | margin: 0 0 0.5em 0; 16 | } 17 | 18 | h3 { 19 | margin: 0 0 0.5em 0; 20 | } 21 | 22 | time { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /examples/esm/src/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Notes 8 | 9 | 10 | 11 | 12 |
13 |

Notes

14 | 15 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/esm/src/notes/note.css: -------------------------------------------------------------------------------- 1 | .note { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .note-body { 7 | margin: 2em 0; 8 | } 9 | -------------------------------------------------------------------------------- /examples/esm/src/notes/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [title] 9 | 10 | 11 | 12 | 13 |
14 |

[title]

15 | 16 |
[body]
17 |

18 | Thanks for reading! Check out some other 19 | notes. 20 |

21 |
22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /examples/esm/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "workspaces": [ 4 | "packages/*", 5 | "examples/*", 6 | "tests", 7 | "docs" 8 | ], 9 | "os": [ 10 | "darwin", 11 | "linux", 12 | "win32" 13 | ], 14 | "engines": { 15 | "node": ">=16.17.1" 16 | }, 17 | "scripts": { 18 | "clear": "rimraf packages/*/dist", 19 | "dev": "npx rollup --watch --config rollup.config.js", 20 | "build": "npm run clear && rollup --config rollup.config.js" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-commonjs": "^19.0.0", 24 | "@rollup/plugin-node-resolve": "^13.0.0", 25 | "@types/node": "^16.0.0", 26 | "prettier": "^2.3.2", 27 | "rimraf": "^3.0.2", 28 | "rollup": "^2.52.7", 29 | "rollup-plugin-typescript2": "^0.31.1", 30 | "typescript": "^4.3.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/core). 9 | 10 | # [0.4.0](https://github.com/tyhopp/prpl/compare/@prpl/core@0.3.5...@prpl/core@0.4.0) (2022-10-02) 11 | 12 | ### Features 13 | 14 | * Windows support ([#72](https://github.com/tyhopp/prpl/pull/72)) 15 | 16 | ## [0.3.1](https://github.com/tyhopp/prpl/compare/@prpl/core@0.3.0...@prpl/core@0.3.1) (2021-12-02) 17 | ### Bug Fixes 18 | 19 | * Remove unnecessary stat lib func ([0652c5a](https://github.com/tyhopp/prpl/commit/0652c5a49bbdaaf39329ff8bd4b2f6214960af84)) 20 | 21 | # [0.3.0](https://github.com/tyhopp/prpl/compare/@prpl/core@0.2.12...@prpl/core@0.3.0) (2021-12-02) 22 | ### Bug Fixes 23 | 24 | * Stat lib func default case ([87f97a5](https://github.com/tyhopp/prpl/commit/87f97a57dfecfa6cef6881d06278fccf38d5c256)) 25 | ### Features 26 | 27 | * Deno support via import map ([66af07b](https://github.com/tyhopp/prpl/commit/66af07b1c1ccf7ac663aa06db4d1c800fb39840f)) 28 | 29 | ## [0.2.7](https://github.com/tyhopp/prpl/compare/@prpl/core@0.2.6...@prpl/core@0.2.7) (2021-10-02) 30 | ### Bug Fixes 31 | 32 | * **core:** Router array destructuring ([5f1ce20](https://github.com/tyhopp/prpl/commit/5f1ce208155e4a5b70ee369f37c68e7ededeace2)) 33 | 34 | ## [0.2.6](https://github.com/tyhopp/prpl/compare/@prpl/core@0.2.5...@prpl/core@0.2.6) (2021-10-02) 35 | ### Bug Fixes 36 | 37 | * **core:** Client router error control flow ([f39ce42](https://github.com/tyhopp/prpl/commit/f39ce421be9bb60f7ac8df66b55f859d0349c1c0)) 38 | 39 | ## [0.2.5](https://github.com/tyhopp/prpl/compare/@prpl/core@0.2.4...@prpl/core@0.2.5) (2021-08-02) 40 | ### Bug Fixes 41 | 42 | * **core:** Router hash state changes ([0977d56](https://github.com/tyhopp/prpl/commit/0977d5675f3589d4e04fd0eb3a23c7491d64675b)) 43 | 44 | ## [0.2.4](https://github.com/tyhopp/prpl/compare/@prpl/core@0.2.3...@prpl/core@0.2.4) (2021-08-02) 45 | ### Bug Fixes 46 | 47 | * **core:** Remove clear dist export ([ebf8685](https://github.com/tyhopp/prpl/commit/ebf8685852e8ca193026f445e14d83ffbdda125c)) 48 | 49 | ## [0.1.4](https://github.com/tyhopp/prpl/compare/@prpl/core@0.1.3...@prpl/core@0.1.4) (2021-07-02) 50 | ### Bug Fixes 51 | 52 | * **core:** Optional interpolate arg type ([885927f](https://github.com/tyhopp/prpl/commit/885927f232ae995fa4e9717ff3ca96c521baa15c)) 53 | 54 | ## [0.1.1](https://github.com/tyhopp/prpl/compare/@prpl/core@0.1.0...@prpl/core@0.1.1) (2021-07-01) 55 | ### Bug Fixes 56 | 57 | * **interpolate:** Make template regex option a func ([327fc26](https://github.com/tyhopp/prpl/commit/327fc26a46d8a8a36dfdfc7bfbd5e319e29e0905)) 58 | 59 | # [0.1.0](https://github.com/tyhopp/prpl/compare/@prpl/core@0.0.70...@prpl/core@0.1.0) (2021-07-01) 60 | ### Features 61 | 62 | * **core:** Introduce interpolate options ([707eed6](https://github.com/tyhopp/prpl/commit/707eed654d301bbbf197e0d731a21f89bf8a90d9)) 63 | * **core:** Introduce marked options ([91a8b51](https://github.com/tyhopp/prpl/commit/91a8b5151ef1bf9904593631986e5b9f9a5b0cb2)) 64 | * **core:** Introduce no client JS option ([e8943fe](https://github.com/tyhopp/prpl/commit/e8943fef3cb813524d55e695e837c150fca670a7)) 65 | 66 | ## [0.0.65](https://github.com/tyhopp/prpl/compare/@prpl/core@0.0.64...@prpl/core@0.0.65) (2021-07-01) 67 | ### Bug Fixes 68 | 69 | * **interpolate:** Ensure dist ([becf867](https://github.com/tyhopp/prpl/commit/becf86773572f761d7a1f1393e4a625945c287dc)) 70 | 71 | ## [0.0.64](https://github.com/tyhopp/prpl/compare/@prpl/core@0.0.63...@prpl/core@0.0.64) (2021-07-01) 72 | ### Bug Fixes 73 | 74 | * Client script path resolution ([b83ac0d](https://github.com/tyhopp/prpl/commit/b83ac0df16d52c7f455f62081ee996a5746b7d11)) 75 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/core 2 | 3 | This module interpolates your content into HTML. 4 | 5 | ## Usage 6 | 7 | Can be used via the `prpl` CLI command, or in CJS/ESM. Example in ESM: 8 | 9 | ```javascript 10 | import { interpolate } from '@prpl/core'; 11 | 12 | // Default options 13 | const options = { 14 | noClientJS: false, 15 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 16 | markedOptions: {} 17 | }; 18 | 19 | async function build() { 20 | await interpolate({ options }); 21 | } 22 | 23 | build(); 24 | ``` 25 | 26 | ## Dependencies 27 | 28 | `@prpl/core` has one dependency: [`marked`](https://github.com/markedjs/marked), a markdown compiler. Reasons 29 | for relying on it include: 30 | 31 | - It is not practical to implement a markdown compiler within the PRPL library 32 | - It is fair to assume that most users will author content in markdown given its ubiquity 33 | - [`marked`](https://github.com/markedjs/marked) itself has zero dependencies and is actively maintained 34 | 35 | In the future it may make sense to externalize this dependency, but given the above reasons it is included for now. -------------------------------------------------------------------------------- /packages/core/bin/prpl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { interpolate } from '../dist/index.mjs'; 4 | 5 | interpolate(); 6 | -------------------------------------------------------------------------------- /packages/core/deno-import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "marked": "https://unpkg.com/marked@4.0.2/lib/marked.esm.js" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/core", 3 | "version": "0.4.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@prpl/core", 9 | "version": "0.4.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "marked": "^4.0.12" 13 | }, 14 | "bin": { 15 | "prpl": "bin/prpl.js" 16 | }, 17 | "devDependencies": { 18 | "@types/marked": "^4.0.2" 19 | } 20 | }, 21 | "node_modules/@types/marked": { 22 | "version": "4.0.2", 23 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 24 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 25 | "dev": true 26 | }, 27 | "node_modules/marked": { 28 | "version": "4.0.12", 29 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 30 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 31 | "bin": { 32 | "marked": "bin/marked.js" 33 | }, 34 | "engines": { 35 | "node": ">= 12" 36 | } 37 | } 38 | }, 39 | "dependencies": { 40 | "@types/marked": { 41 | "version": "4.0.2", 42 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 43 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 44 | "dev": true 45 | }, 46 | "marked": { 47 | "version": "4.0.12", 48 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 49 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/core", 3 | "version": "0.4.0", 4 | "description": "HTML-based static site generator", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "bin": { 7 | "prpl": "bin/prpl.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": [ 12 | { 13 | "require": "./dist/index.cjs", 14 | "import": "./dist/index.mjs" 15 | }, 16 | "./dist/index.cjs" 17 | ], 18 | "./deno-import-map.json": "./deno-import-map.json" 19 | }, 20 | "type": "module", 21 | "main": "dist/index.cjs", 22 | "module": "dist/index.mjs", 23 | "types": "dist/packages/core/src/index.d.ts", 24 | "files": [ 25 | "dist", 26 | "deno-import-map.json" 27 | ], 28 | "keywords": [ 29 | "static-site-generator", 30 | "ssg", 31 | "prpl" 32 | ], 33 | "license": "MIT", 34 | "dependencies": { 35 | "marked": "^4.0.12" 36 | }, 37 | "devDependencies": { 38 | "@types/marked": "^4.0.2" 39 | }, 40 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/client/prefetch-worker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Web worker that receives a set of page paths to prefetch data for and returns the html string. 3 | * 4 | * The context variable notifies TS that the context is Worker, not Window. 5 | * @see {@link https://stackoverflow.com/questions/50402004/error-ts2554-expected-2-3-arguments-but-got-1/50420456#50420456} 6 | * 7 | * This file deliberately does not import any types because TypeScript will treat this file as a module and emit an empty export 8 | * declaration at the end of this file. Since modules in web workers are not supported in all browsers, this would break. 9 | * @see {@link https://github.com/microsoft/TypeScript/issues/41567} 10 | */ 11 | const context: Worker = self as any; 12 | 13 | onmessage = (event: { data: string[] }): void => { 14 | try { 15 | const uniqueRelativeLinks: string[] = event?.data; 16 | const prefetchedItems = uniqueRelativeLinks?.map((link) => { 17 | return fetch(link) 18 | .then((response) => response?.text()) 19 | .then((html) => { 20 | return { 21 | storageKey: `prpl-${link}`, 22 | storageValue: html 23 | }; 24 | }) 25 | .catch((error) => { 26 | console.warn('[PRPL] Failed to prefetch page.', error); 27 | }); 28 | }); 29 | Promise.all(prefetchedItems) 30 | .then((response) => { 31 | context?.postMessage(response); 32 | }) 33 | .catch((error) => { 34 | console.warn('[PRPL] Failed to prefetch pages.', error); 35 | }); 36 | } catch (error) { 37 | console.warn('[PRPL] Failed to prefetch in worker', error); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/client/prefetch.ts: -------------------------------------------------------------------------------- 1 | import { PRPLClientStorageItem, PRPLClientEvent } from '../types/prpl.js'; 2 | 3 | /** 4 | * Utility function to calculate unique relative paths to prefetch in a worker. 5 | */ 6 | function getRelativePaths(): string[] { 7 | // TODO - Define more granular definition of which anchor tags the PRPL prefetch worker should to try to fetch 8 | const relativePaths: string[] = [ 9 | ...Array.from(document?.querySelectorAll('a:not([rel])')) 10 | .filter((link) => (link as HTMLAnchorElement)?.href?.includes(window?.location?.origin)) 11 | .map((link) => (link as HTMLAnchorElement)?.href) 12 | ]; 13 | return Array.from(new Set(relativePaths)); 14 | } 15 | 16 | if (window.Worker) { 17 | // Instantiate prefetch worker 18 | const prefetchWorker = new Worker('prefetch-worker.js', { type: 'module' }); 19 | 20 | // Initial prefetch 21 | prefetchWorker?.postMessage([window?.location?.href, ...getRelativePaths()]); 22 | 23 | // Listen for responses 24 | prefetchWorker.onmessage = (event: { data: PRPLClientStorageItem[] }): void => { 25 | const prefetchedPages = event?.data; 26 | for (let i = 0; i < prefetchedPages?.length; i++) { 27 | const { storageKey, storageValue } = prefetchedPages?.[i] || {}; 28 | if (!storageKey || !storageValue) { 29 | return; 30 | } 31 | sessionStorage?.setItem(storageKey, storageValue); 32 | } 33 | }; 34 | 35 | // Subsequent prefetch 36 | window.addEventListener(PRPLClientEvent.render, () => { 37 | try { 38 | prefetchWorker?.postMessage(getRelativePaths()); 39 | } catch (error) { 40 | console.info('[PRPL] Failed to prefetch on subsequent page route. Error:', error); 41 | } 42 | }); 43 | } else { 44 | console.info(`[PRPL] Your browser doesn't support web workers.`); 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types/prpl.js'; 2 | export { interpolate } from './interpolate/interpolate.js'; 3 | export { interpolateHTML } from './interpolate/interpolate-html.js'; 4 | export { interpolatePage } from './interpolate/interpolate-page.js'; 5 | export { interpolateList } from './interpolate/interpolate-list.js'; 6 | export { parsePRPLAttributes } from './interpolate/parse-prpl-attributes.js'; 7 | export { parsePRPLMetadata } from './interpolate/parse-prpl-metadata.js'; 8 | export { transformMarkdown } from './interpolate/transform-markdown.js'; 9 | export { PRPLCache } from './lib/cache.js'; 10 | export { cwd } from './lib/cwd.js'; 11 | export { ensureDir } from './lib/ensure-dir.js'; 12 | export { ensureFile } from './lib/ensure-file.js'; 13 | export { exists } from './lib/exists.js'; 14 | export { generateFileSystemTree } from './lib/generate-fs-tree.js'; 15 | export { generateOrRetrieveFileSystemTree } from './lib/generate-or-retrieve-fs-tree.js'; 16 | export { log } from './lib/log.js'; 17 | export { readDirSafe } from './lib/read-dir-safe.js'; 18 | -------------------------------------------------------------------------------- /packages/core/src/interpolate/interpolate-html.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os'; 2 | import { resolve } from 'path'; 3 | import { writeFile } from 'fs/promises'; 4 | import { parsePRPLAttributes } from './parse-prpl-attributes.js'; 5 | import { 6 | PRPLFileSystemTree, 7 | PRPLTagAttribute, 8 | PRPLTag, 9 | PRPLInterpolateOptions 10 | } from '../types/prpl.js'; 11 | import { interpolatePage } from './interpolate-page.js'; 12 | import { interpolateList } from './interpolate-list.js'; 13 | 14 | /** 15 | * Interpolate an HTML file. 16 | */ 17 | async function interpolateHTML(args: { 18 | srcTree: PRPLFileSystemTree; 19 | options?: PRPLInterpolateOptions; 20 | }): Promise { 21 | const { srcTree, options = {} } = args || {}; 22 | 23 | // Add prefetch and router script tags 24 | if (!options?.noClientJS) { 25 | srcTree.src = srcTree?.src?.replace( 26 | /<\/head>/, 27 | `${EOL}${EOL}` 28 | ); 29 | } 30 | 31 | // If no PRPL tags, write the file to dist 32 | if (!/.*<\/prpl>`, 's'); 59 | page.src = page?.src?.replace(listRegex, listFragment); 60 | } 61 | 62 | // Write page to dist 63 | await writeFile(page?.targetFilePath, page?.src); 64 | return; 65 | } 66 | 67 | // Create and interpolate page 68 | const contentDir = resolve(firstAttr?.parsed?.[PRPLTagAttribute?.src]); 69 | await interpolatePage({ 70 | srcTree, 71 | contentDir, 72 | attrs, 73 | options 74 | }); 75 | } 76 | 77 | export { interpolateHTML }; 78 | -------------------------------------------------------------------------------- /packages/core/src/interpolate/interpolate.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { copyFile } from 'fs/promises'; 3 | import { generateOrRetrieveFileSystemTree } from '../lib/generate-or-retrieve-fs-tree.js'; 4 | import { log } from '../lib/log.js'; 5 | import { cwd } from '../lib/cwd.js'; 6 | import { 7 | PRPLClientScript, 8 | PRPLSourceFileExtension, 9 | PRPLFileSystemTree, 10 | PRPLCacheManager, 11 | PRPLCachePartitionKey, 12 | PRPLInterpolateOptions 13 | } from '../types/prpl.js'; 14 | import { ensureDir } from '../lib/ensure-dir.js'; 15 | import { interpolateHTML } from './interpolate-html.js'; 16 | import { PRPLCache } from '../lib/cache.js'; 17 | 18 | const PRPLClientScripts: PRPLClientScript[] = [ 19 | PRPLClientScript.prefetch, 20 | PRPLClientScript.prefetchWorker, 21 | PRPLClientScript.router 22 | ]; 23 | 24 | /** 25 | * Initialize recursive interpolation. 26 | */ 27 | async function interpolate(args?: { 28 | options?: PRPLInterpolateOptions; 29 | }): Promise { 30 | const { options = {} } = args || {}; 31 | 32 | // Make sure dist exists 33 | await ensureDir(resolve('dist')); 34 | 35 | // Add PRPL client scripts to dist 36 | if (!options?.noClientJS) { 37 | for (let s = 0; s < PRPLClientScripts.length; s++) { 38 | try { 39 | await copyFile( 40 | resolve(await cwd(import.meta), 'client', `${PRPLClientScripts[s]}.js`), 41 | resolve(`dist`, `${PRPLClientScripts[s]}.js`) 42 | ); 43 | } catch (error) { 44 | log.error(`Failed to copy '${PRPLClientScripts[s]}.js' to dist. Error:`, error?.message); 45 | } 46 | } 47 | } 48 | 49 | // Recursively walk the source tree depth first 50 | async function walkSourceTree(items: PRPLFileSystemTree['children']) { 51 | for (let i = 0; i < items.length; i++) { 52 | switch (items?.[i]?.entity) { 53 | case 'file': 54 | await ensureDir(items?.[i]?.targetDir); 55 | 56 | if (items?.[i]?.extension === PRPLSourceFileExtension.html) { 57 | await interpolateHTML({ srcTree: items?.[i], options }); 58 | break; 59 | } 60 | 61 | try { 62 | await copyFile(items?.[i]?.path, items?.[i]?.targetFilePath); 63 | } catch (error) { 64 | log.error( 65 | `Failed to copy '${items?.[i]?.srcRelativeFilePath}' to dist. Error:`, 66 | error?.message 67 | ); 68 | } 69 | break; 70 | case 'directory': 71 | await walkSourceTree(items?.[i]?.children); 72 | break; 73 | } 74 | } 75 | } 76 | 77 | const srcDir = resolve('src'); 78 | const srcTreeReadFileRegExp = new RegExp(PRPLSourceFileExtension.html); 79 | 80 | // Create source tree 81 | const srcTree: PRPLFileSystemTree = await generateOrRetrieveFileSystemTree({ 82 | partitionKey: PRPLCachePartitionKey.src, 83 | entityPath: srcDir, 84 | readFileRegExp: srcTreeReadFileRegExp 85 | }); 86 | 87 | // Walk source tree 88 | await walkSourceTree(srcTree?.children || []); 89 | 90 | log.info('Build complete'); 91 | 92 | // Return cache as an artifact 93 | return PRPLCache?.cache; 94 | } 95 | 96 | export { interpolate }; 97 | -------------------------------------------------------------------------------- /packages/core/src/interpolate/parse-prpl-attributes.ts: -------------------------------------------------------------------------------- 1 | import { PRPLAttributes } from '../types/prpl.js'; 2 | 3 | /** 4 | * Parse attributes from a tag. 5 | */ 6 | async function parsePRPLAttributes(args: { html: string }): Promise { 7 | const { html } = args || {}; 8 | 9 | return [...html?.matchAll(//gs)] 10 | .map((attrs) => attrs?.[1]?.trim()) 11 | .reduce((attrsCollection, attrs) => { 12 | attrsCollection?.push({ 13 | raw: attrs, 14 | parsed: [...attrs?.matchAll(/\s*((.*?)="(.*?)")/g)]?.reduce((acc, curr) => { 15 | return { 16 | ...acc, 17 | [curr?.[2]]: curr?.[3] 18 | }; 19 | }, {}) 20 | }); 21 | 22 | return attrsCollection; 23 | }, []); 24 | } 25 | 26 | export { parsePRPLAttributes }; 27 | -------------------------------------------------------------------------------- /packages/core/src/interpolate/parse-prpl-metadata.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os'; 2 | import { PRPLMetadata } from '../types/prpl.js'; 3 | 4 | /** 5 | * Parse PRPL metadata at the top of content files. 6 | */ 7 | async function parsePRPLMetadata(args: { 8 | src: string; 9 | srcRelativeFilePath: string; 10 | }): Promise { 11 | const { src, srcRelativeFilePath } = args || {}; 12 | 13 | let metadata; 14 | let body; 15 | 16 | try { 17 | const metadataStringRegex = new RegExp(`${EOL}$`, 's'); 18 | const metadataString = //s?.exec(src)?.[1]?.replace(metadataStringRegex, ''); 19 | 20 | const metadataArrayRegex = new RegExp(`${EOL}(.*?): `, 'm'); 21 | const metadataArray = metadataString?.split(metadataArrayRegex)?.slice(1); 22 | 23 | metadata = metadataArray?.reduce((acc, curr, index) => { 24 | if (!(index % 2)) { 25 | acc[curr] = metadataArray?.[index + 1]; 26 | } 27 | return acc; 28 | }, {}); 29 | 30 | const metadataBodyRegex = new RegExp(`-->${EOL}(.*?)$`, 's'); 31 | body = metadataBodyRegex.exec(src)?.[1]; 32 | } catch (error) { 33 | console.error( 34 | `Unable to parse metadata${ 35 | srcRelativeFilePath ? ` in page ${srcRelativeFilePath}` : '' 36 | }. Metadata must be at the top of your file with at least a title and slug property: 37 | ` 41 | ); 42 | } 43 | 44 | return { 45 | ...metadata, 46 | body 47 | }; 48 | } 49 | 50 | export { parsePRPLMetadata }; 51 | -------------------------------------------------------------------------------- /packages/core/src/interpolate/transform-markdown.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked'; 2 | import { PRPLInterpolateOptions } from '../types/prpl'; 3 | 4 | // Override code block rendering 5 | const renderer = { 6 | code(code: string, lang: string) { 7 | return `
${code}
`; 8 | } 9 | }; 10 | 11 | marked.use({ renderer }); 12 | 13 | /** 14 | * Transform content markdown to HTML. 15 | */ 16 | async function transformMarkdown(args: { 17 | markdown: string; 18 | options?: PRPLInterpolateOptions; 19 | }): Promise { 20 | const { markdown, options = {} } = args || {}; 21 | return marked(markdown, options?.markedOptions); 22 | } 23 | 24 | export { transformMarkdown }; 25 | -------------------------------------------------------------------------------- /packages/core/src/lib/cache.ts: -------------------------------------------------------------------------------- 1 | import { PRPLCacheManager, PRPLCachePartitionKey } from '../types/prpl.js'; 2 | import { log } from './log.js'; 3 | 4 | /** 5 | * In-memory cache for file system trees and user-defined objects. 6 | */ 7 | const PRPLCache: PRPLCacheManager = { 8 | cache: { 9 | [PRPLCachePartitionKey.src]: {}, 10 | [PRPLCachePartitionKey.content]: {}, 11 | [PRPLCachePartitionKey.dist]: {} 12 | }, 13 | async define(partitionKey) { 14 | try { 15 | PRPLCache.cache[partitionKey] = {}; 16 | } catch (error) { 17 | log.error(`Failed to define a new partition '${partitionKey}'. Error:`, error?.message); 18 | } 19 | }, 20 | async get(partitionKey, dirPath) { 21 | try { 22 | return PRPLCache?.cache?.[partitionKey]?.[dirPath]; 23 | } catch (error) { 24 | log.error( 25 | `Failed to get cached item '${dirPath}' in partition '${partitionKey}'. Error:`, 26 | error?.message 27 | ); 28 | } 29 | }, 30 | async set(partitionKey, dirPath, item) { 31 | try { 32 | PRPLCache.cache[partitionKey][dirPath] = item; 33 | } catch (error) { 34 | log.error( 35 | `Failed to cache item '${dirPath}' in partition '${partitionKey}'. Error:`, 36 | error?.message 37 | ); 38 | } 39 | } 40 | }; 41 | 42 | export { PRPLCache }; 43 | -------------------------------------------------------------------------------- /packages/core/src/lib/cwd.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { log } from './log.js'; 4 | 5 | /** 6 | * Calculate the current working directory relative to the calling file. 7 | */ 8 | async function cwd(importMeta: ImportMeta): Promise { 9 | try { 10 | return parse(fileURLToPath(importMeta.url)).dir; 11 | } catch (error) { 12 | log.error('Failed to get current working directory. Error:', error?.message); 13 | } 14 | } 15 | 16 | export { cwd }; 17 | -------------------------------------------------------------------------------- /packages/core/src/lib/ensure-dir.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, stat } from 'fs/promises'; 2 | import { log } from './log.js'; 3 | 4 | /** 5 | * Ensure a directory exists given an absolute path. 6 | */ 7 | async function ensureDir(dir: string): Promise { 8 | try { 9 | const fileInfo = await stat(dir); 10 | if (!fileInfo.isDirectory()) { 11 | log.error(`There is no directory at path '${dir}'.`); 12 | } 13 | } catch (error) { 14 | if (error?.code === 'ENOENT') { 15 | await mkdir(dir, { recursive: true }); 16 | return; 17 | } 18 | log.error(`Failed to ensure '${dir}' exists. Error:`, error?.message); 19 | } 20 | } 21 | 22 | export { ensureDir }; 23 | -------------------------------------------------------------------------------- /packages/core/src/lib/ensure-file.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { writeFile, stat } from 'fs/promises'; 3 | import { ensureDir } from './ensure-dir.js'; 4 | import { log } from './log.js'; 5 | 6 | /** 7 | * Ensure a file exists given an absolute path. 8 | */ 9 | async function ensureFile(filePath: string): Promise { 10 | try { 11 | const stats = await stat(filePath); 12 | if (!stats.isFile()) { 13 | log.error(`There is no file at path '${filePath}'.`); 14 | } 15 | } catch (error) { 16 | if (error?.code === 'ENOENT') { 17 | await ensureDir(dirname(filePath)); 18 | await writeFile(filePath, new Uint8Array()); 19 | return; 20 | } 21 | 22 | log.error(`Failed to ensure '${filePath}' exists. Error:`, error?.message); 23 | } 24 | } 25 | 26 | export { ensureFile }; 27 | -------------------------------------------------------------------------------- /packages/core/src/lib/exists.ts: -------------------------------------------------------------------------------- 1 | import { stat } from 'fs/promises'; 2 | import { log } from './log.js'; 3 | 4 | /** 5 | * Check whether an entity exists given an absolute path. 6 | */ 7 | async function exists(filePath: string): Promise { 8 | try { 9 | await stat(filePath); 10 | return true; 11 | } catch (error) { 12 | if (error?.code === 'ENOENT') { 13 | return false; 14 | } 15 | 16 | log.error(`Failed to check '${filePath}' exists. Error:`, error?.message); 17 | } 18 | } 19 | 20 | export { exists }; 21 | -------------------------------------------------------------------------------- /packages/core/src/lib/generate-fs-tree.ts: -------------------------------------------------------------------------------- 1 | import { readFile, stat } from 'fs/promises'; 2 | import { basename, extname, join, parse, resolve, sep } from 'path'; 3 | import { PRPLFileSystemTree, PRPLFileSystemTreeEntity } from '../types/prpl.js'; 4 | import { readDirSafe } from './read-dir-safe.js'; 5 | 6 | export interface PRPLGenerateFileSystemTreeArgs { 7 | entityPath: string; 8 | readFileRegExp?: RegExp; 9 | } 10 | 11 | /** 12 | * Generate a recursive file system tree. 13 | */ 14 | async function generateFileSystemTree( 15 | args: PRPLGenerateFileSystemTreeArgs 16 | ): Promise { 17 | const { entityPath, readFileRegExp } = args; 18 | 19 | const name = basename(entityPath); 20 | 21 | const item: PRPLFileSystemTree = { 22 | path: entityPath, 23 | name, 24 | entity: null 25 | }; 26 | 27 | let stats; 28 | 29 | try { 30 | stats = await stat(entityPath); 31 | } catch (_) { 32 | return null; 33 | } 34 | 35 | if (stats?.isFile()) { 36 | const { dir, base } = parse(entityPath); 37 | 38 | item.srcRelativeDir = dir?.replace(resolve('.'), ''); 39 | item.srcRelativeFilePath = `${item?.srcRelativeDir.split(sep).slice(1).join(sep)}${sep}${base}`; 40 | 41 | item.targetFilePath = entityPath?.replace('src', 'dist'); 42 | item.targetDir = parse(item?.targetFilePath)?.dir; 43 | 44 | item.extension = extname(entityPath)?.toLowerCase(); 45 | item.entity = PRPLFileSystemTreeEntity.file; 46 | 47 | try { 48 | if (typeof readFileRegExp === 'object' && readFileRegExp?.constructor == RegExp) { 49 | if (readFileRegExp?.test(item?.extension)) { 50 | const srcBuffer = await readFile(item?.path); 51 | item.src = srcBuffer?.toString(); 52 | } 53 | } 54 | return item; 55 | } catch (_) {} 56 | } 57 | 58 | if (stats?.isDirectory()) { 59 | let entitiesInDirectory = await readDirSafe(item?.path); 60 | 61 | if (entitiesInDirectory === null) { 62 | return null; 63 | } 64 | 65 | item.children = []; 66 | 67 | for (let i = 0; i < entitiesInDirectory?.length; i++) { 68 | const child = await generateFileSystemTree({ 69 | entityPath: join(item?.path, entitiesInDirectory?.[i]), 70 | readFileRegExp 71 | }); 72 | item?.children?.push(child); 73 | } 74 | 75 | item.entity = PRPLFileSystemTreeEntity.directory; 76 | 77 | return item; 78 | } 79 | 80 | return null; 81 | } 82 | 83 | export { generateFileSystemTree }; 84 | -------------------------------------------------------------------------------- /packages/core/src/lib/generate-or-retrieve-fs-tree.ts: -------------------------------------------------------------------------------- 1 | import { generateFileSystemTree, PRPLGenerateFileSystemTreeArgs } from './generate-fs-tree.js'; 2 | import { PRPLCache } from './cache.js'; 3 | import { log } from './log.js'; 4 | import { PRPLFileSystemTree, PRPLCachePartitionKey } from '../types/prpl.js'; 5 | 6 | interface PRPLRetrieveOrGenerateFileSystemTreeArgs extends PRPLGenerateFileSystemTreeArgs { 7 | partitionKey: PRPLCachePartitionKey | string; 8 | } 9 | 10 | /** 11 | * Retrieve a cached file system tree or generate and cache a new one. 12 | */ 13 | async function generateOrRetrieveFileSystemTree( 14 | args: PRPLRetrieveOrGenerateFileSystemTreeArgs 15 | ): Promise { 16 | const { partitionKey, entityPath, readFileRegExp } = args; 17 | 18 | try { 19 | let fileSystemTree = await PRPLCache?.get(partitionKey, entityPath); 20 | 21 | if (!fileSystemTree) { 22 | fileSystemTree = await generateFileSystemTree({ 23 | entityPath, 24 | readFileRegExp 25 | }); 26 | await PRPLCache?.set(partitionKey, entityPath, fileSystemTree); 27 | } 28 | 29 | return fileSystemTree; 30 | } catch (error) { 31 | log.error(`Failed generate file system tree from '${entityPath}'. Error:`, error?.message); 32 | } 33 | } 34 | 35 | export { generateOrRetrieveFileSystemTree }; 36 | -------------------------------------------------------------------------------- /packages/core/src/lib/log.ts: -------------------------------------------------------------------------------- 1 | interface Log { 2 | debug: (...data: any[]) => void; 3 | info: (...data: any[]) => void; 4 | warning: (...data: any[]) => void; 5 | error: (...data: any[]) => void; 6 | critical: (...data: any[]) => void; 7 | } 8 | 9 | /** 10 | * Console wrapper providing context and color. 11 | */ 12 | const log: Log = { 13 | debug(...args) { 14 | console.debug('\x1b[35m', '[PRPL]', ...args, '\x1b[0m'); 15 | }, 16 | info(...args) { 17 | console.info('\x1b[35m', '[PRPL]', ...args, '\x1b[0m'); 18 | }, 19 | warning(...args) { 20 | console.warn('\x1b[35m', '[PRPL]', ...args, '\x1b[0m'); 21 | }, 22 | error(...args) { 23 | console.error('\x1b[35m', '[PRPL]', ...args, '\x1b[0m'); 24 | }, 25 | critical(...args) { 26 | console.error('\x1b[35m', '[PRPL]', ...args, '\x1b[0m'); 27 | } 28 | }; 29 | 30 | export { log }; 31 | -------------------------------------------------------------------------------- /packages/core/src/lib/read-dir-safe.ts: -------------------------------------------------------------------------------- 1 | import { readdir } from 'fs/promises'; 2 | 3 | /** 4 | * Read only directories that have access permission. 5 | */ 6 | async function readDirSafe(dirPath: string): Promise { 7 | let dirData = null; 8 | try { 9 | dirData = await readdir(dirPath); 10 | } catch (error) { 11 | if (error?.code == 'EACCES' || error?.code == 'EPERM') { 12 | return dirData; 13 | } else { 14 | throw error; 15 | } 16 | } 17 | return dirData; 18 | } 19 | 20 | export { readDirSafe }; 21 | -------------------------------------------------------------------------------- /packages/core/src/types/prpl.ts: -------------------------------------------------------------------------------- 1 | import type { marked } from 'marked'; 2 | 3 | export enum PRPLSourceFileExtension { 4 | html = '.html' 5 | } 6 | 7 | export enum PRPLContentFileExtension { 8 | html = '.html', 9 | markdown = '.md' 10 | } 11 | 12 | export const enum PRPLClientScript { 13 | prefetch = 'prefetch', 14 | prefetchWorker = 'prefetch-worker', 15 | router = 'router' 16 | } 17 | 18 | export enum PRPLTag { 19 | page = 'page', 20 | list = 'list' 21 | } 22 | 23 | export enum PRPLTagAttribute { 24 | type = 'type', 25 | src = 'src', 26 | sortBy = 'sort-by', 27 | direction = 'direction', 28 | limit = 'limit' 29 | } 30 | 31 | export enum PRPLDirectionAttributeValue { 32 | asc = 'asc', 33 | desc = 'desc' 34 | } 35 | 36 | export enum PRPLRequiredMetadata { 37 | title = 'title', 38 | slug = 'slug' 39 | } 40 | 41 | export type PRPLMetadata = Record; 42 | 43 | export enum PRPLFileSystemTreeEntity { 44 | directory = 'directory', 45 | file = 'file' 46 | } 47 | 48 | export interface PRPLFileSystemTree { 49 | path: string; 50 | name: string; 51 | entity: PRPLFileSystemTreeEntity; 52 | extension?: string; 53 | src?: string; 54 | children?: PRPLFileSystemTree[]; 55 | srcRelativeDir?: string; 56 | srcRelativeFilePath?: string; 57 | targetFilePath?: string; 58 | targetDir?: string; 59 | } 60 | 61 | export enum PRPLCachePartitionKey { 62 | src = 'src', 63 | content = 'content', 64 | dist = 'dist' 65 | } 66 | 67 | export type PRPLCachePartition = Record; 68 | 69 | export interface PRPLCacheManager { 70 | cache: Record; 71 | define: (partitionKey: string) => Promise; 72 | get: ( 73 | partitionKey: PRPLCachePartitionKey | string, 74 | dirPath: string 75 | ) => Promise; 76 | set: ( 77 | partitionKey: PRPLCachePartitionKey | string, 78 | dirpath: string, 79 | item: PRPLFileSystemTree | any 80 | ) => Promise; 81 | } 82 | 83 | export type PRPLAttributeMap = { 84 | [key in PRPLTagAttribute]: string; 85 | }; 86 | 87 | export type PRPLAttributes = { 88 | raw: string; 89 | parsed: PRPLAttributeMap; 90 | }; 91 | 92 | export type PRPLClientStorageItem = { 93 | storageKey: string; 94 | storageValue: string; 95 | }; 96 | 97 | export const enum PRPLClientEvent { 98 | render = 'prpl-render' 99 | } 100 | 101 | export const enum PRPLClientPerformanceMark { 102 | renderStart = 'prpl-render-start', 103 | renderEnd = 'prpl-render-end' 104 | } 105 | 106 | export interface PRPLInterpolateOptions { 107 | noClientJS?: boolean; 108 | templateRegex?: RegExp | ((key: string) => RegExp | any); 109 | markedOptions?: marked.MarkedOptions; 110 | } 111 | -------------------------------------------------------------------------------- /packages/create-prpl/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/create-prpl). 9 | 10 | # 0.2.0 (2022-10-23) 11 | 12 | ### Bug Fixes 13 | 14 | * More windows compat ([#77](https://github.com/tyhopp/prpl/pull/77)) 15 | 16 | ## 0.0.31 (2021-12-23) 17 | 18 | ### Bug Fixes 19 | 20 | * **create-prpl:** Remove unnecessary branch arg ([9f979ae](https://github.com/tyhopp/prpl/commit/9f979aea10ac63f8be6c8a63f75fc5b134dcde05)) 21 | -------------------------------------------------------------------------------- /packages/create-prpl/README.md: -------------------------------------------------------------------------------- 1 | # create-prpl 2 | 3 | Utility used for initializing PRPL projects via `npx -y create-prpl@latest`. 4 | 5 | It does three things: 6 | 7 | 1. Copies the [basic PRPL example](https://github.com/tyhopp/prpl/tree/main/examples/basic) to your file system 8 | 2. Downloads one dependency (`@prpl/core`) and one dev dependency (`@prpl/server`) 9 | 3. Runs the local dev server 10 | -------------------------------------------------------------------------------- /packages/create-prpl/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-prpl", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "create-prpl", 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "bin": { 12 | "create-prpl": "dist/index.cjs" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-prpl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-prpl", 3 | "version": "0.2.0", 4 | "description": "Initializer for PRPL projects", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs" 14 | ] 15 | }, 16 | "type": "module", 17 | "bin": "dist/index.cjs", 18 | "main": "dist/index.cjs", 19 | "module": "dist/index.mjs", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "initializer" 28 | ], 29 | "license": "MIT", 30 | "dependencies": { 31 | "fs-extra": "^10.1.0", 32 | "rimraf": "^3.0.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/create-prpl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import path from 'path'; 3 | import rimraf from 'rimraf'; 4 | import fsExtra from 'fs-extra'; 5 | 6 | const cwd = path.resolve(''); 7 | 8 | const repo = 'https://github.com/tyhopp/prpl'; 9 | const example = 'basic'; 10 | 11 | // Exec sync convenience wrapper 12 | function sh(cmd: string, args = {}): void { 13 | execSync(cmd, { 14 | stdio: [0, 1, 2], 15 | cwd, 16 | ...args 17 | }); 18 | } 19 | 20 | // Clone basic starter via sparse checkout, GitHub since Git v2.19 21 | sh(`git clone ${repo} --depth 1 --single-branch --quiet --sparse`); 22 | sh(`git sparse-checkout set examples/${example}`, { cwd: path.resolve('prpl') }); 23 | 24 | // Collapse and rename example 25 | const originalExamplePath = path.resolve(`prpl/examples/${example}`); 26 | const renamedExample = `prpl-example-${example}`; 27 | 28 | // Copy to renamed example 29 | fsExtra.copySync(originalExamplePath, renamedExample); 30 | 31 | // Remove original example 32 | rimraf.sync('prpl'); 33 | 34 | // Remove git history 35 | rimraf.sync(path.join(renamedExample, '.git')); 36 | 37 | // Install dependencies 38 | sh('npm install', { cwd: renamedExample }); 39 | 40 | // Run project 41 | sh('npm run dev', { cwd: renamedExample }); 42 | -------------------------------------------------------------------------------- /packages/plugin-aws/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-aws). 9 | 10 | # [0.3.0](https://github.com/tyhopp/prpl/compare/@prpl/core@0.3.5...@prpl/core@0.4.0) (2022-10-02) 11 | 12 | ### Features 13 | 14 | * Windows support ([#72](https://github.com/tyhopp/prpl/pull/72)) 15 | 16 | ## 0.2.3 (2021-08-13) 17 | 18 | ### Bug Fixes 19 | 20 | * **plugin-aws:** Upload entire file ([a625359](https://github.com/tyhopp/prpl/commit/a62535922d8f675a1bd724301295080343addd64)) 21 | 22 | ## 0.1.2 (2021-07-25) 23 | 24 | ### Bug Fixes 25 | 26 | * **plugin-aws:** Ensure dir slash ([c43ba59](https://github.com/tyhopp/prpl/commit/c43ba59151266927f9d9aa3301d0698b8a3494c2)) 27 | 28 | ## 0.1.1 (2021-07-25) 29 | 30 | ### Bug Fixes 31 | 32 | * **plugin-aws:** Ensure dir ([ce46740](https://github.com/tyhopp/prpl/commit/ce46740e7b7872943bf454c453cc0c19fa5e18fa)) 33 | 34 | # 0.1.0 (2021-07-25) 35 | 36 | ### Features 37 | 38 | * **plugin-aws:** Implement AWS plugin ([81ed380](https://github.com/tyhopp/prpl/commit/81ed380334a2d1ba8bd60278003aac269e8cc44c)) 39 | -------------------------------------------------------------------------------- /packages/plugin-aws/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-aws 2 | 3 | A plugin for [PRPL](https://github.com/tyhopp/prpl) for working with [AWS S3](https://aws.amazon.com/s3/). Useful if 4 | you would rather have your content files stored in S3 instead of checked in under version control. 5 | 6 | ## Dependencies 7 | 8 | `@prpl/plugin-aws` relies on one dependency, [`aws-sdk`](https://github.com/aws/aws-sdk-js). 9 | 10 | ## Requirements 11 | 12 | For this plugin to work, you must have: 13 | 14 | - [An active AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) 15 | - [An S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) 16 | - [Generated access keys](https://aws.amazon.com/premiumsupport/knowledge-center/create-access-key/) 17 | 18 | ## Usage 19 | 20 | Security recommendations: 21 | - Do not hardcode secrets passed into this plugin's exports 22 | - Do not check in any file (e.g., `.env`) containing secrets under version control 23 | 24 | ### Fetch from S3 25 | 26 | ```javascript 27 | const dotenv = require('dotenv'); 28 | const { fetchFromS3 } = require('@prpl/plugin-aws'); 29 | const { interpolate } = require('@prpl/core'); 30 | 31 | // Load environment variables from .env 32 | dotenv.config(); 33 | 34 | // Destructure environment variables 35 | const { 36 | AWS_ACCESS_KEY: AWSAccessKey, 37 | AWS_SECRET_ACCESS_KEY: AWSSecretAccessKey, 38 | AWS_CONTENT_BUCKET: AWSContentBucket, 39 | AWS_CONTENT_BUCKET_REGION: AWSContentBucketRegion 40 | } = process.env; 41 | 42 | // Define our arguments 43 | const keys = { 44 | AWSAccessKey, 45 | AWSSecretAccessKey, 46 | AWSContentBucket, 47 | AWSContentBucketRegion 48 | }; 49 | 50 | // Relative to project root, will use Node's path.resolve under the hood 51 | const targetDir = 'content' 52 | 53 | // Define an async function because top level await is only available in ECMAScript modules 54 | async function build() { 55 | 56 | // Fetch content from S3 and write it to the local file system 57 | await fetchFromS3(keys, targetDir); 58 | 59 | // Interpolate with PRPL core 60 | await interpolate(); 61 | } 62 | 63 | build(); 64 | ``` 65 | 66 | ### Upload to S3 67 | 68 | This function accepts an array of files, so you can upload one or many files as needed for your use case. 69 | 70 | ```javascript 71 | const dotenv = require('dotenv'); 72 | const { uploadToS3 } = require('@prpl/plugin-aws'); 73 | const { generateOrRetrieveFileSystemTree } = require('@prpl/core'); 74 | const { resolve } = require('path'); 75 | 76 | // Load environment variables from .env 77 | dotenv.config(); 78 | 79 | // Destructure environment variables 80 | const { 81 | AWS_ACCESS_KEY: AWSAccessKey, 82 | AWS_SECRET_ACCESS_KEY: AWSSecretAccessKey, 83 | AWS_CONTENT_BUCKET: AWSContentBucket, 84 | AWS_CONTENT_BUCKET_REGION: AWSContentBucketRegion 85 | } = process.env; 86 | 87 | // Define our arguments 88 | const keys = { 89 | AWSAccessKey, 90 | AWSSecretAccessKey, 91 | AWSContentBucket, 92 | AWSContentBucketRegion 93 | }; 94 | 95 | // Not required, but we will use this PRPL core lib function to take advantage of cached content files 96 | const { children: files = [] } = generateOrRetrieveFileSystemTree({ 97 | partitionKey: PRPLCachePartitionKey.content, 98 | entityPath: resolve('content'), 99 | readFileRegExp: new RegExp(`${PRPLContentFileExtension.html}|${PRPLContentFileExtension.markdown}`) 100 | }) 101 | 102 | // Define an async function because top level await is only available in ECMAScript modules 103 | async function upload() { 104 | 105 | // Upload files from local `content` directory to S3 bucket 106 | await uploadToS3(keys, files); 107 | } 108 | 109 | upload(); 110 | ``` -------------------------------------------------------------------------------- /packages/plugin-aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-aws", 3 | "version": "0.3.0", 4 | "description": "PRPL plugin for working with AWS S3", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-aws/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin", 28 | "aws" 29 | ], 30 | "license": "MIT", 31 | "peerDependencies": { 32 | "@prpl/core": ">=0.3.4" 33 | }, 34 | "dependencies": { 35 | "aws-sdk": "^2.953.0" 36 | }, 37 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-aws/src/fetch-from-s3.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir, log } from '@prpl/core'; 2 | import { writeFile } from 'fs/promises'; 3 | import { resolve, parse } from 'path'; 4 | import { initS3 } from './lib/init-s3.js'; 5 | import { PRPLPluginAWSKeys } from './index.js'; 6 | 7 | /** 8 | * Fetch files from an S3 bucket and write to the local file system. 9 | * @param {PRPLPluginAWSKeys} keys 10 | * @param {string} targetDir 11 | * @returns {Promise} 12 | */ 13 | async function fetchFromS3(keys: PRPLPluginAWSKeys, targetDir?: string): Promise { 14 | const { AWSContentBucket } = keys || {}; 15 | 16 | const s3 = await initS3(keys); 17 | 18 | try { 19 | // Get all object metadata in bucket 20 | const getObjectsResponse = await s3.listObjectsV2({ Bucket: AWSContentBucket }).promise(); 21 | const items = getObjectsResponse.Contents || []; 22 | 23 | // Get each object and write to local file system 24 | for (const item of items) { 25 | const { Key } = item || {}; 26 | 27 | const getObjectResponse = await s3.getObject({ Bucket: AWSContentBucket, Key }).promise(); 28 | const content = getObjectResponse.Body.toString(); 29 | 30 | const targetFilePath = resolve(targetDir || 'content', `${Key}.md`); 31 | await ensureDir(parse(targetFilePath)?.dir); 32 | await writeFile(targetFilePath, content); 33 | } 34 | } catch (error) { 35 | log.error(`Failed to fetch remote content. Error:`, error?.message); 36 | return; 37 | } 38 | 39 | log.info('Fetched remote content'); 40 | } 41 | 42 | export { fetchFromS3 }; 43 | -------------------------------------------------------------------------------- /packages/plugin-aws/src/index.ts: -------------------------------------------------------------------------------- 1 | export interface PRPLPluginAWSKeys { 2 | AWSAccessKey: string; 3 | AWSSecretAccessKey: string; 4 | AWSContentBucket: string; 5 | AWSContentBucketRegion: string; 6 | } 7 | 8 | export interface PRPLPluginAWSUploadFile { 9 | src: string; 10 | srcRelativeFilePath: string; 11 | } 12 | 13 | export { fetchFromS3 } from './fetch-from-s3.js'; 14 | export { uploadToS3 } from './upload-to-s3.js'; 15 | -------------------------------------------------------------------------------- /packages/plugin-aws/src/lib/init-s3.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@prpl/core'; 2 | import AWS from 'aws-sdk'; 3 | import { PRPLPluginAWSKeys } from '../index.js'; 4 | 5 | /** 6 | * Initialize AWS and S3 prior to other operations. 7 | * @param {PRPLPluginAWSKeys} keys 8 | * @returns {Promise} 9 | */ 10 | async function initS3(keys: PRPLPluginAWSKeys): Promise { 11 | const { AWSAccessKey, AWSSecretAccessKey, AWSContentBucketRegion } = keys || {}; 12 | 13 | let s3; 14 | 15 | try { 16 | // Configure AWS 17 | AWS.config.update({ 18 | accessKeyId: AWSAccessKey, 19 | secretAccessKey: AWSSecretAccessKey, 20 | region: AWSContentBucketRegion 21 | }); 22 | 23 | // Initialize S3 24 | s3 = new AWS.S3(); 25 | } catch (error) { 26 | log.error(`Failed to initialize S3. Error:`, error?.message); 27 | } 28 | 29 | return s3; 30 | } 31 | 32 | export { initS3 }; 33 | -------------------------------------------------------------------------------- /packages/plugin-aws/src/upload-to-s3.ts: -------------------------------------------------------------------------------- 1 | import { initS3 } from './lib/init-s3.js'; 2 | import { log, parsePRPLMetadata } from '@prpl/core'; 3 | import { PRPLPluginAWSKeys, PRPLPluginAWSUploadFile } from './index.js'; 4 | 5 | /** 6 | * Upload file(s) to an S3 bucket. 7 | * @param {PRPLPluginAWSKeys} keys 8 | * @param {PRPLPluginAWSUploadFile[]} files 9 | * @returns {Promise} 10 | */ 11 | async function uploadToS3( 12 | keys: PRPLPluginAWSKeys, 13 | files: PRPLPluginAWSUploadFile[] 14 | ): Promise { 15 | const { AWSContentBucket } = keys || {}; 16 | 17 | const s3 = await initS3(keys); 18 | 19 | try { 20 | for (const { src, srcRelativeFilePath } of files) { 21 | const { slug } = await parsePRPLMetadata({ 22 | src, 23 | srcRelativeFilePath 24 | }); 25 | 26 | // Upload to S3 27 | await s3 28 | .putObject({ 29 | Bucket: AWSContentBucket, 30 | Key: slug, 31 | Body: src 32 | }) 33 | .promise(); 34 | } 35 | } catch (error) { 36 | log.error(`Failed to upload content. Error:`, error?.message); 37 | return; 38 | } 39 | 40 | log.info('Uploaded content'); 41 | } 42 | 43 | export { uploadToS3 }; 44 | -------------------------------------------------------------------------------- /packages/plugin-cache/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-cache). 9 | 10 | # 0.1.0 (2021-08-07) 11 | 12 | ### Features 13 | 14 | * **plugin-cache:** Introduce plugin-cache ([c9e1588](https://github.com/tyhopp/prpl/commit/c9e1588e1d138d089a65c010a05aac38f3b1893a)) 15 | -------------------------------------------------------------------------------- /packages/plugin-cache/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-cache (WIP) 2 | 3 | **NOTE - This plugin is experimental. It may change significantly in the future. Not recommended for use at present.** 4 | 5 | A plugin for [PRPL](https://github.com/tyhopp/prpl) for cache manipulation. Useful for defining cache partitions that 6 | subsequent plugins may access. 7 | 8 | ## Dependencies 9 | 10 | `@prpl/plugin-cache` has zero dependencies. 11 | 12 | ## Usage 13 | 14 | For example, you may want to pre-define a partition that reads all HTML and CSS files in `dist` so that those files 15 | can be accessed in memory: 16 | 17 | ```javascript 18 | import { resolve } from 'path'; 19 | import { interpolate, PRPLCachePartitionKey } from '@prpl/core'; 20 | import { createCachePartition } from '@prpl/plugin-cache'; 21 | import { resolveHTMLImports } from '@prpl/plugin-html-imports'; 22 | import { resolveCSSImports } from '@prpl/plugin-css-imports'; 23 | 24 | await interpolate(); 25 | 26 | // Pre-define dist partition and use for subsequent plugins 27 | await createCachePartition({ 28 | entityPath: resolve('dist'), 29 | partitionKey: PRPLCachePartitionKey.dist, 30 | readFileRegExp: new RegExp(`.html|.css`) 31 | }); 32 | 33 | await resolveHTMLImports({ 34 | cachePartitionKey: PRPLCachePartitionKey.dist 35 | }); 36 | 37 | await resolveCSSImports({ 38 | cachePartitionKey: PRPLCachePartitionKey.dist 39 | }); 40 | ``` 41 | 42 | By doing this, we save each plugin from generating its own partition that reads from the file system. All plugins 43 | that interact with the cache should return the cache as an artifact that can be conveniently inspected. -------------------------------------------------------------------------------- /packages/plugin-cache/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-cache", 3 | "version": "0.3.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@prpl/plugin-cache", 9 | "version": "0.3.0", 10 | "license": "MIT", 11 | "peerDependencies": { 12 | "@prpl/core": ">=0.3.4" 13 | } 14 | }, 15 | "node_modules/@prpl/core": { 16 | "version": "0.3.4", 17 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 18 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 19 | "peer": true, 20 | "dependencies": { 21 | "@types/marked": "^4.0.2", 22 | "marked": "^4.0.12" 23 | }, 24 | "bin": { 25 | "prpl": "bin/prpl.js" 26 | } 27 | }, 28 | "node_modules/@types/marked": { 29 | "version": "4.0.2", 30 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 31 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 32 | "peer": true 33 | }, 34 | "node_modules/marked": { 35 | "version": "4.0.12", 36 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 37 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 38 | "peer": true, 39 | "bin": { 40 | "marked": "bin/marked.js" 41 | }, 42 | "engines": { 43 | "node": ">= 12" 44 | } 45 | } 46 | }, 47 | "dependencies": { 48 | "@prpl/core": { 49 | "version": "0.3.4", 50 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 51 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 52 | "peer": true, 53 | "requires": { 54 | "@types/marked": "^4.0.2", 55 | "marked": "^4.0.12" 56 | } 57 | }, 58 | "@types/marked": { 59 | "version": "4.0.2", 60 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 61 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 62 | "peer": true 63 | }, 64 | "marked": { 65 | "version": "4.0.12", 66 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 67 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 68 | "peer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/plugin-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-cache", 3 | "version": "0.3.0", 4 | "description": "PRPL plugin for cache manipulation", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-cache/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin" 28 | ], 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "@prpl/core": ">=0.3.4" 32 | }, 33 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 34 | } 35 | -------------------------------------------------------------------------------- /packages/plugin-cache/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateOrRetrieveFileSystemTree, 3 | log, 4 | PRPLCache, 5 | PRPLCacheManager, 6 | PRPLCachePartitionKey 7 | } from '@prpl/core'; 8 | 9 | /** 10 | * Create a new cache partition. 11 | */ 12 | async function createCachePartition({ 13 | entityPath, 14 | partitionKey, 15 | readFileRegExp 16 | }: { 17 | entityPath: string; 18 | partitionKey: PRPLCachePartitionKey | string; 19 | readFileRegExp: RegExp | any; 20 | }): Promise { 21 | // Define a new cache partition 22 | await PRPLCache?.define(partitionKey); 23 | 24 | // Generate or retrieve new cache tree 25 | await generateOrRetrieveFileSystemTree({ 26 | entityPath, 27 | partitionKey, 28 | readFileRegExp 29 | }); 30 | 31 | log.info(`Created cache partition ${partitionKey}`); 32 | 33 | // Return cache as an artifact 34 | return PRPLCache?.cache; 35 | } 36 | 37 | export { createCachePartition }; 38 | -------------------------------------------------------------------------------- /packages/plugin-code-highlight/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-code-highlight). 9 | 10 | ## 0.3.2 (2021-11-03) 11 | 12 | ### Bug Fixes 13 | 14 | * **plugin-code-highlight:** Lock hljs to exact version ([e8c78ed](https://github.com/tyhopp/prpl/commit/e8c78ed0dfe706db6df69395f2ff4100912db848)) 15 | 16 | # 0.3.0 (2021-08-21) 17 | 18 | ### Features 19 | 20 | * **plugin-code-highlight:** Support HTML ([6a90439](https://github.com/tyhopp/prpl/commit/6a90439935782eee655c43e319ed881dc9b32c4c)) 21 | 22 | ## 0.2.2 (2021-08-11) 23 | 24 | ### Bug Fixes 25 | 26 | * **plugin-code-highlight:** Import lang without file extension ([c71abe9](https://github.com/tyhopp/prpl/commit/c71abe9158b6f7bba2ee5170770aab7e8e9b442a)) 27 | 28 | ## 0.2.1 (2021-08-11) 29 | 30 | ### Bug Fixes 31 | 32 | * **plugin-code-highlight:** Hljs lang path resolution ([2b744fa](https://github.com/tyhopp/prpl/commit/2b744fa9e542d52cbb5aadb14efdad0d207bfffe)) 33 | 34 | # 0.2.0 (2021-08-11) 35 | 36 | ### Features 37 | 38 | * **plugin-code-highlight:** Switch to Highlight.js ([f70c0e4](https://github.com/tyhopp/prpl/commit/f70c0e4eb4b2a9c111775c4c33d9879a3e146b96)) 39 | 40 | # 0.1.0 (2021-08-08) 41 | 42 | ### Features 43 | 44 | * **plugin-highlight-code:** Introduce highlight code plugin ([7dbf596](https://github.com/tyhopp/prpl/commit/7dbf596b13c9c9a3b2f438493df2befc9f8d7c88)) 45 | -------------------------------------------------------------------------------- /packages/plugin-code-highlight/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-code-highlight 2 | 3 | A plugin for [PRPL](https://github.com/tyhopp/prpl) that highlights code blocks with [Highlight.js](https://github.com/highlightjs/highlight.js). 4 | 5 | ## Dependencies 6 | 7 | `@prpl/plugin-code-highlight` has two dependencies, [highlight.js](https://github.com/highlightjs/highlight.js) and 8 | [html-escaper](https://github.com/WebReflection/html-escaper). 9 | 10 | ## Usage 11 | 12 | ```javascript 13 | import { interpolate } from '@prpl/core'; 14 | import { highlightCode } from '@prpl/plugin-code-highlight'; 15 | 16 | await interpolate(); 17 | await highlightCode(); 18 | ``` 19 | 20 | ### Notes 21 | 22 | This plugin **does not include any CSS**. It processes code blocks given the specified syntax (e.g., JavaScript, 23 | Python) and outputs DOM structures in place that are possible to style with CSS. 24 | 25 | `` elements are expected to have a `language-*` class with [languages supported by Highlight.js](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). 26 | 27 | If you're writing in HTML you can write your blocks like this: 28 | ```javascript 29 |
30 |   console.log('Hello world!');
31 | 
32 | ``` 33 | 34 | If you're writing in markdown you can write your blocks like this: 35 | 36 | ```` 37 | ```javascript 38 | console.log('Hello world!'); 39 | ``` 40 | ```` 41 | 42 | See [the default CSS stylesheet](https://github.com/highlightjs/highlight.js/blob/main/src/styles/default.css) for 43 | an example of how to style Highlight.js processed DOM structures. 44 | 45 | -------------------------------------------------------------------------------- /packages/plugin-code-highlight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-code-highlight", 3 | "version": "0.4.0", 4 | "description": "PRPL plugin that highlights code blocks with Highlight.js", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-code-highlight/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin" 28 | ], 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "@prpl/core": ">=0.3.4" 32 | }, 33 | "dependencies": { 34 | "highlight.js": "11.2.0", 35 | "html-escaper": "^3.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/plugin-css-imports/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-css-imports). 9 | 10 | # 0.1.0 (2021-08-07) 11 | 12 | ### Features 13 | 14 | * **plugin-css-imports:** Implementation and tests ([479946a](https://github.com/tyhopp/prpl/commit/479946aeb7d1693080802b3257eebba70171d806)) 15 | -------------------------------------------------------------------------------- /packages/plugin-css-imports/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-css-imports 2 | 3 | A plugin for [PRPL](https://github.com/tyhopp/prpl) that resolves CSS import statements at build time. Useful to 4 | avoid extra requests at runtime for imported CSS files. 5 | 6 | This plugin should be disabled when using the PRPL dev server. The dev server is not yet aware of the graph of resources in your site and will not be able to detect changes in imported CSS files. There should be no difference in behavior given the [CSS at-rule](https://caniuse.com/?search=css%20import) is supported in all modern browsers. 7 | 8 | ## Dependencies 9 | 10 | `@prpl/plugin-css-imports` has zero dependencies. 11 | 12 | ### Usage 13 | 14 | ```javascript 15 | import { interpolate } from '@prpl/core'; 16 | import { resolveCSSImports } from '@prpl/plugin-css-imports'; 17 | 18 | await interpolate(); 19 | await resolveCSSImports(); 20 | ``` 21 | 22 | ### Notes 23 | 24 | Given CSS file `a.css`: 25 | 26 | ```css 27 | @import 'b.css'; 28 | 29 | h1 { 30 | color: mediumslateblue; 31 | } 32 | ``` 33 | 34 | and CSS file `b.css`: 35 | 36 | ```css 37 | p { 38 | color: mediumslateblue; 39 | } 40 | ``` 41 | 42 | and [`resolveCSSImports`](https://github.com/tyhopp/prpl/tree/main/packages/plugin-css-imports/src/index.ts) is called, the output of CSS file `a.css` will be: 43 | 44 | ```css 45 | p { 46 | color: mediumslateblue; 47 | } 48 | 49 | h1 { 50 | color: mediumslateblue; 51 | } 52 | ``` -------------------------------------------------------------------------------- /packages/plugin-css-imports/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-css-imports", 3 | "version": "0.3.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@prpl/plugin-css-imports", 9 | "version": "0.3.0", 10 | "license": "MIT", 11 | "peerDependencies": { 12 | "@prpl/core": ">=0.3.4" 13 | } 14 | }, 15 | "node_modules/@prpl/core": { 16 | "version": "0.3.4", 17 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 18 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 19 | "peer": true, 20 | "dependencies": { 21 | "@types/marked": "^4.0.2", 22 | "marked": "^4.0.12" 23 | }, 24 | "bin": { 25 | "prpl": "bin/prpl.js" 26 | } 27 | }, 28 | "node_modules/@types/marked": { 29 | "version": "4.0.2", 30 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 31 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 32 | "peer": true 33 | }, 34 | "node_modules/marked": { 35 | "version": "4.0.12", 36 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 37 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 38 | "peer": true, 39 | "bin": { 40 | "marked": "bin/marked.js" 41 | }, 42 | "engines": { 43 | "node": ">= 12" 44 | } 45 | } 46 | }, 47 | "dependencies": { 48 | "@prpl/core": { 49 | "version": "0.3.4", 50 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 51 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 52 | "peer": true, 53 | "requires": { 54 | "@types/marked": "^4.0.2", 55 | "marked": "^4.0.12" 56 | } 57 | }, 58 | "@types/marked": { 59 | "version": "4.0.2", 60 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 61 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 62 | "peer": true 63 | }, 64 | "marked": { 65 | "version": "4.0.12", 66 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 67 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 68 | "peer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/plugin-css-imports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-css-imports", 3 | "version": "0.3.0", 4 | "description": "PRPL plugin that resolves CSS imports at build time", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-css-imports/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin" 28 | ], 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "@prpl/core": ">=0.3.4" 32 | }, 33 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 34 | } 35 | -------------------------------------------------------------------------------- /packages/plugin-html-imports/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-html-imports). 9 | -------------------------------------------------------------------------------- /packages/plugin-html-imports/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-html-imports 2 | 3 | A plugin for [PRPL](https://github.com/tyhopp/prpl) that resolves HTML import statements at build time. 4 | 5 | The PRPL dev server is not yet aware of the graph of resources in your site and will not be able to detect changes in imported HTML files. Given that [HTML imports](https://caniuse.com/?search=html%20import) is a deprecated specification, it's recommended to only use this plugin for fragments of HTML that you do not need live reloads of during development (for example, meta tags). 6 | 7 | ## Dependencies 8 | 9 | `@prpl/plugin-html-imports` has zero dependencies. 10 | 11 | ### Usage 12 | 13 | ```javascript 14 | import { interpolate } from '@prpl/core'; 15 | import { resolveHTMLImports } from '@prpl/plugin-html-imports'; 16 | 17 | await interpolate(); 18 | await resolveHTMLImports(); 19 | ``` 20 | 21 | ### Notes 22 | 23 | Given HTML file `hello-world.html`: 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 31 | Hello world 32 | 33 | 34 |
35 |

Hello world

36 |
37 | 38 | 39 | ``` 40 | 41 | and an HTML fragment `meta.html`: 42 | 43 | ```html 44 | 45 | 46 | ``` 47 | 48 | and [`resolveHTMLImports`](https://github.com/tyhopp/prpl/tree/main/packages/plugin-html-imports/src/index.ts) is called, the output of HTML file `hello-world.html` will be: 49 | 50 | ```html 51 | 52 | 53 | 54 | 55 | 56 | Hello world 57 | 58 | 59 |
60 |

Hello world

61 |
62 | 63 | 64 | ``` -------------------------------------------------------------------------------- /packages/plugin-html-imports/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-html-imports", 3 | "version": "0.3.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@prpl/plugin-html-imports", 9 | "version": "0.3.0", 10 | "license": "MIT", 11 | "peerDependencies": { 12 | "@prpl/core": ">=0.3.4" 13 | } 14 | }, 15 | "node_modules/@prpl/core": { 16 | "version": "0.3.4", 17 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 18 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 19 | "peer": true, 20 | "dependencies": { 21 | "@types/marked": "^4.0.2", 22 | "marked": "^4.0.12" 23 | }, 24 | "bin": { 25 | "prpl": "bin/prpl.js" 26 | } 27 | }, 28 | "node_modules/@types/marked": { 29 | "version": "4.0.2", 30 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 31 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 32 | "peer": true 33 | }, 34 | "node_modules/marked": { 35 | "version": "4.0.12", 36 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 37 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 38 | "peer": true, 39 | "bin": { 40 | "marked": "bin/marked.js" 41 | }, 42 | "engines": { 43 | "node": ">= 12" 44 | } 45 | } 46 | }, 47 | "dependencies": { 48 | "@prpl/core": { 49 | "version": "0.3.4", 50 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 51 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 52 | "peer": true, 53 | "requires": { 54 | "@types/marked": "^4.0.2", 55 | "marked": "^4.0.12" 56 | } 57 | }, 58 | "@types/marked": { 59 | "version": "4.0.2", 60 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 61 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 62 | "peer": true 63 | }, 64 | "marked": { 65 | "version": "4.0.12", 66 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 67 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 68 | "peer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/plugin-html-imports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-html-imports", 3 | "version": "0.3.0", 4 | "description": "PRPL plugin that resolves HTML imports at build time", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-html-imports/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin" 28 | ], 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "@prpl/core": ">=0.3.4" 32 | }, 33 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 34 | } 35 | -------------------------------------------------------------------------------- /packages/plugin-rss/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-rss). 9 | 10 | # [0.3.0](https://github.com/tyhopp/prpl/compare/@prpl/core@0.3.5...@prpl/core@0.4.0) (2022-10-02) 11 | 12 | ### Features 13 | 14 | * Windows support ([#72](https://github.com/tyhopp/prpl/pull/72)) 15 | 16 | ## 0.2.8 (2021-12-21) 17 | 18 | ### Bug Fixes 19 | 20 | * **plugin-rss:** Sort by date ([b303896](https://github.com/tyhopp/prpl/commit/b30389651a61bc8f35d103452812eea90263d256)) 21 | 22 | # 0.1.0 (2021-08-07) 23 | 24 | ### Features 25 | 26 | * **plugin-rss:** Introduce RSS plugin ([d1e47d3](https://github.com/tyhopp/prpl/commit/d1e47d3b364bf5c8ceaae0a84ef3068a25deb919)) 27 | -------------------------------------------------------------------------------- /packages/plugin-rss/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-rss 2 | 3 | A plugin for [PRPL](https://github.com/tyhopp/prpl) that generates [Atom feeds](https://en.wikipedia.org/wiki/Atom_ 4 | (Web_standard)). 5 | 6 | ## Dependencies 7 | 8 | `@prpl/plugin-rss` has zero dependencies. 9 | 10 | ### Usage 11 | 12 | ```javascript 13 | import { interpolate } from '@prpl/core'; 14 | import { generateRSSFeed } from '@prpl/plugin-rss'; 15 | 16 | await interpolate(); 17 | await generateRSSFeed({ 18 | dir: 'content/notes', 19 | feedTitle: `Ty Hopp's Feed`, 20 | author: 'Ty Hopp', 21 | origin: 'https://tyhopp.com' 22 | }); 23 | ``` -------------------------------------------------------------------------------- /packages/plugin-rss/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-rss", 3 | "version": "0.3.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@prpl/plugin-rss", 9 | "version": "0.3.0", 10 | "license": "MIT", 11 | "peerDependencies": { 12 | "@prpl/core": ">=0.3.4" 13 | } 14 | }, 15 | "node_modules/@prpl/core": { 16 | "version": "0.3.4", 17 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 18 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 19 | "peer": true, 20 | "dependencies": { 21 | "@types/marked": "^4.0.2", 22 | "marked": "^4.0.12" 23 | }, 24 | "bin": { 25 | "prpl": "bin/prpl.js" 26 | } 27 | }, 28 | "node_modules/@types/marked": { 29 | "version": "4.0.2", 30 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 31 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 32 | "peer": true 33 | }, 34 | "node_modules/marked": { 35 | "version": "4.0.12", 36 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 37 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 38 | "peer": true, 39 | "bin": { 40 | "marked": "bin/marked.js" 41 | }, 42 | "engines": { 43 | "node": ">= 12" 44 | } 45 | } 46 | }, 47 | "dependencies": { 48 | "@prpl/core": { 49 | "version": "0.3.4", 50 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 51 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 52 | "peer": true, 53 | "requires": { 54 | "@types/marked": "^4.0.2", 55 | "marked": "^4.0.12" 56 | } 57 | }, 58 | "@types/marked": { 59 | "version": "4.0.2", 60 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 61 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 62 | "peer": true 63 | }, 64 | "marked": { 65 | "version": "4.0.12", 66 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 67 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 68 | "peer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/plugin-rss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-rss", 3 | "version": "0.3.0", 4 | "description": "PRPL plugin that generates an Atom RSS feed", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-rss/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin" 28 | ], 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "@prpl/core": ">=0.3.4" 32 | }, 33 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 34 | } 35 | -------------------------------------------------------------------------------- /packages/plugin-rss/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateOrRetrieveFileSystemTree, 3 | log, 4 | PRPLCache, 5 | PRPLCacheManager, 6 | parsePRPLMetadata, 7 | PRPLContentFileExtension 8 | } from '@prpl/core'; 9 | import { writeFile } from 'fs/promises'; 10 | import { resolve, relative } from 'path'; 11 | 12 | /** 13 | * Generate an RSS feed from items in a directory. 14 | */ 15 | async function generateRSSFeed(args: { 16 | dir: string; 17 | feedTitle: string; 18 | author: string; 19 | origin: string; 20 | iconFilePath?: string; 21 | }): Promise { 22 | const { dir, feedTitle = '', author = '', origin = '', iconFilePath = '' } = args || {}; 23 | 24 | let entries = ''; 25 | 26 | const entryTemplate = ` 27 | 28 | [url] 29 | [isoDate] 30 | [isoDate] 31 | [title] 32 | 33 | 34 | ${author} 35 | 36 | [description] 37 | 38 | 39 | `; 40 | 41 | const partitionKey = relative(resolve(), resolve(dir)); 42 | 43 | // Define a new cache partition 44 | await PRPLCache?.define(partitionKey); 45 | 46 | // Generate file system tree 47 | const contentTree = await generateOrRetrieveFileSystemTree({ 48 | entityPath: resolve(dir), 49 | partitionKey, 50 | readFileRegExp: new RegExp( 51 | `${PRPLContentFileExtension.html}|${PRPLContentFileExtension.markdown}` 52 | ) 53 | }); 54 | 55 | let files = []; 56 | 57 | // Extract metadata out of files 58 | for (const { src, srcRelativeFilePath } of contentTree?.children) { 59 | const metadata = await parsePRPLMetadata({ 60 | src, 61 | srcRelativeFilePath 62 | }); 63 | 64 | files.push(metadata); 65 | } 66 | 67 | // Sort files by date if there is one 68 | let sortedFiles = files?.sort((a, b) => { 69 | if (a?.date && b?.date) { 70 | return new Date(b?.date)?.getTime() - new Date(a?.date)?.getTime(); 71 | } else { 72 | return 0; 73 | } 74 | }); 75 | 76 | // Construct entries from files 77 | for (const file of sortedFiles) { 78 | const { title, slug, date, description } = file || {}; 79 | 80 | const rawDate = new Date(date); 81 | const isoDate = rawDate?.toISOString(); 82 | const url = `${origin}/${slug}`; 83 | 84 | const adjustedKeys = { 85 | url, 86 | isoDate, 87 | title, 88 | description 89 | }; 90 | 91 | let entryInstance = String(entryTemplate); 92 | 93 | for (const key in adjustedKeys) { 94 | if (entryInstance?.includes(`[${key}]`)) { 95 | const regex = new RegExp(`\\[${key}\\]`, 'g'); 96 | entryInstance = entryInstance.replace(regex, adjustedKeys?.[key]); 97 | } 98 | } 99 | 100 | entries = `${entries}${entryInstance}`; 101 | } 102 | 103 | const now = new Date(); 104 | const updated = now.toISOString(); 105 | 106 | // Construct feed 107 | const feed = ` 108 | ${feedTitle} 109 | ${iconFilePath} 110 | 111 | ${updated} 112 | 113 | ${author} 114 | 115 | ${origin}/rss.xml 116 | ${entries} 117 | `; 118 | 119 | await writeFile(resolve('dist', 'rss.xml'), feed); 120 | 121 | log.info('Generated RSS feed'); 122 | 123 | // Return cache as an artifact 124 | return PRPLCache?.cache; 125 | } 126 | 127 | export { generateRSSFeed }; 128 | -------------------------------------------------------------------------------- /packages/plugin-sitemap/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/plugin-sitemap). 9 | 10 | # [0.2.0](https://github.com/tyhopp/prpl/compare/@prpl/core@0.3.5...@prpl/core@0.4.0) (2022-10-02) 11 | 12 | ### Features 13 | 14 | * Windows support ([#72](https://github.com/tyhopp/prpl/pull/72)) 15 | 16 | # 0.1.0 (2021-08-08) 17 | 18 | ### Features 19 | 20 | * **plugin-sitemap:** Introduce sitemap plugin ([ad28f8f](https://github.com/tyhopp/prpl/commit/ad28f8fa2ad7882fd328a41fcc2757b70599a565)) 21 | -------------------------------------------------------------------------------- /packages/plugin-sitemap/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/plugin-sitemap 2 | 3 | A plugin for [PRPL](https://github.com/tyhopp/prpl) that generates a sitemap. 4 | 5 | ## Dependencies 6 | 7 | `@prpl/plugin-sitemap` has zero dependencies. 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | import { interpolate, PRPLCachePartitionKey } from '@prpl/core'; 13 | import { generateSitemap } from '@prpl/plugin-sitemap'; 14 | 15 | await interpolate(); 16 | await generateSitemap({ 17 | origin: 'https://tyhopp.com', 18 | ignoreDirRegex: new RegExp('dist/fragments'), 19 | }); 20 | ``` -------------------------------------------------------------------------------- /packages/plugin-sitemap/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-sitemap", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@prpl/plugin-sitemap", 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "peerDependencies": { 12 | "@prpl/core": ">=0.3.4" 13 | } 14 | }, 15 | "node_modules/@prpl/core": { 16 | "version": "0.3.4", 17 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 18 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 19 | "peer": true, 20 | "dependencies": { 21 | "@types/marked": "^4.0.2", 22 | "marked": "^4.0.12" 23 | }, 24 | "bin": { 25 | "prpl": "bin/prpl.js" 26 | } 27 | }, 28 | "node_modules/@types/marked": { 29 | "version": "4.0.2", 30 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 31 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 32 | "peer": true 33 | }, 34 | "node_modules/marked": { 35 | "version": "4.0.12", 36 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 37 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 38 | "peer": true, 39 | "bin": { 40 | "marked": "bin/marked.js" 41 | }, 42 | "engines": { 43 | "node": ">= 12" 44 | } 45 | } 46 | }, 47 | "dependencies": { 48 | "@prpl/core": { 49 | "version": "0.3.4", 50 | "resolved": "https://registry.npmjs.org/@prpl/core/-/core-0.3.4.tgz", 51 | "integrity": "sha512-K9+jmDKxmwu7Hq35nEzK44N59T9/+PlK23hKoDK6i1FjXfS6ll1/EDD9/eFZNoFY4uD3UaX9lmGK8mVRniCjJw==", 52 | "peer": true, 53 | "requires": { 54 | "@types/marked": "^4.0.2", 55 | "marked": "^4.0.12" 56 | } 57 | }, 58 | "@types/marked": { 59 | "version": "4.0.2", 60 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz", 61 | "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==", 62 | "peer": true 63 | }, 64 | "marked": { 65 | "version": "4.0.12", 66 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 67 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 68 | "peer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/plugin-sitemap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/plugin-sitemap", 3 | "version": "0.2.0", 4 | "description": "PRPL plugin that generates a sitemap", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": [ 9 | { 10 | "require": "./dist/index.cjs", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./dist/index.cjs.js" 14 | ] 15 | }, 16 | "type": "module", 17 | "main": "dist/index.cjs", 18 | "module": "dist/index.mjs", 19 | "types": "dist/packages/plugin-sitemap/src/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "static-site-generator", 25 | "ssg", 26 | "prpl", 27 | "plugin" 28 | ], 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "@prpl/core": ">=0.3.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes to this module will be documented in this file. 4 | 5 | The latest note here may not correspond to the the latest version published. 6 | 7 | If you're looking for what the latest published version is, see [package.json](./package.json) 8 | or [npm](https://www.npmjs.com/package/@prpl/server). 9 | 10 | ## [0.1.11](https://github.com/tyhopp/prpl/compare/@prpl/server@0.1.10...@prpl/server@0.1.11) (2022-03-03) 11 | 12 | ### Bug Fixes 13 | 14 | * **server:** Hard reload for all non html/css files ([c8771e6](https://github.com/tyhopp/prpl/commit/c8771e6e0efb987600f602b94ce779bbb79a5fab)) 15 | 16 | ## [0.1.7](https://github.com/tyhopp/prpl/compare/@prpl/server@0.1.6...@prpl/server@0.1.7) (2021-12-13) 17 | 18 | ### Bug Fixes 19 | 20 | * **server:** HTML reload fails to send ws message to client ([b01a2d9](https://github.com/tyhopp/prpl/commit/b01a2d92f9603bca33046f27ba7a599e5fa65ee0)) 21 | 22 | # [0.1.0](https://github.com/tyhopp/prpl/compare/@prpl/server@0.0.28...@prpl/server@0.1.0) (2021-08-08) 23 | 24 | ### Features 25 | 26 | * **plugin-sitemap:** Introduce sitemap plugin ([ad28f8f](https://github.com/tyhopp/prpl/commit/ad28f8fa2ad7882fd328a41fcc2757b70599a565)) 27 | 28 | ## [0.0.18](https://github.com/tyhopp/prpl/compare/@prpl/server@0.0.17...@prpl/server@0.0.18) (2021-07-24) 29 | 30 | ### Bug Fixes 31 | 32 | * **server:** File watch handlers ([2aae801](https://github.com/tyhopp/prpl/commit/2aae801bbd7dd5c77e5ebb01ac547b26566c49c1)) 33 | 34 | ## [0.0.17](https://github.com/tyhopp/prpl/compare/@prpl/server@0.0.16...@prpl/server@0.0.17) (2021-07-17) 35 | 36 | ### Bug Fixes 37 | 38 | * **server:** Update interpolate HTML args ([2da02fd](https://github.com/tyhopp/prpl/commit/2da02fd4abbfc51107314508449a00eeca40fc2c)) 39 | 40 | ## [0.0.10](https://github.com/tyhopp/prpl/compare/@prpl/server@0.0.9...@prpl/server@0.0.10) (2021-07-10) 41 | 42 | ### Bug Fixes 43 | 44 | * **interpolate:** Ensure dist ([becf867](https://github.com/tyhopp/prpl/commit/becf86773572f761d7a1f1393e4a625945c287dc)) 45 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # @prpl/server 2 | 3 | Development server that watches local file changes and makes those updates in the browser. Usage with `@prpl/core` 4 | is optional. 5 | 6 | ## Dependencies 7 | 8 | [`@prpl/server`](https://github.com/tyhopp/prpl/tree/main/packages/server/src/server.ts) is the only module with more than a couple dependencies: 9 | 10 | - [chokidar](https://github.com/paulmillr/chokidar) for watching file system changes 11 | - [faye-websocket](https://github.com/faye/faye-websocket-node) as a websocket interface on the client and server 12 | - [open](https://www.npmjs.com/package/open) to open the project in the browser 13 | - [serve-handler](https://github.com/vercel/serve-handler) for routing requests and handling responses 14 | 15 | ## Usage 16 | 17 | Run `prpl-server` from the command line in the root of your project. -------------------------------------------------------------------------------- /packages/server/bin/prpl-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { server } from '../dist/index.cjs'; 4 | 5 | server(); 6 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prpl/server", 3 | "version": "0.2.0", 4 | "description": "Development server for PRPL", 5 | "author": "Ty Hopp (https://tyhopp.com)", 6 | "bin": { 7 | "prpl-server": "bin/prpl-server.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": [ 12 | { 13 | "require": "./dist/index.cjs", 14 | "import": "./dist/index.mjs" 15 | }, 16 | "./dist/index.cjs" 17 | ] 18 | }, 19 | "type": "module", 20 | "main": "dist/index.cjs", 21 | "module": "dist/index.mjs", 22 | "types": "dist/packages/server/src/index.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "keywords": [ 27 | "static-site-generator", 28 | "ssg", 29 | "prpl", 30 | "dev-server", 31 | "server" 32 | ], 33 | "license": "MIT", 34 | "peerDependencies": { 35 | "@prpl/core": ">=0.3.4" 36 | }, 37 | "dependencies": { 38 | "chokidar": "^3.5.2", 39 | "faye-websocket": "^0.11.4", 40 | "open": "^8.2.0", 41 | "serve-handler": "^6.1.3" 42 | }, 43 | "gitHead": "57646bee5e16c078b43aa2de487372464594de1c" 44 | } 45 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { server } from './server.js'; 2 | -------------------------------------------------------------------------------- /packages/server/src/socket.ts: -------------------------------------------------------------------------------- 1 | if ('WebSocket' in window) { 2 | const { protocol: originalProtocol, host, pathname } = window?.location || {}; 3 | const protocol = originalProtocol === 'http:' ? 'ws://' : 'wss://'; 4 | const address = `${protocol}${host}${pathname}/ws`; 5 | const socket = new WebSocket(address); 6 | 7 | socket.onmessage = ({ data: href }: { data: string }): void => { 8 | if (!href) { 9 | return; 10 | } 11 | 12 | fetch(href) 13 | .then((response) => { 14 | // Handle HTML files 15 | if (href === '/' || href?.endsWith('.html')) { 16 | return response?.text()?.then((html) => { 17 | const { origin, pathname } = window.location || {}; 18 | 19 | // Refresh the html cache 20 | sessionStorage?.setItem(`prpl-${origin}${href}`, html); 21 | 22 | // If the currently viewing page was updated, refresh it via the router 23 | if (pathname === href) { 24 | const pseudoAnchor = document?.createElement('a'); 25 | pseudoAnchor.href = href; 26 | document.querySelector('main')?.appendChild(pseudoAnchor); 27 | pseudoAnchor?.click(); 28 | } 29 | }); 30 | } 31 | 32 | // Handle CSS 33 | if (href?.endsWith('.css')) { 34 | const styleElements = document.querySelectorAll('[rel="stylesheet"]'); 35 | styleElements.forEach((styleElement) => { 36 | styleElement?.parentNode?.replaceChild(styleElement, styleElement); 37 | }); 38 | return; 39 | } 40 | 41 | // For everything else, keep it simple and refresh the page 42 | window.location.reload(); 43 | }) 44 | .catch((error) => console.warn(`[PRPL] Failed to refresh resource '${href}'. Error:`, error)); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /process-development.md: -------------------------------------------------------------------------------- 1 | # Development process 2 | 3 | This project makes use of [npm workspaces](https://docs.npmjs.com/cli/v8/using-npm/workspaces). 4 | 5 | ## Installation 6 | 7 | In the project root, run: 8 | 9 | ``` 10 | npm i 11 | ``` 12 | 13 | This will install all dependencies for all workspaces. No need to install in any subdirectories. 14 | 15 | ## Development 16 | 17 | To watch changes in source code for specific packages (e.g. `core` and `server`), run: 18 | 19 | ``` 20 | npm run dev core server 21 | ``` 22 | 23 | When you make changes to these package's source code, Rollup will notice and re-bundle them. 24 | 25 | Then navigate to an example project and run the `npm run build` or `npm run dev` commands as per usual. Example project dependencies will automagically be imported from the relevant package workspace, removing the need for `npm link`. 26 | 27 | If you are working with a site outside of a workspace, then you'll still need to `npm link`. Make sure you're using the same Node version in the terminal windows you use since `npm link` works by symlinking to specific Node versions. 28 | -------------------------------------------------------------------------------- /process-release.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | Steps to take when releasing new module versions. 4 | 5 | ## Version bump 6 | 7 | Check out the `main` branch and `git pull` so you have the latest. 8 | 9 | To bump all packages: 10 | 11 | ```shell 12 | npm version patch --workspace=packages # Or minor, major, etc. 13 | ``` 14 | 15 | To bump certain packages: 16 | 17 | ```shell 18 | cd packages/core # Or some other package 19 | npm version patch # Or minor, major, etc. 20 | ``` 21 | 22 | Stage and commit the changes: 23 | 24 | ```shell 25 | git add . 26 | git commit -m "chore: Release 0.4.0" # Swap with target version 27 | ``` 28 | 29 | ## Publish dry run 30 | 31 | To dry run a publish of all packages: 32 | 33 | ```shell 34 | npm publish --dry-run --workspace=packages 35 | ``` 36 | 37 | To dry run a publish of certain packages (e.g. `core`): 38 | 39 | ```shell 40 | cd packages/core # Or some other package 41 | npm publish --dry-run 42 | ``` 43 | 44 | This should build the relevant packages and show log output of what package versions would have been published. Check it is correct. 45 | 46 | ## Publish 47 | 48 | Use the same commands as the publish dry run without the `--dry-run` argument. 49 | 50 | ## Verify 51 | 52 | Verify packages were published successfully, check [npmjs.com](https://www.npmjs.com) and/or run: 53 | 54 | ```shell 55 | npm view [PACKAGE] 56 | ``` 57 | 58 | Once verified, push the version bump changes commit to remote: 59 | 60 | ```shell 61 | git push 62 | ``` 63 | 64 | ## Create tag 65 | 66 | If `core` was bumped, create a new tag and push it: 67 | 68 | ```shell 69 | # Swap with target versions 70 | git tag @prpl/core@0.4.0 71 | git push origin @prpl/core@0.4.0 72 | ``` 73 | 74 | Tags should not be created for other packages. 75 | 76 | ## Changelog update 77 | 78 | Update the changelogs with relevant information for each updated package. 79 | 80 | This process is manual for the time being since the project no longer uses Lerna. 81 | 82 | ## Update sites 83 | 84 | Update docs and example sites with the new published versions. 85 | 86 | This process is manual for the time being, try [npm update --workspaces=1](https://docs.npmjs.com/cli/v8/commands/npm-update#workspaces) and see if it works. 87 | 88 | ## Create a GitHub release (optional) 89 | 90 | For substantial changes, create a GitHub release from the new tag in GitHub. List what the release contains in the description with links to PRs. 91 | 92 | ## Done 93 | 94 | That's it! 95 | 96 | This project used to have more automation, but has removed Lerna since it's no longer maintained and does way more than what this project requires. 97 | 98 | Over time I'd like to automate changelog updates again but without any third party dependencies. If you're interested in helping out, please feel free to open a PR! 99 | -------------------------------------------------------------------------------- /tests/fixtures/core/content/notes/a.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | A body 10 | -------------------------------------------------------------------------------- /tests/fixtures/core/content/notes/b.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | B body 10 | -------------------------------------------------------------------------------- /tests/fixtures/core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-site-core", 3 | "version": "0.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-site-core", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "@prpl/core": "file:../../../packages/core" 12 | } 13 | }, 14 | "../../../packages/core": { 15 | "name": "@prpl/core", 16 | "version": "0.3.4", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@types/marked": "^4.0.2", 20 | "marked": "^4.0.12" 21 | }, 22 | "bin": { 23 | "prpl": "bin/prpl.js" 24 | } 25 | }, 26 | "../../../packages/core/node_modules/@types/marked": { 27 | "version": "4.0.2", 28 | "license": "MIT" 29 | }, 30 | "../../../packages/core/node_modules/marked": { 31 | "version": "4.0.12", 32 | "license": "MIT", 33 | "bin": { 34 | "marked": "bin/marked.js" 35 | }, 36 | "engines": { 37 | "node": ">= 12" 38 | } 39 | }, 40 | "node_modules/@prpl/core": { 41 | "resolved": "../../../packages/core", 42 | "link": true 43 | } 44 | }, 45 | "dependencies": { 46 | "@prpl/core": { 47 | "version": "file:../../../packages/core", 48 | "requires": { 49 | "@types/marked": "^4.0.2", 50 | "marked": "^4.0.12" 51 | }, 52 | "dependencies": { 53 | "@types/marked": { 54 | "version": "4.0.2" 55 | }, 56 | "marked": { 57 | "version": "4.0.12" 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/fixtures/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-site-core", 3 | "version": "0.0.1", 4 | "description": "PRPL site to run tests against", 5 | "dependencies": { 6 | "@prpl/core": "file:../../../packages/core" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/core/scripts/build.cjs: -------------------------------------------------------------------------------- 1 | const { interpolate } = require('@prpl/core'); 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | async function build() { 11 | await interpolate({ options }); 12 | } 13 | 14 | build(); 15 | -------------------------------------------------------------------------------- /tests/fixtures/core/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { interpolate } from '@prpl/core'; 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | await interpolate({ options }); 11 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/fragments/a.html: -------------------------------------------------------------------------------- 1 |

A fragment

-------------------------------------------------------------------------------- /tests/fixtures/core/src/fragments/b.html: -------------------------------------------------------------------------------- 1 |

B fragment

-------------------------------------------------------------------------------- /tests/fixtures/core/src/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | margin: 0 auto; 3 | width: 500px; 4 | } 5 | 6 | h1 { 7 | text-align: center; 8 | } 9 | 10 | .routes-list { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PRPL Test Site 8 | 9 | 10 | 11 | 12 |
13 |

PRPL Test Site

14 |
15 |

Routes:

16 | 24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .notes-title { 7 | text-align: center; 8 | } 9 | 10 | article { 11 | margin: 2em 0; 12 | } 13 | 14 | article > * { 15 | margin: 0 0 0.5em 0; 16 | } 17 | 18 | h3 { 19 | margin: 0 0 0.5em 0; 20 | } 21 | 22 | time { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Notes 11 | 12 | 13 | 14 | 15 |
16 |

Notes

17 | 18 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/notes/note.css: -------------------------------------------------------------------------------- 1 | .note { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .note-body { 7 | margin: 2em 0; 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/notes/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [title] 9 | 10 | 11 | 12 | 13 |
14 |

[title]

15 | 16 |
[body]
17 |

Misc

18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-code-highlight.css: -------------------------------------------------------------------------------- 1 | .plugin-code-highlight { 2 | margin: 0 auto; 3 | width: 520px; 4 | } 5 | 6 | pre, 7 | code { 8 | --code-text: var(--text); 9 | --code-comment: slategray; 10 | --code-punctuation: #999; 11 | --code-keyword: #8a47f5; 12 | --code-title: rgb(16, 163, 207); 13 | --code-variable: #34c982; 14 | --code-background: rgb(248, 248, 250); 15 | } 16 | 17 | @media (prefers-color-scheme: dark) { 18 | pre, 19 | code { 20 | --code-text: var(--text); 21 | --code-comment: slategray; 22 | --code-punctuation: #999; 23 | --code-keyword: #9166ff; 24 | --code-title: #24c1f0; 25 | --code-variable: #5fe3b9; 26 | --code-background: rgb(12, 12, 26); 27 | } 28 | } 29 | 30 | pre, 31 | code { 32 | color: var(--code-text); 33 | background-color: var(--code-background); 34 | font-family: monospace; 35 | font-size: 14px; 36 | text-align: left; 37 | white-space: pre; 38 | line-height: 1.5; 39 | word-spacing: normal; 40 | word-break: normal; 41 | word-wrap: normal; 42 | border-radius: 0.3em; 43 | 44 | -moz-tab-size: 2; 45 | -o-tab-size: 2; 46 | tab-size: 2; 47 | 48 | -webkit-hyphens: none; 49 | -moz-hyphens: none; 50 | -ms-hyphens: none; 51 | hyphens: none; 52 | } 53 | 54 | code::-moz-selection { 55 | text-shadow: none; 56 | } 57 | 58 | code::selection { 59 | text-shadow: none; 60 | } 61 | 62 | @media print { 63 | code { 64 | text-shadow: none; 65 | } 66 | } 67 | 68 | pre { 69 | padding: 1em; 70 | margin: 0.5em 0; 71 | overflow: auto; 72 | } 73 | 74 | :not(pre) > code { 75 | white-space: normal; 76 | padding: 0.25em; 77 | } 78 | 79 | .hljs-comment, 80 | .hljs-string { 81 | color: var(--code-comment); 82 | } 83 | 84 | .hljs-tag, 85 | .hljs-punctuation { 86 | color: var(--code-punctuation); 87 | } 88 | 89 | .hljs-type, 90 | .hljs-number, 91 | .hljs-selector-id, 92 | .hljs-selector-class, 93 | .hljs-quote, 94 | .hljs-template-tag, 95 | .hljs-deletion { 96 | color: var(--code-property); 97 | } 98 | 99 | .hljs-title { 100 | color: var(--code-title); 101 | } 102 | 103 | .hljs-keyword { 104 | color: var(--code-keyword); 105 | } 106 | 107 | .hljs-regexp, 108 | .hljs-symbol, 109 | .hljs-variable, 110 | .hljs-template-variable, 111 | .hljs-link, 112 | .hljs-selector-attr, 113 | .hljs-operator, 114 | .hljs-selector-pseudo { 115 | color: var(--code-variable); 116 | } -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-code-highlight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test code highlight plulgin 8 | 9 | 10 | 11 | 12 |
13 |

Page testing code highlight plugin

14 |
15 |         
16 |           function helloWorld() {
17 |             console.log('Hello world!');
18 |           };
19 | 
20 |           helloWorld();
21 |         
22 |       
23 |

This is some inline code: console.log('Hello world!');

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-css-imports-2.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-decoration: underline; 3 | } -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-css-imports.css: -------------------------------------------------------------------------------- 1 | @import 'plugin-css-imports-2.css'; 2 | 3 | .plugin-css-imports { 4 | margin: 0 auto; 5 | width: 300px; 6 | } -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-css-imports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test CSS imports plugin 8 | 9 | 10 | 11 | 12 |
13 |

Page testing CSS imports plugin

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-html-imports.css: -------------------------------------------------------------------------------- 1 | .plugin-html-imports { 2 | margin: 0 auto; 3 | width: 300px; 4 | } -------------------------------------------------------------------------------- /tests/fixtures/core/src/plugin-html-imports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test HTML imports plugin 8 | 9 | 10 | 11 | 12 |
13 |

Page testing HTML imports plugin

14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/server-file.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/server-file.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test server file mutations 8 | 9 | 10 | 11 | 12 |
13 |

Test server file mutations

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/fixtures/core/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | 42 | h1 { 43 | line-height: 1.25em; 44 | } 45 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/content/notes/a.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | A body 10 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/content/notes/b.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | B body 10 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-site-plugins", 3 | "version": "0.0.1", 4 | "description": "PRPL site to run tests against", 5 | "dependencies": { 6 | "@prpl/core": "file:../../../packages/core", 7 | "@prpl/plugin-code-highlight": "file:../../../packages/plugin-code-highlight", 8 | "@prpl/plugin-css-imports": "file:../../../packages/plugin-css-imports", 9 | "@prpl/plugin-html-imports": "file:../../../packages/plugin-html-imports", 10 | "@prpl/plugin-rss": "file:../../../packages/plugin-rss", 11 | "@prpl/plugin-sitemap": "file:../../../packages/plugin-sitemap" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/scripts/build.cjs: -------------------------------------------------------------------------------- 1 | const { interpolate } = require('@prpl/core'); 2 | const { resolveHTMLImports } = require('@prpl/plugin-html-imports'); 3 | const { resolveCSSImports } = require('@prpl/plugin-css-imports'); 4 | const { highlightCode } = require('@prpl/plugin-code-highlight'); 5 | const { generateRSSFeed } = require('@prpl/plugin-rss'); 6 | const { generateSitemap } = require('@prpl/plugin-sitemap'); 7 | 8 | // Default options 9 | const options = { 10 | noClientJS: false, 11 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 12 | markedOptions: {} 13 | }; 14 | 15 | async function build() { 16 | await interpolate({ options }); 17 | 18 | await resolveHTMLImports(); 19 | 20 | await resolveCSSImports(); 21 | 22 | await highlightCode(); 23 | 24 | const origin = 'http://localhost:8000'; 25 | 26 | await generateRSSFeed({ 27 | dir: 'content/notes', 28 | feedTitle: 'Test feed', 29 | author: 'Ty Hopp', 30 | origin 31 | }); 32 | 33 | await generateSitemap({ 34 | origin, 35 | ignoreDirRegex: new RegExp('dist/fragments') 36 | }); 37 | } 38 | 39 | build(); 40 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { interpolate } from '@prpl/core'; 2 | import { resolveHTMLImports } from '@prpl/plugin-html-imports'; 3 | import { resolveCSSImports } from '@prpl/plugin-css-imports'; 4 | import { highlightCode } from '@prpl/plugin-code-highlight'; 5 | import { generateRSSFeed } from '@prpl/plugin-rss'; 6 | import { generateSitemap } from '@prpl/plugin-sitemap'; 7 | 8 | // Default options 9 | const options = { 10 | noClientJS: false, 11 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 12 | markedOptions: {} 13 | }; 14 | 15 | await interpolate({ options }); 16 | 17 | await resolveHTMLImports(); 18 | 19 | await resolveCSSImports(); 20 | 21 | await highlightCode(); 22 | 23 | const origin = 'http://localhost:8000'; 24 | 25 | await generateRSSFeed({ 26 | dir: 'content/notes', 27 | feedTitle: 'Test feed', 28 | author: 'Ty Hopp', 29 | origin 30 | }); 31 | 32 | await generateSitemap({ 33 | origin, 34 | ignoreDirRegex: new RegExp('dist/fragments') 35 | }); 36 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/fragments/a.html: -------------------------------------------------------------------------------- 1 |

A fragment

-------------------------------------------------------------------------------- /tests/fixtures/plugins/src/fragments/b.html: -------------------------------------------------------------------------------- 1 |

B fragment

-------------------------------------------------------------------------------- /tests/fixtures/plugins/src/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | margin: 0 auto; 3 | width: 500px; 4 | } 5 | 6 | h1 { 7 | text-align: center; 8 | } 9 | 10 | .routes-list { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PRPL Test Site 8 | 9 | 10 | 11 | 12 |
13 |

PRPL Test Site

14 |
15 |

Routes:

16 | 24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .notes-title { 7 | text-align: center; 8 | } 9 | 10 | article { 11 | margin: 2em 0; 12 | } 13 | 14 | article > * { 15 | margin: 0 0 0.5em 0; 16 | } 17 | 18 | h3 { 19 | margin: 0 0 0.5em 0; 20 | } 21 | 22 | time { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Notes 11 | 12 | 13 | 14 | 15 |
16 |

Notes

17 | 18 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/notes/note.css: -------------------------------------------------------------------------------- 1 | .note { 2 | margin: 0 auto; 3 | width: 300px; 4 | } 5 | 6 | .note-body { 7 | margin: 2em 0; 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/notes/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [title] 9 | 10 | 11 | 12 | 13 |
14 |

[title]

15 | 16 |
[body]
17 |

Misc

18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-code-highlight.css: -------------------------------------------------------------------------------- 1 | .plugin-code-highlight { 2 | margin: 0 auto; 3 | width: 520px; 4 | } 5 | 6 | pre, 7 | code { 8 | --code-text: var(--text); 9 | --code-comment: slategray; 10 | --code-punctuation: #999; 11 | --code-keyword: #8a47f5; 12 | --code-title: rgb(16, 163, 207); 13 | --code-variable: #34c982; 14 | --code-background: rgb(248, 248, 250); 15 | } 16 | 17 | @media (prefers-color-scheme: dark) { 18 | pre, 19 | code { 20 | --code-text: var(--text); 21 | --code-comment: slategray; 22 | --code-punctuation: #999; 23 | --code-keyword: #9166ff; 24 | --code-title: #24c1f0; 25 | --code-variable: #5fe3b9; 26 | --code-background: rgb(12, 12, 26); 27 | } 28 | } 29 | 30 | pre, 31 | code { 32 | color: var(--code-text); 33 | background-color: var(--code-background); 34 | font-family: monospace; 35 | font-size: 14px; 36 | text-align: left; 37 | white-space: pre; 38 | line-height: 1.5; 39 | word-spacing: normal; 40 | word-break: normal; 41 | word-wrap: normal; 42 | border-radius: 0.3em; 43 | 44 | -moz-tab-size: 2; 45 | -o-tab-size: 2; 46 | tab-size: 2; 47 | 48 | -webkit-hyphens: none; 49 | -moz-hyphens: none; 50 | -ms-hyphens: none; 51 | hyphens: none; 52 | } 53 | 54 | code::-moz-selection { 55 | text-shadow: none; 56 | } 57 | 58 | code::selection { 59 | text-shadow: none; 60 | } 61 | 62 | @media print { 63 | code { 64 | text-shadow: none; 65 | } 66 | } 67 | 68 | pre { 69 | padding: 1em; 70 | margin: 0.5em 0; 71 | overflow: auto; 72 | } 73 | 74 | :not(pre) > code { 75 | white-space: normal; 76 | padding: 0.25em; 77 | } 78 | 79 | .hljs-comment, 80 | .hljs-string { 81 | color: var(--code-comment); 82 | } 83 | 84 | .hljs-tag, 85 | .hljs-punctuation { 86 | color: var(--code-punctuation); 87 | } 88 | 89 | .hljs-type, 90 | .hljs-number, 91 | .hljs-selector-id, 92 | .hljs-selector-class, 93 | .hljs-quote, 94 | .hljs-template-tag, 95 | .hljs-deletion { 96 | color: var(--code-property); 97 | } 98 | 99 | .hljs-title { 100 | color: var(--code-title); 101 | } 102 | 103 | .hljs-keyword { 104 | color: var(--code-keyword); 105 | } 106 | 107 | .hljs-regexp, 108 | .hljs-symbol, 109 | .hljs-variable, 110 | .hljs-template-variable, 111 | .hljs-link, 112 | .hljs-selector-attr, 113 | .hljs-operator, 114 | .hljs-selector-pseudo { 115 | color: var(--code-variable); 116 | } -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-code-highlight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test code highlight plulgin 8 | 9 | 10 | 11 | 12 |
13 |

Page testing code highlight plugin

14 |
15 |         
16 |           function helloWorld() {
17 |             console.log('Hello world!');
18 |           };
19 | 
20 |           helloWorld();
21 |         
22 |       
23 |

This is some inline code: console.log('Hello world!');

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-css-imports-2.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-decoration: underline; 3 | } -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-css-imports.css: -------------------------------------------------------------------------------- 1 | @import 'plugin-css-imports-2.css'; 2 | 3 | .plugin-css-imports { 4 | margin: 0 auto; 5 | width: 300px; 6 | } -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-css-imports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test CSS imports plugin 8 | 9 | 10 | 11 | 12 |
13 |

Page testing CSS imports plugin

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-html-imports.css: -------------------------------------------------------------------------------- 1 | .plugin-html-imports { 2 | margin: 0 auto; 3 | width: 300px; 4 | } -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/plugin-html-imports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test HTML imports plugin 8 | 9 | 10 | 11 | 12 |
13 |

Page testing HTML imports plugin

14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/server-file.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/server-file.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test server file mutations 8 | 9 | 10 | 11 | 12 |
13 |

Test server file mutations

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/fixtures/plugins/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | 42 | h1 { 43 | line-height: 1.25em; 44 | } 45 | -------------------------------------------------------------------------------- /tests/fixtures/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-site-server", 3 | "version": "0.0.1", 4 | "description": "PRPL site to run tests against", 5 | "scripts": { 6 | "clear": "rimraf dist", 7 | "dev": "npm run clear && prpl && prpl-server" 8 | }, 9 | "dependencies": { 10 | "@prpl/core": "file:../../../packages/core" 11 | }, 12 | "devDependencies": { 13 | "@prpl/server": "file:../../../packages/server", 14 | "rimraf": "^3.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/server/scripts/build.cjs: -------------------------------------------------------------------------------- 1 | const { interpolate } = require('@prpl/core'); 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | async function build() { 11 | await interpolate({ options }); 12 | } 13 | 14 | build(); 15 | -------------------------------------------------------------------------------- /tests/fixtures/server/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { interpolate } from '@prpl/core'; 2 | 3 | // Default options 4 | const options = { 5 | noClientJS: false, 6 | templateRegex: (key) => new RegExp(`\\[${key}\\]`, 'g'), 7 | markedOptions: {} 8 | }; 9 | 10 | await interpolate({ options }); 11 | -------------------------------------------------------------------------------- /tests/fixtures/server/scripts/serve.cjs: -------------------------------------------------------------------------------- 1 | const { server } = require('@prpl/server'); 2 | 3 | async function serve() { 4 | await server(); 5 | } 6 | 7 | serve(); 8 | -------------------------------------------------------------------------------- /tests/fixtures/server/scripts/serve.mjs: -------------------------------------------------------------------------------- 1 | import { server } from '@prpl/server'; 2 | 3 | await server(); 4 | -------------------------------------------------------------------------------- /tests/fixtures/server/src/index.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/server/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test server file mutations 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Test server file mutations

15 |

And JavaScript mutations

16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/server/src/index.js: -------------------------------------------------------------------------------- 1 | document.querySelector('p').textContent = 'I was updated by JavaScript once'; 2 | -------------------------------------------------------------------------------- /tests/fixtures/server/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | :root { 7 | --text: #000; 8 | --background: #fff; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --text: #fff; 14 | --background: #000; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'system-ui', sans-serif; 20 | font-size: 16px; 21 | line-height: 26px; 22 | letter-spacing: 0.2px; 23 | color: var(--text); 24 | background-color: var(--background); 25 | } 26 | 27 | main { 28 | margin: 2em; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | padding-bottom: 0.1em; 34 | border-bottom: 1px dashed; 35 | } 36 | 37 | a:link, 38 | a:visited { 39 | color: var(--text); 40 | } 41 | 42 | h1 { 43 | line-height: 1.25em; 44 | } 45 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "version": "0.0.1", 4 | "description": "Tests for PRPL", 5 | "type": "module", 6 | "scripts": { 7 | "test": "uvu tests --bail" 8 | }, 9 | "devDependencies": { 10 | "cssom": "^0.5.0", 11 | "linkedom": "^0.14.2", 12 | "rimraf": "^3.0.2", 13 | "uvu": "^0.5.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/tests/code-highlight.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | 6 | let document; 7 | 8 | test.before(async () => { 9 | await buildSite('plugins'); 10 | const { document: codeHighlightDocument } = await constructDOM({ 11 | src: 'plugins/dist/plugin-code-highlight.html' 12 | }); 13 | document = codeHighlightDocument; 14 | }); 15 | 16 | test('should highlight code blocks', () => { 17 | assert.ok(document.querySelector('pre > code > span[class*=hljs]')); 18 | }); 19 | 20 | test('should highlight inline code', () => { 21 | assert.ok(document.querySelector('pre > code > span[class*=hljs]')); 22 | }); 23 | 24 | test.run(); 25 | -------------------------------------------------------------------------------- /tests/tests/css-imports.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructCSSOM } from '../utils/construct-cssom.js'; 5 | 6 | let cssom; 7 | 8 | test.before(async () => { 9 | await buildSite('plugins'); 10 | cssom = await constructCSSOM({ src: 'plugins/dist/plugin-css-imports.css' }); 11 | }); 12 | 13 | test('should interpolate CSS imports', () => { 14 | const [firstRule, secondRule] = cssom.cssRules; 15 | assert.is(firstRule.selectorText, 'h1'); 16 | assert.is(secondRule.selectorText, '.plugin-css-imports'); 17 | }); 18 | 19 | test.run(); 20 | -------------------------------------------------------------------------------- /tests/tests/html-imports.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | 6 | const fragments = { 7 | a: { 8 | id: 'a', 9 | text: 'A fragment' 10 | }, 11 | b: { 12 | id: 'b', 13 | text: 'B fragment' 14 | } 15 | }; 16 | 17 | const { a, b } = fragments; 18 | 19 | let document; 20 | 21 | test.before(async () => { 22 | await buildSite('plugins'); 23 | const { document: htmlImportsDocument } = await constructDOM({ 24 | src: 'plugins/dist/plugin-html-imports.html' 25 | }); 26 | document = htmlImportsDocument; 27 | }); 28 | 29 | test('should interpolate HTML imports', () => { 30 | assert.is(document.querySelector(`[data-cy=fragment-${a.id}]`).textContent, a.text); 31 | assert.is(document.querySelector(`[data-cy=fragment-${b.id}]`).textContent, b.text); 32 | }); 33 | 34 | test.run(); 35 | -------------------------------------------------------------------------------- /tests/tests/lists.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | 6 | const page = { 7 | a: { 8 | id: 'a', 9 | slug: 'notes/a', 10 | title: 'A', 11 | description: 'A description', 12 | date: '2020-01-01', 13 | other: 'Misc' 14 | }, 15 | b: { 16 | id: 'b', 17 | slug: 'notes/b', 18 | title: 'B', 19 | description: 'B description', 20 | date: '2020-01-02', 21 | other: 'Misc' 22 | } 23 | }; 24 | 25 | let document; 26 | 27 | test.before(async () => { 28 | await buildSite('core'); 29 | const { document: notesDocument } = await constructDOM({ src: 'core/dist/notes.html' }); 30 | document = notesDocument; 31 | }); 32 | 33 | test('should interpolate templates', () => { 34 | assert.is( 35 | document.querySelector(`[data-cy=list-item-${page.a.id}-slug]`).getAttribute('href'), 36 | page.a.slug 37 | ); 38 | assert.is( 39 | document.querySelector(`[data-cy=list-item-${page.a.id}-title]`).textContent, 40 | page.a.title 41 | ); 42 | assert.is( 43 | document.querySelector(`[data-cy=list-item-${page.a.id}-description]`).textContent, 44 | page.a.description 45 | ); 46 | assert.is( 47 | document.querySelector(`[data-cy=list-item-${page.a.id}-date]`).textContent, 48 | page.a.date 49 | ); 50 | 51 | assert.is( 52 | document.querySelector(`[data-cy=list-item-${page.b.id}-slug]`).getAttribute('href'), 53 | page.b.slug 54 | ); 55 | assert.is( 56 | document.querySelector(`[data-cy=list-item-${page.b.id}-title]`).textContent, 57 | page.b.title 58 | ); 59 | assert.is( 60 | document.querySelector(`[data-cy=list-item-${page.b.id}-description]`).textContent, 61 | page.b.description 62 | ); 63 | assert.is( 64 | document.querySelector(`[data-cy=list-item-${page.b.id}-date]`).textContent, 65 | page.b.date 66 | ); 67 | }); 68 | 69 | test('should render non template markup as is', () => { 70 | assert.is( 71 | document.querySelector(`[data-cy=list-item-${page.a.id}-other]`).textContent, 72 | page.a.other 73 | ); 74 | assert.is( 75 | document.querySelector(`[data-cy=list-item-${page.b.id}-other]`).textContent, 76 | page.b.other 77 | ); 78 | }); 79 | 80 | test.run(); 81 | -------------------------------------------------------------------------------- /tests/tests/pages.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | 6 | const page = { 7 | a: { 8 | id: 'a', 9 | title: 'A', 10 | date: '2020-01-01', 11 | body: 'A body', 12 | other: 'Misc' 13 | }, 14 | b: { 15 | id: 'b', 16 | title: 'B', 17 | date: '2020-01-02', 18 | body: 'B body', 19 | other: 'Misc' 20 | } 21 | }; 22 | 23 | const document = {}; 24 | 25 | test.before(async () => { 26 | await buildSite('core'); 27 | const { document: documentA } = await constructDOM({ src: 'core/dist/notes/a.html' }); 28 | const { document: documentB } = await constructDOM({ src: 'core/dist/notes/b.html' }); 29 | document.a = documentA; 30 | document.b = documentB; 31 | }); 32 | 33 | test('should interpolate templates', () => { 34 | assert.is(document.a.querySelector('[data-cy=page-meta-title]').textContent, page.a.title); 35 | assert.is(document.a.querySelector('[data-cy=page-title]').textContent, page.a.title); 36 | assert.is(document.a.querySelector('[data-cy=page-date]').textContent, page.a.date); 37 | assert.is(document.a.querySelector('[data-cy=page-body] > p').textContent, page.a.body); 38 | 39 | assert.is(document.b.querySelector('[data-cy=page-meta-title]').textContent, page.b.title); 40 | assert.is(document.b.querySelector('[data-cy=page-title]').textContent, page.b.title); 41 | assert.is(document.b.querySelector('[data-cy=page-date]').textContent, page.b.date); 42 | assert.is(document.b.querySelector('[data-cy=page-body] > p').textContent, page.b.body); 43 | }); 44 | 45 | test('should render non template markup as is', () => { 46 | assert.is(document.a.querySelector('[data-cy=page-other]').textContent, page.a.other); 47 | assert.is(document.b.querySelector('[data-cy=page-other]').textContent, page.b.other); 48 | }); 49 | 50 | test.run(); 51 | -------------------------------------------------------------------------------- /tests/tests/server.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | import { constructCSSOM } from '../utils/construct-cssom.js'; 6 | import { fetch } from '../utils/fetch.js'; 7 | import { listenForChange } from '../utils/listen-for-change.js'; 8 | import { writeSiteFile } from '../utils/write-site-file.js'; 9 | import { readFile, writeFile } from 'fs/promises'; 10 | import { resolve } from 'path'; 11 | 12 | let currentModified; 13 | 14 | // TODO: Reinstate server test, removed during further Windows compat changes 15 | test.skip(); 16 | 17 | test.before(async () => { 18 | const { lastModified } = await fetch('/'); 19 | currentModified = lastModified; 20 | }); 21 | 22 | const files = { 'index.html': null, 'index.css': null, 'index.js': null }; 23 | 24 | test.before(async () => { 25 | await buildSite('server'); 26 | }); 27 | 28 | test.before.each(async () => { 29 | for (const file in files) { 30 | files[file] = await readFile(resolve(`fixtures/server/src/${file}`)); 31 | } 32 | }); 33 | 34 | test.after.each(async () => { 35 | for (const file in files) { 36 | await writeFile(resolve(`fixtures/server/src/${file}`), files[file]); 37 | files[file] = null; 38 | } 39 | }); 40 | 41 | const edited = 'I was edited and reloaded by PRPL server'; 42 | 43 | test('should update if a source HTML file is changed', async () => { 44 | const file = 'index.html'; 45 | 46 | const { document: srcDom } = await constructDOM({ src: `server/src/${file}` }); 47 | srcDom.querySelector('h1').textContent = edited; 48 | await writeSiteFile({ target: `server/src/${file}`, om: srcDom }); 49 | 50 | const { changed, data: html } = await listenForChange('/', currentModified); 51 | assert.ok(changed); 52 | 53 | const { document: serverDom } = await constructDOM({ src: html, type: 'string' }); 54 | assert.equal(serverDom.querySelector('h1').textContent, edited); 55 | }); 56 | 57 | test('should update if a source CSS file is changed', async () => { 58 | const file = 'index.css'; 59 | const color = 'blue'; 60 | const cssRule = `h1 {color: ${color} !important;}`; 61 | 62 | const srcCssom = await constructCSSOM({ src: `server/src/${file}` }); 63 | srcCssom.insertRule(cssRule); 64 | await writeSiteFile({ target: `server/src/${file}`, om: srcCssom, type: 'css' }); 65 | 66 | const { changed, data: css } = await listenForChange(`/${file}`, currentModified); 67 | assert.ok(changed); 68 | 69 | const serverCssom = await constructCSSOM({ src: css, type: 'string' }); 70 | const [editedServerRule] = serverCssom.cssRules; 71 | assert.equal(editedServerRule.cssText, cssRule); 72 | }); 73 | 74 | test('should update if a source JS file is changed', async () => { 75 | const file = 'index.js'; 76 | const contents = `document.querySelector('p').textContent = 'I was updated by JavaScript twice';`; 77 | 78 | await writeFile(resolve(`fixtures/server/src/${file}`), contents); 79 | 80 | const { changed, data: js } = await listenForChange(`/${file}`, currentModified); 81 | assert.ok(changed); 82 | assert.equal(js, contents); 83 | }); 84 | 85 | test.run(); 86 | -------------------------------------------------------------------------------- /tests/tests/sitemap.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | 6 | const origin = 'http://localhost:8000'; 7 | 8 | const slugs = { 9 | notes: 'notes', 10 | noteA: 'notes/a', 11 | noteB: 'notes/b', 12 | pluginCSSImports: 'plugin-css-imports', 13 | pluginHTMLimports: 'plugin-html-imports' 14 | }; 15 | 16 | let document; 17 | 18 | test.before(async () => { 19 | await buildSite('plugins'); 20 | const { document: sitemapDocument } = await constructDOM({ 21 | src: 'plugins/dist/sitemap.xml', 22 | mimeType: 'text/xml' 23 | }); 24 | document = sitemapDocument; 25 | }); 26 | 27 | test('should output a list of urls', () => { 28 | const urls = Array.from(document.querySelectorAll('urlset > url')).reduce((acc, curr) => { 29 | const uri = curr.querySelector('loc').textContent; 30 | acc[uri] = uri; 31 | return acc; 32 | }, {}); 33 | 34 | for (const slug in slugs) { 35 | assert.equal(urls[`${origin}/${slugs[slug]}`], `${origin}/${slugs[slug]}`); 36 | } 37 | }); 38 | 39 | test.run(); 40 | -------------------------------------------------------------------------------- /tests/tests/tagless-files.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { buildSite } from '../utils/build-site.js'; 4 | import { constructDOM } from '../utils/construct-dom.js'; 5 | 6 | let document; 7 | 8 | test.before(async () => { 9 | await buildSite('core'); 10 | const { document: indexDocument } = await constructDOM({ src: 'core/dist/index.html' }); 11 | document = indexDocument; 12 | }); 13 | 14 | test('should be copied without interpolation', () => { 15 | assert.is(document.querySelector('h1').textContent, 'PRPL Test Site'); 16 | }); 17 | 18 | test.run(); 19 | -------------------------------------------------------------------------------- /tests/utils/build-site.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, sep } from 'path'; 2 | import { execSync } from 'child_process'; 3 | import rimraf from 'rimraf'; 4 | 5 | // TODO: Pass in build script, for now use ESM script 6 | async function buildSite(site) { 7 | const siteDir = resolve(process.cwd(), 'fixtures', site); 8 | rimraf.sync(join(siteDir, 'dist')); 9 | const script = join('scripts', 'build.mjs'); 10 | execSync(`node ${script}`, { stdio: 'inherit', cwd: siteDir }); 11 | } 12 | 13 | export { buildSite }; 14 | -------------------------------------------------------------------------------- /tests/utils/construct-cssom.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readFile } from 'fs/promises'; 3 | import { parse as parseCSS } from 'cssom'; 4 | 5 | async function constructCSSOM({ src, type = 'file' }) { 6 | try { 7 | let css = src; 8 | 9 | if (type === 'file') { 10 | const buffer = await readFile(resolve(process.cwd(), 'fixtures', src)); 11 | css = buffer.toString(); 12 | } 13 | 14 | return parseCSS(css); 15 | } catch (error) { 16 | console.error(error); 17 | } 18 | } 19 | 20 | export { constructCSSOM }; 21 | -------------------------------------------------------------------------------- /tests/utils/construct-dom.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readFile } from 'fs/promises'; 3 | import { DOMParser, parseHTML } from 'linkedom'; 4 | 5 | async function constructDOM({ src, type = 'file', mimeType = 'text/html' }) { 6 | try { 7 | let data; 8 | 9 | if (type === 'file') { 10 | const buffer = await readFile(resolve(process.cwd(), 'fixtures', src)); 11 | data = buffer.toString(); 12 | } 13 | 14 | if (mimeType === 'text/html') { 15 | return parseHTML(data); 16 | } 17 | 18 | const parser = new DOMParser(); 19 | return { 20 | document: parser.parseFromString(data, mimeType) 21 | }; 22 | } catch (error) { 23 | console.error(error); 24 | } 25 | } 26 | export { constructDOM }; 27 | -------------------------------------------------------------------------------- /tests/utils/fetch.js: -------------------------------------------------------------------------------- 1 | import { request } from 'http'; 2 | 3 | async function fetch(path = '/') { 4 | const params = { 5 | method: 'GET', 6 | host: 'localhost', 7 | port: 8000, 8 | path 9 | }; 10 | 11 | return new Promise((resolve, reject) => { 12 | const req = request(params, (res) => { 13 | const { statusCode, headers } = res; 14 | const lastModified = new Date(headers['last-modified']).getTime(); 15 | 16 | if (statusCode < 200 || statusCode >= 300) { 17 | return reject(new Error(`Status code: ${statusCode}`)); 18 | } 19 | 20 | const data = []; 21 | 22 | res.on('data', (chunk) => { 23 | data.push(chunk); 24 | }); 25 | 26 | res.on('end', () => { 27 | resolve({ 28 | data: Buffer.concat(data).toString(), 29 | lastModified 30 | }); 31 | }); 32 | }); 33 | 34 | req.on('error', reject); 35 | 36 | req.end(); 37 | }); 38 | } 39 | 40 | export { fetch }; 41 | -------------------------------------------------------------------------------- /tests/utils/listen-for-change.js: -------------------------------------------------------------------------------- 1 | import { fetch } from './fetch.js'; 2 | import { wait } from './wait.js'; 3 | 4 | async function listenForChange(filePath, currentModified) { 5 | let changedAt; 6 | let changed = false; 7 | let data; 8 | 9 | for (let i = 0; i < 30; i++) { 10 | await wait(100); 11 | 12 | const { data: fetchedData, lastModified } = await fetch(filePath); 13 | 14 | if (currentModified !== lastModified) { 15 | changedAt = lastModified; 16 | changed = true; 17 | data = fetchedData; 18 | break; 19 | } 20 | } 21 | 22 | return { changed, changedAt, data }; 23 | } 24 | 25 | export { listenForChange }; 26 | -------------------------------------------------------------------------------- /tests/utils/wait.js: -------------------------------------------------------------------------------- 1 | function wait(ms) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | 5 | export { wait }; 6 | -------------------------------------------------------------------------------- /tests/utils/write-site-file.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises'; 2 | import { resolve } from 'path'; 3 | 4 | async function writeSiteFile({ target, om }) { 5 | const srcPath = resolve(process.cwd(), 'fixtures', target); 6 | await writeFile(srcPath, om.toString()); 7 | } 8 | 9 | export { writeSiteFile }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext", "DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "isolatedModules": false, 8 | "esModuleInterop": true, 9 | "sourceMap": false, 10 | "removeComments": true, 11 | "skipLibCheck": true, 12 | "declaration": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "allowUnreachableCode": false, 17 | "allowUnusedLabels": false, 18 | "baseUrl": ".", 19 | "rootDir": ".", 20 | "paths": { 21 | "@prpl/*": ["packages/*/dist"] 22 | }, 23 | "incremental": true 24 | }, 25 | "exclude": ["**/node_modules", "**/dist"] 26 | } 27 | --------------------------------------------------------------------------------