├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── CONTRIBUTING.md ├── LIBS.md ├── README.md ├── babel.config.js ├── distributions └── nuxt-press │ ├── README.md │ ├── nuxt.config.js │ ├── package.json │ ├── postinstall.js │ └── src │ └── index.js ├── docs ├── customize.md ├── en │ ├── customize.md │ ├── guide.md │ ├── index.md │ └── roadmap.md ├── nuxt.config.js ├── nuxt.press.css ├── nuxt.press.json └── pt-BR │ ├── customize.md │ ├── guide.md │ ├── index.md │ └── roadmap.md ├── examples ├── blog-posts │ ├── intro.md │ └── links.md ├── docs │ ├── en │ │ ├── customize.md │ │ ├── guide.md │ │ ├── index.md │ │ └── roadmap.md │ ├── pt-BR │ │ ├── customize.md │ │ ├── guide.md │ │ ├── index.md │ │ └── roadmap.md │ └── static │ │ ├── nuxt-press-cover.png │ │ └── nuxt-press.png ├── layouts │ └── other.vue ├── nuxt.config.js ├── nuxt.press.css ├── nuxt.press.json ├── pages │ ├── blog │ │ └── about.md │ ├── index.md │ └── subpage │ │ ├── index.md │ │ └── other.md ├── plugins │ └── my-component.js ├── press │ └── docs │ │ └── components │ │ └── home.vue ├── slides │ └── hello.md └── static │ ├── blog │ └── rss.xml │ └── rss.xml ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── blog │ ├── package.json │ ├── src │ │ ├── blueprint │ │ │ ├── api.js │ │ │ ├── components │ │ │ │ ├── entry.vue │ │ │ │ └── sidebar.tmpl.vue │ │ │ ├── data.js │ │ │ ├── index.js │ │ │ ├── layouts │ │ │ │ ├── blog.tmpl.vue │ │ │ │ └── head.tmpl.js │ │ │ ├── pages │ │ │ │ ├── archive.vue │ │ │ │ └── index.vue │ │ │ ├── source.js │ │ │ └── static │ │ │ │ └── rss.xml │ │ ├── index.js │ │ └── theme.css │ └── test │ │ ├── e2e │ │ ├── basic.test.js │ │ └── prefix.test.js │ │ ├── fixtures │ │ ├── basic │ │ │ ├── basic.test.js │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ ├── pages │ │ │ │ └── about.md │ │ │ ├── posts │ │ │ │ ├── intro.md │ │ │ │ └── links.md │ │ │ └── static │ │ │ │ └── rss.xml │ │ └── prefix │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ ├── posts │ │ │ ├── intro.md │ │ │ └── links.md │ │ │ ├── prefix.test.js │ │ │ └── static │ │ │ └── rss.xml │ │ └── unit │ │ ├── blueprint.generateRoutes.test.js │ │ ├── blueprint.routes.test.js │ │ ├── data.test.js │ │ └── source.test.js ├── cli │ ├── bin │ │ └── nuxt-press.js │ └── package.json ├── core │ ├── nuxt.config.js │ ├── package.json │ ├── src │ │ ├── autoregister.js │ │ ├── blueprint │ │ │ ├── api.js │ │ │ ├── components │ │ │ │ ├── nuxt-static.js │ │ │ │ ├── nuxt-template.js │ │ │ │ ├── observer.js │ │ │ │ └── press-link.js │ │ │ ├── index.js │ │ │ ├── middleware │ │ │ │ └── press.tmpl.js │ │ │ ├── pages │ │ │ │ └── source.tmpl.vue │ │ │ ├── plugins │ │ │ │ ├── press.tmpl.js │ │ │ │ └── scroll.client.js │ │ │ ├── source.js │ │ │ └── utils │ │ │ │ └── index.js │ │ ├── index.js │ │ └── prism.css │ └── test │ │ └── unit │ │ └── blueprint.routes.test.js ├── docs │ ├── package.json │ ├── src │ │ ├── blueprint │ │ │ ├── components │ │ │ │ ├── header.tmpl.vue │ │ │ │ ├── home.vue │ │ │ │ ├── nav-link.vue │ │ │ │ ├── outbound-link-icon.vue │ │ │ │ ├── sidebar-section.vue │ │ │ │ ├── sidebar-sections.vue │ │ │ │ ├── sidebar.vue │ │ │ │ └── topic.vue │ │ │ ├── data.js │ │ │ ├── index.js │ │ │ ├── layouts │ │ │ │ └── docs.vue │ │ │ ├── mixins │ │ │ │ └── docs.tmpl.js │ │ │ ├── plugins │ │ │ │ └── press.$tmpl.js │ │ │ ├── source.js │ │ │ └── utils │ │ │ │ └── index.js │ │ ├── index.js │ │ ├── sidebar.js │ │ └── theme.css │ └── test │ │ ├── e2e │ │ ├── basic.test.js │ │ ├── full.test.js │ │ ├── locales.test.js │ │ ├── prefix-locales.test.js │ │ └── prefix.test.js │ │ ├── fixtures │ │ ├── basic │ │ │ ├── a │ │ │ │ ├── README.md │ │ │ │ ├── first.md │ │ │ │ └── second.md │ │ │ ├── b-ext.md │ │ │ ├── b.md │ │ │ ├── basic.test.js │ │ │ ├── c.md │ │ │ ├── index.md │ │ │ ├── nuxt.config.js │ │ │ └── nuxt.press.js │ │ ├── full │ │ │ ├── docs-en │ │ │ │ └── index.md │ │ │ ├── docs-guide │ │ │ │ ├── en │ │ │ │ │ └── index.md │ │ │ │ └── nl │ │ │ │ │ └── index.md │ │ │ ├── docs-nl │ │ │ │ └── index.md │ │ │ ├── full.test.js │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ └── pages │ │ │ │ └── index.md │ │ ├── locales │ │ │ ├── en │ │ │ │ ├── guide.md │ │ │ │ └── index.md │ │ │ ├── locales.test.js │ │ │ ├── nl │ │ │ │ ├── guide.md │ │ │ │ └── index.md │ │ │ ├── nuxt.config.js │ │ │ └── nuxt.press.json │ │ ├── prefix-locales │ │ │ ├── en │ │ │ │ ├── guide.md │ │ │ │ └── index.md │ │ │ ├── nl │ │ │ │ ├── guide.md │ │ │ │ └── index.md │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ └── prefix-locales.test.js │ │ └── prefix │ │ │ ├── a │ │ │ ├── README.md │ │ │ ├── first.md │ │ │ └── second.md │ │ │ ├── b.md │ │ │ ├── c.md │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ ├── prefix.test.js │ │ │ └── readme.md │ │ └── unit │ │ ├── blueprint.generateRoutes.test.js │ │ ├── data.test.js │ │ └── sidebar.test.js ├── pages │ ├── package.json │ ├── src │ │ ├── blueprint.js │ │ ├── data.js │ │ └── index.js │ └── test │ │ ├── e2e │ │ └── basic.test.js │ │ ├── fixtures │ │ └── basic │ │ │ ├── basic.test.js │ │ │ ├── nuxt.config.js │ │ │ └── pages │ │ │ ├── about │ │ │ └── index.md │ │ │ ├── contact.md │ │ │ └── index.md │ │ └── unit │ │ ├── blueprint.generateRoutes.test.js │ │ └── data.test.js ├── slides │ ├── package.json │ ├── src │ │ ├── blueprint │ │ │ ├── api.js │ │ │ ├── components │ │ │ │ └── slides.vue │ │ │ ├── data.js │ │ │ ├── index.js │ │ │ ├── layouts │ │ │ │ └── slides.vue │ │ │ ├── pages │ │ │ │ └── index.vue │ │ │ ├── plugins │ │ │ │ └── slides.client.js │ │ │ └── svg │ │ │ │ ├── arrow-left.svg │ │ │ │ └── arrow-right.svg │ │ ├── index.js │ │ └── theme.css │ └── test │ │ ├── e2e │ │ ├── basic.test.js │ │ └── prefix.test.js │ │ ├── fixtures │ │ ├── basic │ │ │ ├── basic.test.js │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ └── presentation.md │ │ └── prefix │ │ │ ├── nuxt.config.js │ │ │ ├── nuxt.press.json │ │ │ ├── prefix.test.js │ │ │ └── presentation.md │ │ └── unit │ │ ├── blueprint.generateRoutes.test.js │ │ ├── blueprint.routes.test.js │ │ └── data.test.js └── utils │ ├── package.json │ ├── src │ ├── config.js │ ├── fs.js │ ├── index.js │ ├── jobs.js │ ├── module.js │ ├── normalize.js │ ├── pool.js │ ├── route.js │ ├── sse.js │ └── string.js │ └── test │ └── unit │ ├── normalize.test.js │ └── route.test.js ├── renovate.json ├── scripts ├── changelog.js ├── rollup.config.js └── workspace-run ├── test └── utils │ ├── blueprint.js │ ├── browser.js │ ├── build.js │ ├── index.js │ ├── nuxt.js │ └── setup.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/dist/** 3 | **/src/blueprint/** 4 | .nuxt 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "parser": "babel-eslint" 4 | }, 5 | "extends": [ 6 | "@nuxtjs" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sandbox 2 | 3 | # Dependencies 4 | node_modules 5 | 6 | # Lock files 7 | package-lock.json 8 | */**/yarn.lock 9 | 10 | # Logs 11 | *.log 12 | *.logs 13 | 14 | # Build/dist folders 15 | .nuxt* 16 | dist 17 | CHANGELOG.md 18 | 19 | # Coverage reports 20 | reports 21 | coverage 22 | *.lcov 23 | .nyc_output 24 | 25 | # Editors 26 | *.iml 27 | .idea 28 | .vscode 29 | 30 | # OSX 31 | .DS_Store 32 | .AppleDouble 33 | .LSOverride 34 | 35 | # Env vars 36 | .env* 37 | 38 | # Test files 39 | src/blueprints/blog/test/fixtures/*/static/rss.xml 40 | 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [introductory blog post](https://hire.jonasgalvez.com.br/2019/aug/19/the-story-of-nuxtpress/) to learn about the architectural decisions that are behind NuxtPress, including **_blueprint modules_** which are the mechanism used to register its built-in apps. 2 | 3 | Contributing is extremely easy, just pull the [nuxt/press repository](https://github.com/nuxt/press), install NPM dependencies and use the `dev` script to test your changes directly in the bundled examples. If you change something in `src/blueprints/blog`, you'll want to test with your changes with: 4 | 5 | ```shell 6 | $ npm run dev examples/blog 7 | ``` 8 | -------------------------------------------------------------------------------- /LIBS.md: -------------------------------------------------------------------------------- 1 | ## Useful libraries 2 | 3 | - https://github.com/egoist/vmark 4 | - https://github.com/nuxt/lmify 5 | - https://github.com/sapegin/mrm-core#json 6 | - https://github.com/nuxt-community/nuxt-tailwindcss 7 | - https://github.com/troxler/vue-headful 8 | - https://github.com/stevenbenisek/rollup-plugin-auto-external 9 | - https://github.com/Akryum/vue-observe-visibility 10 | - https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/markdown/lib/highlight.js 11 | - https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/markdown/lib/highlightLines.js 12 | - https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/plugin-active-header-links/clientRootMixin.js 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![nuxt-press](https://user-images.githubusercontent.com/904724/59497906-a2d9d680-8e94-11e9-8fac-a7172827f349.png) 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![Circle CI][circle-ci-src]][circle-ci-href] 6 | [![Codecov][codecov-src]][codecov-href] 7 | 8 | See documentation at **[https://nuxt.press](https://nuxt.press)**. 9 | 10 | # Publishing the Nuxt way 11 | 12 | **NuxtPress** is a **microframework** that leverages the [Nuxt module system][1]. 13 | 14 | [1]: https://nuxtjs.org/guide/modules/ 15 | 16 | ⚡Hot reloaded Markdown routes: `pages/foo/bar.md` → `/foo/bar` 17 | 18 | ⚡Deploy **static** with `nuxt generate` or **live** with a simple API. 19 | 20 | ⚡Built-in **docs**, **blog** and **slides** base apps. 21 | 22 | ⚡Flexible **Markdown processing** via [@nuxt/markdown][n-md]. 23 | 24 | ⚡Ejectable styles and templates (`nuxt-press eject `). 25 | 26 | [n-md]: https://github.com/nuxt/markdown 27 | 28 | ## Credits 29 | 30 | Created and maintained by [@galvez][galvez] and [@pimlie][pimlie] with the 31 | help of the **Nuxt Team**. 32 | 33 | [galvez]: https://github.com/galvez 34 | [pimlie]: https://github.com/pimlie 35 | 36 | ## Sponsoring 37 | 38 | Interested in **adding your logo here**? [Reach out][contact] for sponsoring details. 39 | 40 | [contact]: mailto:jonasgalvez@gmail.com 41 | 42 | 43 | [npm-version-src]: https://img.shields.io/npm/v/@nuxt/press/latest.svg?style=flat-square 44 | [npm-version-href]: https://npmjs.com/package/@nuxt/press 45 | 46 | [npm-downloads-src]: https://img.shields.io/npm/dt/@nuxt/press.svg?style=flat-square 47 | [npm-downloads-href]: https://npmjs.com/package/@nuxt/press 48 | 49 | [circle-ci-src]: https://img.shields.io/circleci/project/github/nuxt/press.svg?style=flat-square 50 | [circle-ci-href]: https://circleci.com/gh/nuxt/press 51 | 52 | [codecov-src]: https://img.shields.io/codecov/c/github/nuxt/press.svg?style=flat-square 53 | [codecov-href]: https://codecov.io/gh/nuxt/press 54 | 55 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'plugins': [ 3 | '@babel/plugin-proposal-class-properties', 4 | '@babel/plugin-syntax-dynamic-import' 5 | ], 6 | 'env': { 7 | 'test': { 8 | 'presets': [ 9 | [ '@babel/env', { 10 | 'targets': { 11 | 'node': 'current' 12 | } 13 | }] 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /distributions/nuxt-press/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /distributions/nuxt-press/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: ['@nuxt/press'] 3 | } 4 | -------------------------------------------------------------------------------- /distributions/nuxt-press/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt/press", 3 | "version": "0.2.0-beta.0", 4 | "description": "Minimalist Markdown Publishing for Nuxt.js", 5 | "main": "src/index.js", 6 | "repository": "https://github.com/nuxt/press", 7 | "license": "MIT", 8 | "contributors": [ 9 | { 10 | "name": "Jonas Galvez (@galvez)" 11 | }, 12 | { 13 | "name": "Pim (@pimlie)" 14 | } 15 | ], 16 | "files": [ 17 | "src", 18 | "postinstall.js" 19 | ], 20 | "keywords": [ 21 | "nuxt", 22 | "nuxtjs", 23 | "nuxtpress", 24 | "vue", 25 | "vuejs", 26 | "markdown", 27 | "docs", 28 | "documentation", 29 | "slides", 30 | "slideshow", 31 | "blogging" 32 | ], 33 | "scripts": { 34 | "dev": "nuxt dev", 35 | "build": "nuxt build", 36 | "start": "nuxt start", 37 | "postinstall": "node postinstall.js", 38 | "press": "nuxt press" 39 | }, 40 | "dependencies": { 41 | "@nuxt-press/blog": "0.2.0-beta.0", 42 | "@nuxt-press/cli": "0.2.0-beta.0", 43 | "@nuxt-press/docs": "0.2.0-beta.0", 44 | "@nuxt-press/pages": "0.2.0-beta.0", 45 | "@nuxt-press/slides": "0.2.0-beta.0", 46 | "consola": "^2.10.1" 47 | }, 48 | "peerDependencies": { 49 | "nuxt": "^2.10.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /distributions/nuxt-press/postinstall.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync, writeFileSync } = require('fs') 2 | const { join, resolve } = require('path') 3 | 4 | const scripts = { 5 | 'dev': 'nuxt dev', 6 | 'build': 'nuxt build', 7 | 'start': 'nuxt start', 8 | 'press': 'nuxt press' 9 | } 10 | 11 | main() 12 | 13 | function updatePackageJson (initCwd) { 14 | if (!existsSync(join(initCwd, 'package.json'))) { 15 | writeFileSync( 16 | join(initCwd, 'package.json'), 17 | JSON.stringify({ scripts }, null, 2) 18 | ) 19 | return 20 | } 21 | let packageJson 22 | try { 23 | packageJson = JSON.parse( 24 | readFileSync(join(initCwd, 'package.json')).toString() 25 | ) 26 | } catch (_) { 27 | packageJson = {} 28 | } 29 | if (!packageJson.scripts) { 30 | packageJson.scripts = {} 31 | for (const script in scripts) { 32 | if (!packageJson.scripts[script]) { 33 | packageJson.scripts[script] = scripts[script] 34 | } 35 | } 36 | writeFileSync( 37 | join(initCwd, 'package.json'), 38 | JSON.stringify(packageJson, null, 2) 39 | ) 40 | } 41 | } 42 | 43 | function writeNuxtConfig (initCwd) { 44 | const nuxtConfig = join(initCwd, 'nuxt.config.js') 45 | const nuxtConfigTS = join(initCwd, 'nuxt.config.ts') 46 | if (!existsSync(nuxtConfig) && !existsSync(nuxtConfigTS)) { 47 | writeFileSync( 48 | nuxtConfig, 49 | 'export default {\n' + 50 | ' modules: [\'@nuxt/press\']\n' + 51 | '}\n' 52 | ) 53 | } 54 | } 55 | 56 | function main () { 57 | if (!process.env.INIT_CWD) { 58 | return 59 | } 60 | 61 | const initCwd = resolve(process.env.INIT_CWD) 62 | 63 | if (!existsSync(join(initCwd, 'node_modules'))) { 64 | return 65 | } 66 | 67 | // Detect common presence of docs/nuxt.config.js or docs/nuxt.config.ts 68 | if (existsSync(join(initCwd, 'docs', 'nuxt.config.js'))) { 69 | return 70 | } 71 | 72 | if (existsSync(join(initCwd, 'docs', 'nuxt.config.ts'))) { 73 | return 74 | } 75 | 76 | updatePackageJson(initCwd) 77 | writeNuxtConfig(initCwd) 78 | } 79 | -------------------------------------------------------------------------------- /distributions/nuxt-press/src/index.js: -------------------------------------------------------------------------------- 1 | import { autoregister } from '@nuxt-press/core' 2 | 3 | export default autoregister 4 | -------------------------------------------------------------------------------- /docs/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: NuxtPress 4 | heroImage: /nuxt-press.png 5 | heroText: NuxtPress 6 | tagline: Publishing the Nuxt way 7 | actionText: Get Started → 8 | actionLink: /en/guide/ 9 | footer: MIT License 10 | --- 11 | 12 |
13 | 14 | ⚡Hot reloaded Markdown routes: `pages/foo/bar.md` → `/foo/bar` 15 | 16 | ⚡Deploy **static** with `nuxt generate` or **live** with a simple API. 17 | 18 | ⚡Built-in **docs**, **blog** and **slides** pluggable apps. 19 | 20 | ⚡Flexible **Markdown processing** via [@nuxt/markdown][n-md]. 21 | 22 | ⚡Ejectable styles and templates (`nuxt-press eject `). 23 | 24 | [n-md]: https://github.com/nuxt/markdown 25 | 26 |
27 | -------------------------------------------------------------------------------- /docs/en/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarSkipLevel: 2 3 | --- 4 | 5 | # Roadmap 6 | 7 | Althought NuxtPress' first release packs many useful features, we've got a much bigger vision for it. We've have planned to roll out a series of refactorings and features leading up to its 1.0 release a year from now, i.e., one major release per month as a general goal. 8 | 9 | ## v0.2 10 | 11 | - Core: 12 | - add basic support for multiple ids/configs per blueprint 13 | - template definitions can be a function 14 | 15 | - Blueprints/Docs: 16 | - support dynamic loaded configs per locale 17 | - support multiple folders for a blueprint 18 | - support multiple ids per blueprint 19 | - define sidebar items with glob/regex 20 | - add custom (ie non nuxt/press page) items to sidebar 21 | - support source folder aliasing 22 | 23 | > Work on these [has already taken place](https://github.com/nuxt/press/pull/44) and should be released soon. 24 | 25 | - I18n 26 | - Complete docs translation to Brazilian portuguese 27 | 28 | ## v0.3 29 | 30 | - Core: 31 | - Register webpack assets for sources when fully using filesystem 32 | - Maintain ability to override this with press/common middleware/sources 33 | 34 | ## v0.4 35 | 36 | - Refactor blueprint.js into Nuxt Core (RFC pending) 37 | 38 | - Blueprints/Common: 39 | - Refactor press/common into @nuxt/markdown using Nuxt Core blueprints 40 | - Refactor docs/blogs/slides using Nuxt Core blueprints 41 | -------------------------------------------------------------------------------- /docs/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [ 5 | [NuxtPress, 'docs'] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /docs/nuxt.press.css: -------------------------------------------------------------------------------- 1 | #nuxt-press.docs .content:not(.has-sidebar) { 2 | background: inherit; 3 | } 4 | 5 | #nuxt-press.docs .home { 6 | background: #fff; 7 | } 8 | 9 | .codefund-wrapper { 10 | padding-bottom: 2rem; 11 | } -------------------------------------------------------------------------------- /docs/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "dir": "", 4 | "prefix": "/", 5 | "title": "NuxtPress", 6 | "search": true, 7 | "localizedSidebar": false, 8 | "configPerLocale": false, 9 | "nav": [ 10 | { 11 | "Guide": "https://nuxt.press" 12 | }, 13 | { 14 | "GitHub": "https://github.com/nuxt/press" 15 | } 16 | ], 17 | "image": "https://nuxt.press/nuxt-press-cover.png", 18 | "sidebar": [ 19 | "/guide", 20 | "/customize", 21 | "/roadmap" 22 | ], 23 | "locales": [ 24 | { 25 | "code": "en", 26 | "name": "English" 27 | }, 28 | { 29 | "code": "pt-BR", 30 | "name": "Português (BR)" 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/pt-BR/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: NuxtPress 4 | heroImage: /nuxt-press.png 5 | heroText: NuxtPress 6 | tagline: Publishing the Nuxt way 7 | actionText: Get Started → 8 | actionLink: /en/guide/ 9 | footer: MIT License 10 | --- 11 | 12 |
13 | 14 | ⚡Rotas Markdown com hot reload: `pages/foo/bar.md` → `/foo/bar` 15 | 16 | ⚡Deploy **static** com `nuxt generate` ou **live** com uma API simples. 17 | 18 | ⚡Inclui apps **docs**, **blog** and **slides** plugáveis. 19 | 20 | ⚡**Processamento Markdown** flexível via [@nuxt/markdown][n-md]. 21 | 22 | ⚡Estilos e templates ejetáveis (`nuxt-press eject `). 23 | 24 | [n-md]: https://github.com/nuxt/markdown 25 | 26 |
27 | -------------------------------------------------------------------------------- /docs/pt-BR/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarSkipLevel: 2 3 | --- 4 | 5 | # Roadmap 6 | 7 | Althought NuxtPress' first release packs many useful features, we've got a much bigger vision for it. We've have planned to roll out a series of refactorings and features leading up to its 1.0 release a year from now, i.e., one major release per month as a general goal. 8 | 9 | ## v0.2 10 | 11 | - Core: 12 | - add basic support for multiple ids/configs per blueprint 13 | - template definitions can be a function 14 | 15 | - Blueprints/Docs: 16 | - support dynamic loaded configs per locale 17 | - support multiple folders for a blueprint 18 | - support multiple ids per blueprint 19 | - define sidebar items with glob/regex 20 | - add custom (ie non nuxt/press page) items to sidebar 21 | - support source folder aliasing 22 | 23 | > Work on these [has already taken place](https://github.com/nuxt/press/pull/44) and should be released soon. 24 | 25 | - I18n 26 | - Complete docs translation to Brazilian portuguese 27 | 28 | ## v0.3 29 | 30 | - Core: 31 | - Register webpack assets for sources when fully using filesystem 32 | - Maintain ability to override this with press/common middleware/sources 33 | 34 | ## v0.4 35 | 36 | - Refactor blueprint.js into Nuxt Core (RFC pending) 37 | 38 | - Blueprints/Common: 39 | - Refactor press/common into @nuxt/markdown using Nuxt Core blueprints 40 | - Refactor docs/blogs/slides using Nuxt Core blueprints 41 | -------------------------------------------------------------------------------- /examples/blog-posts/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: April 20, 2019 3 | --- 4 | 5 | # Publishing blogs 6 | 7 | > This content comes from the `docs` suite. It used here to exemplify a blog entry from **April 20, 2019**. 8 | 9 | Let's add a new paragraph. OMG it works. 10 | 11 | To publish a blog, add your Markdown files to the `blog/` folder. You can 12 | structure them in however many subdirectories you want (for grouping posts by 13 | year of publication for, for instance). What determines the publishing date of 14 | each blog entry is actually their Markdown source header. 15 | 16 | By default, NuxtPress uses a simple format where the first line is parsed out 17 | as the publication date. **Titles** and **slugs** are automatically generated 18 | from the first heading (`#`) of your Markdown sources: 19 | 20 | ``` 21 | June 20, 2019 22 | 23 | # Blog Entry's Title 24 | 25 | ``` 26 | 27 | If your Markdown sources however start with a `---`, NuxtPress will try and 28 | parse it via [gray-matter][gm] and will look for `title`, `slug` and `date`. 29 | 30 | [gm]: https://github.com/jonschlinkert/gray-matter 31 | 32 | ```markup 33 | --- 34 | title: Blog Entry's Title 35 | date: June 20, 2019 36 | slug: blog-entry-slug 37 | --- 38 | 39 | # This Heading Is Not Used As Title 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/blog-posts/links.md: -------------------------------------------------------------------------------- 1 | June 20, 2019 2 | 3 | # Adding sidebar links 4 | 5 | > This content comes from the `docs` suite. It used here to exemplify a blog entry from **June 20, 2019**. 6 | 7 | NuxtPress' default blog template makes it easy to automatically include sidebar text links. 8 | 9 | Here's `nuxt.press.json` from [`examples/blog`][examples-blog]. 10 | 11 | [examples-blog]: https://github.com/nuxt/press/tree/master/examples/blog 12 | 13 | ```json 14 | { 15 | "blog": { 16 | "links": [ 17 | {"Home": "/blog"}, 18 | {"Archive": "/blog/archive"}, 19 | {"About": "/blog/about"}, 20 | ] 21 | } 22 | } 23 | ``` 24 | 25 | This feature is mostly illustrative. You're likely to benefit more from ejecting 26 | the entire app bundle and adding your code for the `sidebar` component. Your 27 | component can still have access to these options you define under the `blog` 28 | configuration key in `nuxt.press.json` or `nuxt.press.js`, making it extremely 29 | to customize templates even with your own configuration options. 30 | -------------------------------------------------------------------------------- /examples/docs/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: NuxtPress 4 | heroImage: /nuxt-press.png 5 | heroText: NuxtPress 6 | tagline: Publishing the Nuxt way 7 | actionText: Get Started → 8 | actionLink: /en/guide/ 9 | footer: MIT License 10 | --- 11 | 12 |
13 | 14 | ⚡Hot reloaded Markdown routes: `pages/foo/bar.md` → `/foo/bar` 15 | 16 | ⚡Deploy **static** with `nuxt generate` or **live** with a simple API. 17 | 18 | ⚡Built-in **docs**, **blog** and **slides** pluggable apps. 19 | 20 | ⚡Flexible **Markdown processing** via [@nuxt/markdown][n-md]. 21 | 22 | ⚡Ejectable styles and templates (`nuxt-press eject `). 23 | 24 | [n-md]: https://github.com/nuxt/markdown 25 | 26 |
27 | -------------------------------------------------------------------------------- /examples/docs/en/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarSkipLevel: 2 3 | --- 4 | 5 | # Roadmap 6 | 7 | Althought NuxtPress' first release packs many useful features, we've got a much bigger vision for it. We've have planned to roll out a series of refactorings and features leading up to its 1.0 release a year from now, i.e., one major release per month as a general goal. 8 | 9 | ## v0.2 10 | 11 | - Core: 12 | - add basic support for multiple ids/configs per blueprint 13 | - template definitions can be a function 14 | 15 | - Blueprints/Docs: 16 | - support dynamic loaded configs per locale 17 | - support multiple folders for a blueprint 18 | - support multiple ids per blueprint 19 | - define sidebar items with glob/regex 20 | - add custom (ie non nuxt/press page) items to sidebar 21 | - support source folder aliasing 22 | 23 | > Work on these [has already taken place](https://github.com/nuxt/press/pull/44) and should be released soon. 24 | 25 | - I18n 26 | - Complete docs translation to Brazilian portuguese 27 | 28 | ## v0.3 29 | 30 | - Core: 31 | - Register webpack assets for sources when fully using filesystem 32 | - Maintain ability to override this with press/common middleware/sources 33 | 34 | ## v0.4 35 | 36 | - Refactor blueprint.js into Nuxt Core (RFC pending) 37 | 38 | - Blueprints/Common: 39 | - Refactor press/common into @nuxt/markdown using Nuxt Core blueprints 40 | - Refactor docs/blogs/slides using Nuxt Core blueprints 41 | -------------------------------------------------------------------------------- /examples/docs/pt-BR/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: NuxtPress 4 | heroImage: /nuxt-press.png 5 | heroText: NuxtPress 6 | tagline: Publishing the Nuxt way 7 | actionText: Get Started → 8 | actionLink: /en/guide/ 9 | footer: MIT License 10 | --- 11 | 12 |
13 | 14 | ⚡Rotas Markdown com hot reload: `pages/foo/bar.md` → `/foo/bar` 15 | 16 | ⚡Deploy **static** com `nuxt generate` ou **live** com uma API simples. 17 | 18 | ⚡Inclui apps **docs**, **blog** and **slides** plugáveis. 19 | 20 | ⚡**Processamento Markdown** flexível via [@nuxt/markdown][n-md]. 21 | 22 | ⚡Estilos e templates ejetáveis (`nuxt-press eject `). 23 | 24 | [n-md]: https://github.com/nuxt/markdown 25 | 26 |
27 | -------------------------------------------------------------------------------- /examples/docs/pt-BR/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarSkipLevel: 2 3 | --- 4 | 5 | # Roadmap 6 | 7 | Althought NuxtPress' first release packs many useful features, we've got a much bigger vision for it. We've have planned to roll out a series of refactorings and features leading up to its 1.0 release a year from now, i.e., one major release per month as a general goal. 8 | 9 | ## v0.2 10 | 11 | - Core: 12 | - add basic support for multiple ids/configs per blueprint 13 | - template definitions can be a function 14 | 15 | - Blueprints/Docs: 16 | - support dynamic loaded configs per locale 17 | - support multiple folders for a blueprint 18 | - support multiple ids per blueprint 19 | - define sidebar items with glob/regex 20 | - add custom (ie non nuxt/press page) items to sidebar 21 | - support source folder aliasing 22 | 23 | > Work on these [has already taken place](https://github.com/nuxt/press/pull/44) and should be released soon. 24 | 25 | - I18n 26 | - Complete docs translation to Brazilian portuguese 27 | 28 | ## v0.3 29 | 30 | - Core: 31 | - Register webpack assets for sources when fully using filesystem 32 | - Maintain ability to override this with press/common middleware/sources 33 | 34 | ## v0.4 35 | 36 | - Refactor blueprint.js into Nuxt Core (RFC pending) 37 | 38 | - Blueprints/Common: 39 | - Refactor press/common into @nuxt/markdown using Nuxt Core blueprints 40 | - Refactor docs/blogs/slides using Nuxt Core blueprints 41 | -------------------------------------------------------------------------------- /examples/docs/static/nuxt-press-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/press/ba4c633e1cc721e04c30a0f8b5f3cc92e763bd28/examples/docs/static/nuxt-press-cover.png -------------------------------------------------------------------------------- /examples/docs/static/nuxt-press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/press/ba4c633e1cc721e04c30a0f8b5f3cc92e763bd28/examples/docs/static/nuxt-press.png -------------------------------------------------------------------------------- /examples/layouts/other.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /examples/nuxt.press.css: -------------------------------------------------------------------------------- 1 | #nuxt-press.docs .content:not(.has-sidebar) { 2 | background: inherit; 3 | } 4 | 5 | #nuxt-press.docs .home { 6 | background: #fff; 7 | } 8 | 9 | .codefund-wrapper { 10 | padding-bottom: 2rem; 11 | } -------------------------------------------------------------------------------- /examples/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "blog": { 3 | "dir": "blog-posts", 4 | "prefix": "/blog", 5 | "title": "A NuxtPress Blog", 6 | "links": [ 7 | { 8 | "Home": "/blog/" 9 | }, 10 | { 11 | "Archive": "/blog/archive/" 12 | }, 13 | { 14 | "About": "/blog/about" 15 | } 16 | ], 17 | "icons": [ 18 | { 19 | "github": "http://github.com/nuxt/nuxt.js" 20 | }, 21 | { 22 | "twitter": "https://twitter.com/nuxt_js" 23 | } 24 | ], 25 | "feed": { 26 | "link": "https://nuxt.press/", 27 | "description": "A NuxtPress Blog Description", 28 | "tagDomain": "nuxt.press", 29 | "path": "/rss.xml" 30 | }, 31 | "tagDomain": "nuxt.press" 32 | }, 33 | "slides": { 34 | "dir": "slides", 35 | "prefix": "/slides/" 36 | }, 37 | "docs": { 38 | "dir": "docs", 39 | "prefix": "/docs/", 40 | "title": "NuxtPress", 41 | "search": true, 42 | "localizedSidebar": false, 43 | "configPerLocale": false, 44 | "locales": [ 45 | { 46 | "code": "en", 47 | "name": "English" 48 | }, 49 | { 50 | "code": "pt-BR", 51 | "name": "Português (BR)" 52 | } 53 | ], 54 | "nav": [ 55 | { 56 | "Home": "/" 57 | }, 58 | { 59 | "Blog": "/blog" 60 | }, 61 | { 62 | "Slides": "/slides" 63 | } 64 | ], 65 | "image": "https://nuxt.press/nuxt-press-cover.png", 66 | "sidebar": [ 67 | "/guide", 68 | "/customize", 69 | "/roadmap" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/pages/blog/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello **world**! 3 | layout: blog 4 | --- 5 | 6 | # About 7 | 8 | Hey, this works! 9 | -------------------------------------------------------------------------------- /examples/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | someText: hey, this works 3 | someOtherText: hey, this works too 4 | --- 5 | 6 | # Hello world 7 | 8 | Go to [/subpage](/subpage) 9 | 10 | Go to [/subpage/other](/subpage/other) 11 | 12 | Msg: {{ $press.source.someText }} 13 | 14 | Msg: **{{ $press.source.someOtherText }}** -------------------------------------------------------------------------------- /examples/pages/subpage/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: other 3 | --- 4 | 5 | Hello from **/subpage**. 6 | 7 | Go to [/](/) 8 | 9 | Go to [/subpage/other](/subpage/other) 10 | -------------------------------------------------------------------------------- /examples/pages/subpage/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: other 3 | --- 4 | 5 | Hello from **/subpage/other**. 6 | 7 | Go to [/subpage](/subpage) 8 | 9 | Go to [/](/) 10 | -------------------------------------------------------------------------------- /examples/plugins/my-component.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.component('my-component', { 4 | template: '
This comes from a custom vue component
' 5 | }) 6 | -------------------------------------------------------------------------------- /examples/press/docs/components/home.vue: -------------------------------------------------------------------------------- 1 | // Credit: this component is largely adapted 2 | // from VuePress to maintain commonality 3 | 4 | 67 | 68 | 114 | -------------------------------------------------------------------------------- /examples/slides/hello.md: -------------------------------------------------------------------------------- 1 | A NuxtPress
2 | Presentation 3 | 4 | # It's a simple idea 5 | 6 | Let's take a Markdown file and use
7 | the `

` headers as slide delimiters 8 | 9 | # 10 | 11 | ```md 12 | Opener 13 | 14 | # Slide 2 15 | 16 | - Bullet point 17 | - Bullet pint 18 | 19 | # Slide 3 20 | 21 | ... 22 | ``` 23 | 24 | 27 | 28 | # Then 29 | 30 | We use Vue's [full build][vfb] to
31 | enable runtime **template compilation**. 32 | 33 | [vfb]: https://vuejs.org/v2/guide/installation.html#Explanation-of-Different-Builds 34 | 35 | # So that 36 | 37 | We can have ` 55 | 56 | # Noticed the background? 57 | 58 | ```html 59 | 64 | ``` 65 | 66 | Yep, that's **inlined**. 67 | -------------------------------------------------------------------------------- /examples/static/blog/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A NuxtPress Blog 5 | https://nuxt.press 6 | A NuxtPress Blog Description 7 | 8 | https://nuxt.press/blog/2019/jun/20/adding-sidebar-links/ 9 | tag:nuxt.press,2019:undefined 10 | Thu Jun 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 11 | Adding sidebar links 12 | 13 | 14 | https://nuxt.press/blog/2019/apr/20/publishing-blogs/ 15 | tag:nuxt.press,2019:undefined 16 | Sat Apr 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 17 | Publishing blogs 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/static/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A NuxtPress Blog 5 | https://nuxt.press/ 6 | A NuxtPress Blog Description 7 | 8 | https://nuxt.press//blog/2019/jun/20/adding-sidebar-links/ 9 | tag:nuxt.press,2019:undefined 10 | Thu Jun 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 11 | Adding sidebar links 12 | 13 | 14 | https://nuxt.press//blog/2019/apr/20/publishing-blogs/ 15 | tag:nuxt.press,2019:undefined 16 | Sat Apr 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 17 | Publishing blogs 18 | 19 | 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const stdEnv = require('std-env') 2 | 3 | module.exports = { 4 | testEnvironment: 'node', 5 | 6 | expand: true, 7 | 8 | forceExit: true, 9 | 10 | setupFilesAfterEnv: ['./test/utils/setup'], 11 | 12 | coverageDirectory: './coverage', 13 | 14 | collectCoverageFrom: [ 15 | 'packages/**/*.js', 16 | '!**/blueprint/*/**', 17 | '!**/test/**' 18 | ], 19 | 20 | moduleNameMapper: { 21 | "test-utils(.*)$": "/test/utils$1", 22 | // TODO: enable this again when we re-introduce a build step 23 | "^pressModule$": false && stdEnv.ci ? "/" : "/distributions/nuxt-press/src" 24 | }, 25 | 26 | transform: { 27 | '^.+\\.js$': 'babel-jest', 28 | '^.+\\.vue$': 'vue-jest' 29 | }, 30 | 31 | moduleFileExtensions: [ 32 | 'js', 33 | 'json' 34 | ], 35 | 36 | reporters: [ 37 | 'default', 38 | // ['jest-junit', { outputDirectory: 'reports/junit' }] 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0-beta.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "conventionalCommits": true, 6 | "exact": true, 7 | "packages": [ 8 | "packages/*", 9 | "distributions/*" 10 | ], 11 | "command": { 12 | "publish": { 13 | "npmClient": "npm" 14 | }, 15 | "init": { 16 | "exact": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-press", 3 | "private": true, 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/nuxt/press.git" 7 | }, 8 | "workspaces": [ 9 | "packages/*", 10 | "distributions/*" 11 | ], 12 | "license": "MIT", 13 | "scripts": { 14 | "clean": "rimraf ./**/.nuxt packages/**/dist test/**/dist packages/**/node_modules", 15 | "coverage": "codecov", 16 | "dev": "nuxt dev", 17 | "build": "nuxt build", 18 | "dist": "rimraf dist && rollup -c rollup.config.js && rimraf dist/blueprints/*/test/", 19 | "generate": "nuxt generate", 20 | "start": "nuxt start", 21 | "lint": "eslint --fix --ext .js packages distributions test", 22 | "postinstall": "lerna link", 23 | "test": "yarn test:unit && yarn test:fixtures && yarn test:e2e", 24 | "test:unit": "jest test/unit", 25 | "test:e2e": "jest packages/*/test/e2e", 26 | "test:fixtures": "jest packages/*/test/fixtures" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.6.4", 30 | "@babel/plugin-proposal-class-properties": "^7.5.5", 31 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 32 | "@babel/preset-env": "^7.6.3", 33 | "@nuxtjs/eslint-config": "^1.1.2", 34 | "babel-eslint": "^10.0.3", 35 | "babel-jest": "^24.9.0", 36 | "babel-plugin-dynamic-import-node": "^2.3.0", 37 | "browserstack-local": "^1.4.2", 38 | "chromedriver": "^77.0.0", 39 | "codecov": "^3.6.1", 40 | "eslint": "^6.5.1", 41 | "eslint-config-standard": "^14.1.0", 42 | "eslint-plugin-import": "^2.18.2", 43 | "eslint-plugin-jest": "^22.19.0", 44 | "eslint-plugin-node": "^10.0.0", 45 | "eslint-plugin-promise": "^4.2.1", 46 | "eslint-plugin-standard": "^4.0.1", 47 | "eslint-plugin-vue": "^5.2.3", 48 | "exit": "^0.1.2", 49 | "geckodriver": "^1.19.0", 50 | "get-port": "^5.0.0", 51 | "jest": "^24.9.0", 52 | "lerna": "^3.18.1", 53 | "node-env-file": "^0.1.8", 54 | "nuxt": "^2.10.1", 55 | "puppeteer-core": "^1.20.0", 56 | "rimraf": "^3.0.0", 57 | "rollup": "^1.25.1", 58 | "rollup-plugin-auto-external": "^2.0.0", 59 | "rollup-plugin-commonjs": "^10.1.0", 60 | "rollup-plugin-copy": "^3.1.0", 61 | "selenium-webdriver": "^4.0.0-alpha.4", 62 | "std-env": "^2.2.1", 63 | "tib": "^0.7.2", 64 | "vue-jest": "^3.0.5" 65 | }, 66 | "dependencies": { 67 | "hable": "3.0.0-0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/blog", 3 | "version": "0.2.0-beta.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/nuxt/press", 6 | "license": "MIT", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@nuxt-press/core": "0.2.0-beta.0", 12 | "lodash": "^4.17.15" 13 | }, 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "scripts": { 18 | "dev": "nuxt dev", 19 | "build": "nuxt build", 20 | "start": "nuxt start", 21 | "press": "nuxt press" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/api.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readJsonSync } from 'fs-extra' 3 | 4 | const cache = {} 5 | 6 | export default function blogApi ({ rootDir, id, prefix, dev }) { 7 | return { 8 | index: (req, res, next) => { 9 | if (dev || !cache.index) { 10 | cache.index = readJsonSync(path.join(rootDir, id, prefix, 'index.json')) 11 | } 12 | 13 | res.json(cache.index) 14 | }, 15 | archive: (req, res, next) => { 16 | if (dev || !cache.archive) { 17 | cache.archive = readJsonSync(path.join(rootDir, id, prefix, 'archive.json')) 18 | } 19 | 20 | res.json(cache.archive) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/components/entry.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/components/sidebar.tmpl.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/layouts/blog.tmpl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/layouts/head.tmpl.js: -------------------------------------------------------------------------------- 1 | export default { 2 | htmlAttrs: { 3 | class: 'blog' 4 | }, 5 | meta: [ 6 | { charset: 'utf-8' }, 7 | { name: 'viewport', content: 'width=device-width, maximum-scale=1.0, minimum-scale=1.0, initial-scale=1' }, 8 | { rel: 'alternate', title: 'RSS Feed', type: 'application/rss+xml', href: `<%= options.options.feed.link %><%= options.options.feed.path %>` }, 9 | { property: 'og:site_name', content: `<%= options.options.title %>` } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/pages/archive.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/pages/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/source.js: -------------------------------------------------------------------------------- 1 | import Markdown from '@nuxt/markdown' 2 | import graymatter from 'gray-matter' 3 | import { slugify } from '@nuxt-press/utils' 4 | 5 | const source = { 6 | processor () { 7 | return new Markdown({ 8 | toc: false, 9 | sanitize: false 10 | }) 11 | }, 12 | markdown (source, processor) { 13 | return processor.toMarkup(source).then(({ html }) => html) 14 | }, 15 | 16 | // metadata() parses the starting block of text in a Markdown source, 17 | // considering the first and (optionally) second lines as 18 | // publishing date and summary respectively 19 | metadata (source, fileName) { 20 | if (source.trimLeft().startsWith('---')) { 21 | const { content, data: meta } = graymatter(source) 22 | if (meta.date) { 23 | meta.published = new Date(Date.parse(meta.date)) 24 | } 25 | delete meta.date 26 | 27 | return { 28 | content, 29 | meta 30 | } 31 | } 32 | 33 | let published 34 | published = source.substr(0, source.indexOf('#')).trim() 35 | published = Date.parse(published) 36 | if (isNaN(published)) { 37 | throw new Error(`Missing or invalid publication date in ${fileName} -- see documentation at https://nuxt.press`) 38 | } 39 | return { 40 | meta: { 41 | published: new Date(published) 42 | } 43 | } 44 | }, 45 | 46 | // path() determines the final URL path of a Markdown source 47 | // In `blog` mode, the default format is /YYYY/MM/DD/ 48 | path (fileName, { title, published, meta = {} }) { 49 | if (meta.slug) { 50 | return meta.slug 51 | } 52 | 53 | const slug = slugify(title || fileName) 54 | const date = published.toString().split(/\s+/).slice(1, 4).reverse() 55 | return `${date[0]}/${date[2].toLowerCase()}/${date[1]}/${slug}/` 56 | }, 57 | 58 | // id() determines the unique RSS ID of a Markdown source 59 | // Default RFC4151-based format is used. See https://tools.ietf.org/html/rfc4151 60 | id ({ published, path }) { 61 | const tagDomain = this.config.feed.tagDomain 62 | const year = published.getFullYear() 63 | return `tag:${tagDomain},${year}:${path}` 64 | }, 65 | 66 | // title() determines the title of a Markdown source 67 | title (body) { 68 | const titleMatch = body.substr(body.indexOf('#')).match(/^#\s+(.*)/) 69 | return titleMatch ? titleMatch[1] : '' 70 | } 71 | } 72 | 73 | export default source 74 | -------------------------------------------------------------------------------- /packages/blog/src/blueprint/static/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= blog.title %> 5 | <%= blog.feed.link %> 6 | <%= blog.feed.description %> 7 | <% entries.forEach((entry) => { %> 8 | <%= blog.feed.link %><%= entry.path %> 9 | <%= entry.id %> 10 | <%= entry.published.toString() %> 11 | <%= entry.title %> 12 | 13 | <% }) %> 14 | -------------------------------------------------------------------------------- /packages/blog/src/index.js: -------------------------------------------------------------------------------- 1 | import { autoregister as _autoregister } from '@nuxt-press/core' 2 | import Blueprint from './blueprint' 3 | 4 | export { Blueprint } 5 | 6 | export function autoregister (options) { 7 | return _autoregister.call(this, options, [Blueprint.id]) 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog/src/theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | box-sizing: border-box; 3 | font-family: sans-serif; 4 | } 5 | 6 | html.blog #__layout { 7 | width: 740px !important; 8 | margin: 0px auto; 9 | } 10 | 11 | #nuxt-press.blog { 12 | display: flex; 13 | flex-grow: 1; 14 | justify-content: center; 15 | min-height: 100vh; 16 | 17 | & .sidebar { 18 | background: linear-gradient(-90deg, rgba(244,244,244,1.0) 50%, rgba(255,255,255,1.0) 95%, rgba(255,255,255,1.0) 100%); 19 | border-right: 1px solid #000; 20 | flex-basis: 25%; 21 | margin: 0px; 22 | padding: 20px; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: flex-end; 26 | 27 | & .about { 28 | margin: 15px 0px; 29 | margin-bottom: 100px; 30 | & p { 31 | margin: 0; 32 | font-size: 28px; 33 | display: table-caption; 34 | text-align: right; 35 | } 36 | & .text-links { 37 | margin-top: 100px; 38 | & a { 39 | font-size: 15px; 40 | display: block; 41 | text-align: right; 42 | &:hover { 43 | border-bottom: none; 44 | text-decoration: underline; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | & .content { 52 | flex-basis: 75%; 53 | padding: 20px; 54 | 55 | & code { 56 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 57 | font-size: 1em; 58 | color: #669900; 59 | font-weight: bold; 60 | } 61 | 62 | & a { 63 | margin: 0px; 64 | margin-bottom: 5px; 65 | color: #000 !important; 66 | padding: 0px; 67 | & code { 68 | background: none; 69 | } 70 | &:hover { 71 | border-bottom: none; 72 | } 73 | & img { 74 | display: inline-block; 75 | width: 30px; 76 | height: auto; 77 | } 78 | } 79 | 80 | & .h1 { 81 | font-size: 17px; 82 | margin-left: 10px; 83 | } 84 | 85 | & .h2 { 86 | font-size: 16px; 87 | margin-left: 20px; 88 | } 89 | 90 | & .h3 { 91 | font-size: 15px; 92 | margin-left: 30px; 93 | } 94 | 95 | & blockquote { 96 | font-style: italic; 97 | font-size: 0.9em; 98 | margin-left: 0px; 99 | padding-top: 0px; 100 | padding-bottom: 0px; 101 | padding-left: 10px; 102 | margin-right: 0px; 103 | border-left: 3px solid #555555; 104 | } 105 | 106 | /* prismjs override */ 107 | & pre, 108 | & pre[class*="language-"] { 109 | background: #f6f8fa; 110 | padding: 15px; 111 | & > code { 112 | font-size: 0.9em !important; 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/blog/test/e2e/basic.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('basic', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/basic/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testPageHome () { 21 | expect(await page.getText('.blog h1')).toBe('Adding sidebar links') 22 | 23 | const expectedLinks = [ 'Home', 'Archive', 'About' ] 24 | const sidebarLinks = await page.getTexts('.sidebar .text-links a', true) 25 | expect(sidebarLinks).toEqual(expectedLinks) 26 | 27 | expect(await page.getText('.about p', true)).toEqual('A NuxtPress Blog') 28 | } 29 | 30 | async function testPageArchive () { 31 | expect(await page.getText('.blog h1')).toBe('2019') 32 | 33 | expect(await page.getElementCount('.blog .title a')).toBe(2) 34 | 35 | const expectedLinks = [ 36 | '/2019/jun/20/adding-sidebar-links/', 37 | '/2019/apr/20/publishing-blogs/' 38 | ] 39 | expect(await page.getAttributes('.blog .title a', 'href')).toEqual(expectedLinks) 40 | } 41 | 42 | async function testPageAbout () { 43 | expect(await page.getText('.blog h1')).toBe('About') 44 | } 45 | 46 | test('open home', async () => { 47 | const url = browser.getUrl('/') 48 | 49 | page = await browser.page(url) 50 | 51 | await testPageHome() 52 | }) 53 | 54 | test('nav /archive', async () => { 55 | await page.navigate('/archive') 56 | 57 | await testPageArchive() 58 | }) 59 | 60 | test('open archive', async () => { 61 | const url = browser.getUrl('/archive') 62 | 63 | page = await browser.page(url) 64 | 65 | await testPageArchive() 66 | }) 67 | 68 | test('nav /about', async () => { 69 | await page.navigate('/about') 70 | 71 | await testPageAbout() 72 | }) 73 | 74 | test('open about', async () => { 75 | const url = browser.getUrl('/about') 76 | 77 | page = await browser.page(url) 78 | 79 | await testPageAbout() 80 | }) 81 | 82 | test('nav /', async () => { 83 | await page.navigate('/') 84 | 85 | await testPageHome() 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /packages/blog/test/e2e/prefix.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('prefix', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/prefix/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testPageHome () { 21 | expect(await page.getText('.blog h1')).toBe('Adding sidebar links') 22 | 23 | const expectedLinks = [ 'Home', 'Archive', 'About' ] 24 | const sidebarLinks = await page.getTexts('.sidebar .text-links a', true) 25 | expect(sidebarLinks).toEqual(expectedLinks) 26 | 27 | expect(await page.getText('.about p', true)).toEqual('A NuxtPress Blog') 28 | } 29 | 30 | async function testPageArchive () { 31 | expect(await page.getText('.blog h1')).toBe('2019') 32 | 33 | expect(await page.getElementCount('.blog .title a')).toBe(2) 34 | 35 | const expectedLinks = [ 36 | '/my-blog/2019/jun/20/adding-sidebar-links/', 37 | '/my-blog/2019/apr/20/publishing-blogs/' 38 | ] 39 | expect(await page.getAttributes('.blog .title a', 'href')).toEqual(expectedLinks) 40 | } 41 | 42 | test('open home', async () => { 43 | const url = browser.getUrl('/my-blog') 44 | 45 | page = await browser.page(url) 46 | 47 | await testPageHome() 48 | }) 49 | 50 | test('nav /archive', async () => { 51 | await page.navigate('/my-blog/archive') 52 | 53 | await testPageArchive() 54 | }) 55 | 56 | test('open archive', async () => { 57 | const url = browser.getUrl('/my-blog/archive') 58 | 59 | page = await browser.page(url) 60 | 61 | await testPageArchive() 62 | }) 63 | 64 | test('nav /', async () => { 65 | await page.navigate('/my-blog/') 66 | 67 | await testPageHome() 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/basic.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname, changedPaths: ['static$', 'static/rss.xml$'] }) 4 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "blog": { 3 | "dir": "posts", 4 | "prefix": "/", 5 | "title": "A NuxtPress Blog", 6 | "links": [ 7 | { 8 | "Home": "/" 9 | }, 10 | { 11 | "Archive": "/archive" 12 | }, 13 | { 14 | "About": "/about" 15 | } 16 | ], 17 | "icons": [ 18 | { 19 | "github": "http://github.com/nuxt/nuxt.js" 20 | }, 21 | { 22 | "twitter": "https://twitter.com/nuxt_js" 23 | } 24 | ], 25 | "feed": { 26 | "link": "https://nuxt.press/", 27 | "description": "A NuxtPress Blog Description", 28 | "tagDomain": "nuxt.press", 29 | "path": "/rss.xml" 30 | }, 31 | "tagDomain": "nuxt.press" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/pages/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello **world**! 3 | layout: blog 4 | --- 5 | 6 | # About 7 | 8 | Hey, this works! 9 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/posts/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: April 20, 2019 3 | --- 4 | 5 | # Publishing blogs 6 | 7 | > This content comes from the `docs` suite. It used here to exemplify 8 | > a blog entry from **April 20, 2019**. 9 | 10 | To publish a blog, add your Markdown files to the `blog/` folder. You can 11 | structure them in however many subdirectories you want (for grouping posts by 12 | year of publication for, for instance). What determines the publishing date of 13 | each blog entry is actually their Markdown source header. 14 | 15 | By default, NuxtPress uses a simple format where the first line is parsed out 16 | as the publication date. **Titles** and **slugs** are automatically generated 17 | from the first heading (`#`) of your Markdown sources: 18 | 19 | ``` 20 | June 20, 2019 21 | 22 | # Blog Entry's Title 23 | 24 | ``` 25 | 26 | If your Markdown sources however start with a `---`, NuxtPress will try and 27 | parse it via [gray-matter][gm] and will look for `title`, `slug` and `date`. 28 | 29 | [gm]: https://github.com/jonschlinkert/gray-matter 30 | 31 | ```markup 32 | --- 33 | title: Blog Entry's Title 34 | date: June 20, 2019 35 | slug: blog-entry-slug 36 | --- 37 | 38 | # This Heading Is Not Used As Title 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/posts/links.md: -------------------------------------------------------------------------------- 1 | June 20, 2019 2 | 3 | # Adding sidebar links 4 | 5 | > This content comes from the `docs` suite. It used here to exemplify 6 | > a blog entry from **June 20, 2019**. 7 | 8 | NuxtPress' default blog template makes it easy to automatically include sidebar text links. 9 | 10 | Here's `nuxt.press.json` from [`examples/blog`][examples-blog]. 11 | 12 | [examples-blog]: https://github.com/nuxt/press/tree/master/examples/blog 13 | 14 | ```json 15 | { 16 | "blog": { 17 | "links": [ 18 | {"Home": "/blog"}, 19 | {"Archive": "/blog/archive"}, 20 | {"About": "/blog/about"}, 21 | ] 22 | } 23 | } 24 | ``` 25 | 26 | This feature is mostly illustrative. You're likely to benefit more from ejecting 27 | the entire app bundle and adding your code for the `sidebar` component. Your 28 | component can still have access to these options you define under the `blog` 29 | configuration key in `nuxt.press.json` or `nuxt.press.js`, making it extremely 30 | to customize templates even with your own configuration options. 31 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/basic/static/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A NuxtPress Blog 5 | https://nuxt.press/ 6 | A NuxtPress Blog Description 7 | 8 | https://nuxt.press//2019/jun/20/adding-sidebar-links/ 9 | tag:nuxt.press,2019:undefined 10 | Thu Jun 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 11 | Adding sidebar links 12 | 13 | 14 | https://nuxt.press//2019/apr/20/publishing-blogs/ 15 | tag:nuxt.press,2019:undefined 16 | Sat Apr 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 17 | Publishing blogs 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/prefix/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/prefix/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "blog": { 3 | "dir": "posts", 4 | "prefix": "/my-blog/", 5 | "title": "A NuxtPress Blog", 6 | "links": [ 7 | { 8 | "Home": "/my-blog/" 9 | }, 10 | { 11 | "Archive": "/my-blog/archive" 12 | }, 13 | { 14 | "About": "/my-blog/about" 15 | } 16 | ], 17 | "icons": [ 18 | { 19 | "github": "http://github.com/nuxt/nuxt.js" 20 | }, 21 | { 22 | "twitter": "https://twitter.com/nuxt_js" 23 | } 24 | ], 25 | "feed": { 26 | "link": "https://nuxt.press/", 27 | "description": "A NuxtPress Blog Description", 28 | "tagDomain": "nuxt.press", 29 | "path": "/rss.xml" 30 | }, 31 | "tagDomain": "nuxt.press" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/prefix/posts/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: April 20, 2019 3 | --- 4 | 5 | # Publishing blogs 6 | 7 | > This content comes from the `docs` suite. It used here to exemplify 8 | > a blog entry from **April 20, 2019**. 9 | 10 | To publish a blog, add your Markdown files to the `blog/` folder. You can 11 | structure them in however many subdirectories you want (for grouping posts by 12 | year of publication for, for instance). What determines the publishing date of 13 | each blog entry is actually their Markdown source header. 14 | 15 | By default, NuxtPress uses a simple format where the first line is parsed out 16 | as the publication date. **Titles** and **slugs** are automatically generated 17 | from the first heading (`#`) of your Markdown sources: 18 | 19 | ``` 20 | June 20, 2019 21 | 22 | # Blog Entry's Title 23 | 24 | ``` 25 | 26 | If your Markdown sources however start with a `---`, NuxtPress will try and 27 | parse it via [gray-matter][gm] and will look for `title`, `slug` and `date`. 28 | 29 | [gm]: https://github.com/jonschlinkert/gray-matter 30 | 31 | ```markup 32 | --- 33 | title: Blog Entry's Title 34 | date: June 20, 2019 35 | slug: blog-entry-slug 36 | --- 37 | 38 | # This Heading Is Not Used As Title 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/prefix/posts/links.md: -------------------------------------------------------------------------------- 1 | June 20, 2019 2 | 3 | # Adding sidebar links 4 | 5 | > This content comes from the `docs` suite. It used here to exemplify 6 | > a blog entry from **June 20, 2019**. 7 | 8 | NuxtPress' default blog template makes it easy to automatically include sidebar text links. 9 | 10 | Here's `nuxt.press.json` from [`examples/blog`][examples-blog]. 11 | 12 | [examples-blog]: https://github.com/nuxt/press/tree/master/examples/blog 13 | 14 | ```json 15 | { 16 | "blog": { 17 | "links": [ 18 | {"Home": "/blog"}, 19 | {"Archive": "/blog/archive"}, 20 | {"About": "/blog/about"}, 21 | ] 22 | } 23 | } 24 | ``` 25 | 26 | This feature is mostly illustrative. You're likely to benefit more from ejecting 27 | the entire app bundle and adding your code for the `sidebar` component. Your 28 | component can still have access to these options you define under the `blog` 29 | configuration key in `nuxt.press.json` or `nuxt.press.js`, making it extremely 30 | to customize templates even with your own configuration options. 31 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/prefix/prefix.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname, changedPaths: ['static$', 'static/rss.xml$'] }) 4 | -------------------------------------------------------------------------------- /packages/blog/test/fixtures/prefix/static/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A NuxtPress Blog 5 | https://nuxt.press/ 6 | A NuxtPress Blog Description 7 | 8 | https://nuxt.press//my-blog/2019/jun/20/adding-sidebar-links/ 9 | tag:nuxt.press,2019:undefined 10 | Thu Jun 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 11 | Adding sidebar links 12 | 13 | 14 | https://nuxt.press//my-blog/2019/apr/20/publishing-blogs/ 15 | tag:nuxt.press,2019:undefined 16 | Sat Apr 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time) 17 | Publishing blogs 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/blog/test/unit/blueprint.generateRoutes.test.js: -------------------------------------------------------------------------------- 1 | import defu from 'defu' 2 | import * as utils from '@nuxt-press/utils' 3 | import Blueprint from '../../src/blueprint' 4 | 5 | jest.mock('@nuxt-press/utils') 6 | 7 | async function createInstance (config = {}, options) { 8 | Blueprint._runGuards = undefined 9 | Blueprint.templates = undefined 10 | 11 | const nuxt = { options: { css: [] } } 12 | options = defu({ id: 'my-test' }, options) 13 | 14 | const bp = new Blueprint(nuxt, options) 15 | bp.nuxt = nuxt 16 | bp.blueprintOptions = {} 17 | bp.loadConfig = jest.fn().mockReturnValue(config) 18 | bp.setLocales = _ => _ 19 | bp.coreSetup = _ => _ 20 | bp.createApi = _ => _ 21 | bp.addServerMiddleware = _ => _ 22 | bp.rootConfig = {} 23 | await bp.setup() 24 | 25 | return bp 26 | } 27 | 28 | describe('blog blueprint', () => { 29 | test('createGenerateRoutes', async () => { 30 | const { normalizePath } = jest.requireActual('@nuxt-press/utils') 31 | utils.normalizePath.mockImplementation(normalizePath) 32 | 33 | const bp = await createInstance() 34 | 35 | bp.data = { 36 | topLevel: { 37 | '/index': 'index', 38 | '/archive': 'archive' 39 | }, 40 | sources: { 41 | '/my-path/': {} 42 | } 43 | } 44 | 45 | const prefix = jest.fn(p => p) 46 | const routes = await bp.createGenerateRoutes('/var/nuxt', prefix) 47 | 48 | await Promise.all(routes.map(route => route.payload)) 49 | 50 | expect(routes).toEqual([ 51 | { route: '/', payload: undefined }, 52 | { route: '/archive/', payload: undefined }, 53 | { route: '/my-path/', payload: undefined } 54 | ]) 55 | }) 56 | 57 | test('generateExtendRoutes', async () => { 58 | const { normalizePathPrefix } = jest.requireActual('@nuxt-press/utils') 59 | utils.normalizePathPrefix.mockImplementation(normalizePathPrefix) 60 | 61 | const bp = await createInstance({ 62 | prefix: 'my-prefix' 63 | }) 64 | 65 | bp.getGenerateRoot = jest.fn().mockReturnValue('/var/nuxt/_press') 66 | bp.data = { 67 | topLevel: { 68 | '/my-prefix/index': 'index', 69 | '/my-prefix/archive': 'archive' 70 | }, 71 | sources: { 72 | '/my-path/': {} 73 | } 74 | } 75 | 76 | const routes = await bp.generateExtendRoutes() 77 | 78 | await Promise.all(routes.map(route => route.payload)) 79 | 80 | expect(routes).toEqual([ 81 | { route: '/my-prefix/', payload: undefined }, 82 | { route: '/my-prefix/archive/', payload: undefined }, 83 | { route: '/my-path/', payload: undefined } 84 | ]) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /packages/blog/test/unit/data.test.js: -------------------------------------------------------------------------------- 1 | import 'gray-matter' 2 | import fs from 'fs-extra' 3 | import { createBlueprintContext } from 'test-utils' 4 | import { _parseEntry } from '../../src/blueprint/data' 5 | 6 | jest.mock('gray-matter') 7 | jest.mock('fs-extra') 8 | 9 | describe('parsePage', () => { 10 | afterEach(() => jest.resetAllMocks()) 11 | 12 | test('basic functionality', async () => { 13 | fs.readFile.mockReturnValue('') 14 | 15 | const thisContext = createBlueprintContext('blog') 16 | const pathInfo = { 17 | root: '/var/nuxt', 18 | prefix: '', 19 | path: 'index.md' 20 | } 21 | 22 | const source = await _parseEntry.call(thisContext, pathInfo) 23 | 24 | expect(source.meta).toBeUndefined() 25 | expect(source.metaTest).toBe(true) 26 | expect(thisContext.config.source.markdown).toHaveBeenCalledTimes(1) 27 | 28 | expect(source.type).toEqual('entry') 29 | expect(thisContext.config.source.title).toHaveBeenCalledTimes(1) 30 | expect(source.title).toEqual('the title') 31 | expect(source.body).toEqual('the html') 32 | 33 | expect(source.path).toEqual('/the_path/') 34 | expect(source.src).toEqual('/var/nuxt/index.md') 35 | }) 36 | 37 | test('doesnt return src path in production', async () => { 38 | fs.readFile.mockReturnValue('') 39 | 40 | const thisContext = createBlueprintContext('blog', { nuxt: { options: { dev: false } } }) 41 | const pathInfo = { 42 | root: '/var/nuxt', 43 | prefix: '', 44 | path: 'index.md' 45 | } 46 | 47 | const source = await _parseEntry.call(thisContext, pathInfo) 48 | 49 | expect(source.src).toBeUndefined() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/blog/test/unit/source.test.js: -------------------------------------------------------------------------------- 1 | import source from '../../src/blueprint/source' 2 | 3 | describe('source.metadata', () => { 4 | test('missing date', () => { 5 | expect(() => source.metadata('', 'x.md')) 6 | .toThrow('Missing or invalid publication date in x.md -- see documentation at https://nuxt.press') 7 | }) 8 | 9 | test('with date', () => { 10 | const raw = `--- 11 | date: 2019-09-16 12 | title: x 13 | ---` 14 | expect(source.metadata(raw, 'x.md').meta) 15 | .toEqual({ 16 | published: new Date(Date.parse('2019-09-16')), 17 | title: 'x' 18 | }) 19 | }) 20 | 21 | test('with start date', () => { 22 | const raw = `June 20, 2019 23 | # title` 24 | 25 | expect(source.metadata(raw, 'x.md').meta) 26 | .toEqual({ 27 | published: new Date(Date.parse('June 20, 2019')) 28 | }) 29 | }) 30 | }) 31 | 32 | describe('source.path', () => { 33 | test('with title', () => { 34 | const data = { 35 | title: 'Suppa Doctor', 36 | published: new Date(Date.parse('June 20, 2019')) 37 | } 38 | 39 | expect(source.path('post.md', data)) 40 | .toEqual('2019/jun/20/suppa-doctor/') 41 | }) 42 | 43 | test('without title', () => { 44 | const data = { 45 | published: new Date(Date.parse('June 01, 2019')) 46 | } 47 | 48 | expect(source.path('post', data)) 49 | .toEqual('2019/jun/01/post/') 50 | }) 51 | 52 | test('with meta.slug', () => { 53 | const data = { 54 | published: new Date(Date.parse('June 01, 2019')), 55 | meta: { 56 | slug: 'xyz/my/big/post' 57 | } 58 | } 59 | 60 | expect(source.path('post', data)) 61 | .toEqual('xyz/my/big/post') 62 | }) 63 | }) 64 | 65 | describe('source.title', () => { 66 | test('with title', () => { 67 | const body = ` 68 | # Suppaman the Hero 69 | ## Non title 70 | ## Non Title 2` 71 | 72 | expect(source.title(body)) 73 | .toEqual('Suppaman the Hero') 74 | }) 75 | 76 | test('without title', () => { 77 | const body = ` 78 | ## Suppaman the Hero 79 | ## Non title 80 | ## Non Title 2` 81 | 82 | expect(source.title(body)) 83 | .toEqual('') 84 | }) 85 | }) 86 | 87 | test('source.id', () => { 88 | const data = { published: new Date(Date.parse('2019-09-16')), path: '/my/blog/post-x' } 89 | const context = { 90 | config: { 91 | feed: { 92 | tagDomain: 'domamain.com' 93 | } 94 | } 95 | } 96 | 97 | expect(source.id.call(context, data)) 98 | .toEqual('tag:domamain.com,2019:/my/blog/post-x') 99 | }) 100 | -------------------------------------------------------------------------------- /packages/cli/bin/nuxt-press.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { join, dirname } = require('path') 4 | const { run } = require('@nuxt/blueprints') 5 | 6 | const r = mode => join(dirname(require.resolve(`@nuxt-press/${mode}`)), 'blueprint') 7 | 8 | run({ 9 | name: 'press', 10 | autodiscover: { 11 | filter: ({ dir }) => !!dir 12 | }, 13 | blueprints: { 14 | blog: r('blog'), 15 | core: r('core'), 16 | docs: r('docs'), 17 | slides: r('slides') 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/cli", 3 | "version": "0.2.0-beta.0", 4 | "repository": "https://github.com/nuxt/press", 5 | "license": "MIT", 6 | "bin": { 7 | "nuxt-press": "bin/nuxt-press.js" 8 | }, 9 | "files": [ 10 | "bin" 11 | ], 12 | "dependencies": { 13 | "@nuxt/blueprints": "0.1.0-beta.4" 14 | }, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "scripts": { 19 | "dev": "nuxt dev", 20 | "build": "nuxt build", 21 | "start": "nuxt start", 22 | "press": "nuxt press" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: ['@nuxt/press'] 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/core", 3 | "version": "0.2.0-beta.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/nuxt/press", 6 | "license": "MIT", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@nuxt-press/utils": "0.2.0-beta.0", 12 | "@nuxt/blueprints": "0.1.0-beta.4", 13 | "@nuxt/http": "^0.3.5", 14 | "@nuxt/markdown": "^0.0.21", 15 | "defu": "^0.0.3", 16 | "fs-extra": "^8.1.0", 17 | "gray-matter": "^4.0.2", 18 | "hable": "3.0.0-0", 19 | "normalize.css": "^8.0.1", 20 | "prismjs": "^1.17.1", 21 | "wysiwyg.css": "^0.0.3" 22 | }, 23 | "scripts": { 24 | "dev": "nuxt dev", 25 | "build": "nuxt build", 26 | "start": "nuxt start", 27 | "press": "nuxt press" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/autoregister.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { normalizeConfig, importModule } from '@nuxt-press/utils' 3 | import PressBlueprint from './blueprint' 4 | 5 | const allModes = ['docs', 'blog', 'slides', 'pages'] 6 | 7 | export default async function autoregister (options, modes) { 8 | // Note:`this` refers to the ModuleContainer instance 9 | const config = await PressBlueprint.loadRootConfig({ 10 | rootDir: this.nuxt.options.rootDir, 11 | options: this.nuxt.options, 12 | config: normalizeConfig(options) 13 | }) 14 | 15 | if (Array.isArray(modes)) { 16 | modes = modes.filter(mode => allModes.includes(mode)) 17 | } else { 18 | modes = allModes 19 | } 20 | 21 | let pressInstances = {} 22 | for (const mode of modes) { 23 | try { 24 | const { Blueprint } = await importModule(`@nuxt-press/${mode}`) 25 | 26 | const modeInstances = await Blueprint.register(this, config) 27 | if (modeInstances) { 28 | pressInstances = { 29 | ...pressInstances, 30 | ...modeInstances 31 | } 32 | } 33 | } catch (error) { 34 | // TODO: improve message 35 | if (error.code === 'MODULE_NOT_FOUND') { 36 | consola.warn(`Please install @nuxt-press/${mode}`, error) 37 | } else { 38 | consola.error(error) 39 | } 40 | } 41 | } 42 | 43 | // first run all setups 44 | const setupPromises = Object.values(pressInstances).map(modeInstance => modeInstance.setup()) 45 | await Promise.all(setupPromises) 46 | 47 | // then init the mode instances 48 | const initPromises = Object.values(pressInstances).map(modeInstance => modeInstance.init()) 49 | await Promise.all(initPromises) 50 | 51 | return pressInstances 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/api.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { existsSync, readJsonSync } from 'fs-extra' 3 | import { trimSlashEnd } from '@nuxt-press/utils' 4 | 5 | const sourceCache = {} 6 | 7 | const suffixes = ['/index.json', '.json', ''] 8 | 9 | export default function coreApi ({ rootDir, dev }) { 10 | return { 11 | source (req, res, next) { 12 | const source = trimSlashEnd(req.url).toLowerCase() 13 | const cacheKey = `${rootDir}/${source}` 14 | 15 | if (dev || !sourceCache[cacheKey]) { 16 | for (const suffix of suffixes) { 17 | const sourceFile = path.join(rootDir, 'sources', `${source}${suffix}`) 18 | 19 | if (existsSync(sourceFile)) { 20 | sourceCache[cacheKey] = readJsonSync(sourceFile) 21 | break 22 | } 23 | } 24 | 25 | if (!sourceCache[cacheKey]) { 26 | const err = new Error('NuxtPress: source not found') 27 | err.statusCode = 404 28 | next(err) 29 | return 30 | } 31 | } 32 | 33 | res.json(sourceCache[cacheKey]) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/components/nuxt-static.js: -------------------------------------------------------------------------------- 1 | const isClient = process.client 2 | 3 | let requestIdleCallback = function (cb) { 4 | const start = Date.now() 5 | return setTimeout(() => { 6 | cb({ // eslint-disable-line standard/no-callback-literal 7 | didTimeout: false, 8 | timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) 9 | }) 10 | }, 1) 11 | } 12 | 13 | if (typeof window !== 'undefined' && window.requestIdleCallback) { 14 | requestIdleCallback = window.requestIdleCallback 15 | } 16 | 17 | // @vue/component 18 | export default { 19 | functional: true, 20 | props: { 21 | tag: { 22 | type: String, 23 | default: 'div' 24 | }, 25 | data: { 26 | type: [Object, Array, Boolean], 27 | required: false, 28 | default: false 29 | }, 30 | source: { 31 | type: String, 32 | required: false, 33 | default: '' 34 | } 35 | }, 36 | render (h, { props, slots, parent }) { 37 | const data = props.data || props.source 38 | 39 | if (isClient) { 40 | requestIdleCallback(() => { 41 | const pressLinks = [...parent.$el.querySelectorAll('[data-press-link]')] 42 | for (const pressLink of pressLinks) { 43 | pressLink.addEventListener('click', (e) => { 44 | e.preventDefault() 45 | parent.$router.push(e.target.attributes.href.value) 46 | return false 47 | }) 48 | } 49 | }) 50 | } 51 | 52 | if (!isClient || data) { 53 | if (props.source) { 54 | return h('nuxt-template', { 55 | props: { 56 | tag: props.tag, 57 | value: props.source 58 | } 59 | }) 60 | } 61 | 62 | return h(props.tag, slots().default) 63 | } 64 | 65 | // return empty tag on hydration on client 66 | // to prevent hydration error 67 | // this works because although the nuxt-template will contain 68 | // more html/vue components, those are not included 69 | // within the ssr component tree 70 | const vnode = h(props.tag) 71 | /* this should not be needed (anymore?): 72 | vnode.asyncFactory = {} 73 | vnode.isComment = true */ 74 | return vnode 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/components/nuxt-template.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | functional: true, 4 | props: { 5 | value: { 6 | type: String, 7 | required: true 8 | }, 9 | tag: { 10 | type: String, 11 | default: 'div' 12 | } 13 | }, 14 | render (h, { props, listeners }) { 15 | return h({ 16 | mounted: () => listeners.mounted && listeners.mounted(), 17 | template: `<${props.tag}>${props.value}` 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/components/observer.js: -------------------------------------------------------------------------------- 1 | export class EntriesObserver { 2 | static getEntryId (entry) { 3 | return `${entry.target.tagName}${entry.target.id}` 4 | } 5 | 6 | constructor ({ initialId, callback, throttle = 0 }) { 7 | this.initialId = initialId 8 | this.throttle = throttle 9 | this.callback = callback 10 | } 11 | 12 | update (entries, initial) { 13 | if (this.entries === undefined && initial === undefined) { 14 | initial = true 15 | this.entries = [] 16 | } 17 | 18 | for (const entry of entries) { 19 | entry.$id = this.constructor.getEntryId(entry) 20 | 21 | if (initial) { 22 | this.entries.push(entry) 23 | continue 24 | } 25 | 26 | const oldEntryIndex = this.entries.findIndex(e => e.$id === entry.$id) 27 | this.entries.splice(oldEntryIndex, 1, entry) 28 | } 29 | 30 | this.callActive(initial) 31 | } 32 | 33 | callActive (initial) { 34 | let entry 35 | 36 | if (initial) { 37 | entry = this.entries.find(e => e.target.id === this.initialId) 38 | } else { 39 | entry = this.entries.find(e => e.intersectionRatio > 0) 40 | } 41 | 42 | if (entry && this.callback) { 43 | if (!this.throttle) { 44 | this.callback(entry.target) 45 | return 46 | } 47 | 48 | clearTimeout(this.timeout) 49 | 50 | this.timeout = setTimeout(() => this.callback(entry.target), this.throttle) 51 | } 52 | } 53 | } 54 | 55 | export function startObserver ({ vm, elements, initialId, throttle = 100, options = {} }, callback) { 56 | if (!elements) { 57 | return null 58 | } 59 | 60 | const ahObserver = new EntriesObserver({ 61 | initialId, 62 | throttle, 63 | callback 64 | }) 65 | 66 | const observer = new IntersectionObserver(entries => ahObserver.update(entries), options) 67 | 68 | if (typeof elements === 'string') { 69 | elements = [...document.querySelectorAll(elements)] 70 | } 71 | 72 | elements.forEach(e => observer.observe(e)) 73 | 74 | if (vm) { 75 | if (!Array.isArray(vm.$options.destroyed)) { 76 | vm.$options.destroyed = [] 77 | } 78 | 79 | vm.$options.destroyed.push(() => observer.disconnect()) 80 | } 81 | 82 | return observer 83 | } 84 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/components/press-link.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'PressLink', 4 | functional: true, 5 | props: { 6 | to: { 7 | type: String, 8 | default: '' 9 | } 10 | }, 11 | render (h, { props, slots }) { 12 | const attrs = { 13 | 'href': props.to, 14 | 'data-press-link': 'true' 15 | } 16 | return h('a', { attrs }, slots().default) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/middleware/press.tmpl.js: -------------------------------------------------------------------------------- 1 | import { getRouteMeta } from '../utils' 2 | 3 | const typeToLayout = { 4 | 'entry': 'blog', 5 | 'topic': 'docs', 6 | 'slides': 'slides' 7 | } 8 | 9 | const maxCacheCount = 5 10 | const sourcesCache = [] 11 | 12 | async function getSource ($press, path) { 13 | <% 14 | /* Only load sources from fs when $hasSources is set, 15 | * the folder doesnt exists when there arent sources 16 | * which results in an error. 17 | * Dont like to do a fs.exists cause i/o is heavy 18 | */ 19 | if (options.rootOptions.$hasSources) { 20 | %> 21 | if (path.startsWith('api/source/')) { 22 | try { 23 | const source = await import( 24 | /* webpackInclude: /\.json$/ */ 25 | <% if (options.dev) { %> 26 | /* webpackChunkName: 'source-[request]' */ 27 | <% } %> 28 | /* webpackPreload: true */ 29 | `../../static/sources/${path.substr(11)}` 30 | ) 31 | 32 | return source 33 | } catch (err) { 34 | // return when the source doesnt exists 35 | if (err.code === 'MODULE_NOT_FOUND') { 36 | return 37 | } 38 | 39 | throw err 40 | } 41 | } 42 | <% } %> 43 | 44 | // implement a simple client-side cache for API-sources 45 | const cache = sourcesCache.find(cache => cache.path === path) 46 | 47 | if (cache) { 48 | cache.usedCount++ 49 | return cache.source 50 | } 51 | 52 | if (sourcesCache.length > maxCacheCount) { 53 | // sort most used, then shortest url first 54 | sourcesCache.sort((a, b) => { 55 | if (a.usedCount !== b.usedCount) { 56 | return b.usedCount - a.usedCount 57 | } 58 | return a.path < b.path ? -1 : 1 59 | }) 60 | 61 | while (sourcesCache.length > maxCacheCount) { 62 | sourcesCache.pop() 63 | } 64 | } 65 | 66 | const source = await $press.get(path) 67 | 68 | sourcesCache.push({ 69 | path, 70 | source, 71 | usedCount: 1 72 | }) 73 | 74 | return source 75 | } 76 | 77 | export default async function pressMiddleware (context) { 78 | const { app, route, $press, params, payload } = context 79 | 80 | // do not run when matched is empty, not our route! 81 | if (!route.matched.length) { 82 | return 83 | } 84 | 85 | const meta = getRouteMeta(route) 86 | 87 | if (meta.bp === 'docs') { 88 | const middlewareHookReady = $press.hasHook(`${meta.id}:middleware`) 89 | 90 | // wait for the mode plugin to register itself if it hasnt loaded yet 91 | if (!middlewareHookReady) { 92 | $press.hook('register', async ({ id }) => { 93 | if (meta.id === id) { 94 | await pressMiddleware(context) 95 | $press.clearHook('register') 96 | } 97 | }) 98 | return 99 | } 100 | } 101 | 102 | const middlewareContext = { 103 | path: route.path, 104 | meta 105 | } 106 | 107 | // reset layout to default 108 | $press.layout = 'default' 109 | 110 | // call middleware hooks for current mode first 111 | await $press.callHook(`${meta.id}:middleware`, middlewareContext) 112 | 113 | if (meta.source) { 114 | // this hook is mostly meant to fix the home page / 115 | // when only localized home pages like /en/ or /nl/ exists 116 | await $press.callHook(`${meta.id}:preparePath`, middlewareContext) 117 | 118 | const source = payload || await getSource($press, `api/source${middlewareContext.path}`) 119 | if (!source) { 120 | return 121 | } 122 | 123 | $press.layout = source.layout || typeToLayout[source.type] || $press.layout 124 | $press.source = source 125 | } 126 | 127 | $press.id = meta.id 128 | await $press.callHook(`${meta.id}:middlewareDone`, middlewareContext) 129 | } 130 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/pages/source.tmpl.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 99 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/plugins/press.tmpl.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import consola from 'consola' 3 | import { createPlugin } from 'press/core/utils' 4 | import NuxtStatic from '../components/nuxt-static' 5 | import NuxtTemplate from '../components/nuxt-template' 6 | import PressLink from '../components/press-link' 7 | import pressMiddleware from '../middleware/press' 8 | 9 | Vue.component('NuxtStatic', NuxtStatic) 10 | Vue.component('NuxtTemplate', NuxtTemplate) 11 | Vue.component('PressLink', PressLink) 12 | 13 | // TODO: remove this 14 | const apiToStatic = { 15 | // Blog-only API endpoints 16 | <% for (const id in options.rootOptions.blogPrefixes) { 17 | const prefix = options.rootOptions.blogPrefixes[id] 18 | %> 19 | '<%= id %>': { 20 | 'api/blog/index': '/_press/blog<%= prefix %>/index.json', 21 | 'api/blog/archive': '/_press/blog<%= prefix %>/archive.json' 22 | }, 23 | <% } %> 24 | // Slides-only API endpoints 25 | <% for (const id in options.rootOptions.slidesPrefixes) { 26 | const prefix = options.rootOptions.slidesPrefixes[id] 27 | %> 28 | '<%= id %>': { 29 | 'api/slides/index': '/_press/slides<%= prefix %>/index.json', 30 | }, 31 | <% } %> 32 | // Common API endpoints 33 | 'api/source': path => `/_press/sources/${path}/index.json` 34 | } 35 | 36 | function getUrl(apiPaths, url) { 37 | for (const apiPath in apiPaths) { 38 | if (url.startsWith(apiPath)) { 39 | const staticPath = apiPaths[apiPath] 40 | 41 | if (typeof staticPath === 'function') { 42 | const startSlice = apiPath.length + 1 43 | const endSlice = url.endsWith('/') ? 1 : 0 44 | 45 | url = url.slice(startSlice, url.length - endSlice) 46 | return staticPath(url) 47 | } 48 | 49 | return staticPath 50 | } 51 | } 52 | } 53 | 54 | function $json (url) { 55 | return fetch(url).then(r => r.json()) 56 | } 57 | 58 | export default createPlugin('press', async (plugin, context) => { 59 | plugin.get = function get (url) { 60 | const { route: { path, matched } } = context 61 | const [{ meta }] = matched || [] 62 | 63 | let apiUrl 64 | if (meta && !meta.source && apiToStatic[meta.id]) { 65 | apiUrl = getUrl(apiToStatic[meta.id], url) 66 | } 67 | 68 | if (!apiUrl) { 69 | apiUrl = getUrl(apiToStatic, url) 70 | } 71 | 72 | if (process.static && process.client) { 73 | return $json(apiUrl) 74 | } 75 | 76 | // strip first slash 77 | apiUrl = apiUrl[0] === '/' ? apiUrl.slice(1) : apiUrl 78 | return context.$http.$get(apiUrl).catch(err => consola.warn(err)) 79 | } 80 | 81 | // this is a workaround to prevent hydration errors 82 | // due to middlewares not running on first load on the client 83 | // TODO: try to understand why returning the pressMiddleware 84 | // promise doesnt work and this has to be async/await 85 | await pressMiddleware(context) 86 | }) 87 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/plugins/scroll.client.js: -------------------------------------------------------------------------------- 1 | function scrollTop (el) { 2 | let top = 0 3 | 4 | do { 5 | top += parseInt(el.offsetTop) 6 | el = el.offsetParent 7 | } while (el) 8 | 9 | return top 10 | } 11 | 12 | if (process.client) { 13 | const header = document.querySelector('#nuxt-press > header') 14 | const headerHeight = (header && parseInt(header.offsetHeight)) || 0 15 | 16 | window.onNuxtReady((app) => { 17 | const scrollBehavior = app.$router.options.scrollBehavior 18 | 19 | app.$router.options.scrollBehavior = (to, from, savedPosition) => { 20 | if (savedPosition) { 21 | return Promise.resolve(savedPosition) 22 | } 23 | 24 | if (app.$press.disableScrollBehavior) { 25 | return Promise.resolve(false) 26 | } 27 | 28 | // TODO: remove this once https://github.com/nuxt/nuxt.js/pull/6012 is released 29 | if (to.path === from.path && to.hash !== from.hash) { 30 | let y = 0 31 | 32 | if (to.hash) { 33 | const el = document.querySelector(to.hash) 34 | 35 | if (el) { 36 | const top = scrollTop(el) 37 | y = Math.max(0, top - headerHeight - 14) 38 | } 39 | } 40 | 41 | return Promise.resolve({ x: 0, y }) 42 | } 43 | 44 | return scrollBehavior(to, from, savedPosition) 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/source.js: -------------------------------------------------------------------------------- 1 | import Markdown from '@nuxt/markdown' 2 | import graymatter from 'gray-matter' 3 | 4 | const source = { 5 | processor () { 6 | return new Markdown({ toc: false, sanitize: false }) 7 | }, 8 | markdown (source, processor) { 9 | return processor.toMarkup(source).then(({ html }) => html) 10 | }, 11 | metadata (source) { 12 | if (source.trimLeft().startsWith('---')) { 13 | const { data: meta, content } = graymatter(source) 14 | return { meta, content } 15 | } 16 | return { 17 | meta: {} 18 | } 19 | }, 20 | title (body) { 21 | const [, title] = body.substr(body.indexOf('# ')).match(/^#\s+(.*)/) 22 | return title || '' 23 | } 24 | } 25 | 26 | export default source 27 | -------------------------------------------------------------------------------- /packages/core/src/blueprint/utils/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hookable from 'hable/dist/hable.esm.js' 3 | 4 | export function normalizedPath (path = '', prefix, locale) { 5 | if (prefix) { 6 | path = path.substr(prefix.length) 7 | } 8 | 9 | if ((!path || path === '/') && locale) { 10 | return `/${locale}/` 11 | } 12 | 13 | if (!path.endsWith('/')) { 14 | path = `${path}/` 15 | } 16 | return path 17 | } 18 | 19 | export function getRouteMeta (route) { 20 | const { meta = {} } = route.matched.find(r => r.name && r.name.startsWith('source-')) || {} 21 | return meta 22 | } 23 | 24 | // this functions creates the basic nuxt/press plugin dynamically 25 | // so it doesnt matter which plugin file is loaded first 26 | export function createPlugin (id, extendPlugin) { 27 | return function _createPlugin (context, inject) { 28 | const pluginId = `$${id}` 29 | 30 | if (!context[pluginId]) { 31 | // only props defined here or which are set with Vue.set are reactive 32 | const plugin = Vue.observable({ 33 | id: '', 34 | locale: '', 35 | path: '' 36 | }) 37 | 38 | const bus = new Hookable() 39 | 40 | // TODO: this should be moved to mode specific middleware 41 | // its used in eg blog but prevents using multiple instances 42 | plugin.data = {} 43 | 44 | plugin.hook = bus.hook.bind(bus) 45 | plugin.callHook = bus.callHook.bind(bus) 46 | plugin.clearHook = bus.clearHook.bind(bus) 47 | plugin.hasHook = (hookName) => { 48 | return Array.isArray(bus._hooks[hookName]) && bus._hooks[hookName].length > 0 49 | } 50 | 51 | plugin.register = ({ id, middleware, preparePath, done }) => { 52 | plugin.hook(`${id}:middleware`, middleware) 53 | 54 | if (preparePath) { 55 | plugin.hook(`${id}:preparePath`, preparePath) 56 | } 57 | 58 | if (done) { 59 | plugin.hook(`${id}:middlewareDone`, done) 60 | } 61 | 62 | return plugin.callHook('register', { id }) 63 | } 64 | 65 | context[pluginId] = plugin 66 | inject(id, plugin) 67 | } 68 | 69 | return extendPlugin(context[pluginId], context) 70 | } 71 | } 72 | 73 | export function prepareLocalePath ({ $press, params }, middlewareContext) { 74 | if (!params.locale) { 75 | const config = $press[middlewareContext.meta.id] 76 | 77 | // use default locale 78 | const [locale] = config.locales 79 | 80 | middlewareContext.locale = locale.code 81 | middlewareContext.path = `${middlewareContext.path}${middlewareContext.path.endsWith('/') ? '' : '/'}${locale.code}` 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/core/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as autoregister } from './autoregister' 2 | export { default as Blueprint } from './blueprint' 3 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/docs", 3 | "version": "0.2.0-beta.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/nuxt/press", 6 | "license": "MIT", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@nuxt-press/core": "0.2.0-beta.0", 12 | "@nuxtjs/lunr-module": "^0.3.1", 13 | "remark-container": "^0.1.1" 14 | }, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "scripts": { 19 | "dev": "nuxt dev", 20 | "build": "nuxt build", 21 | "start": "nuxt start", 22 | "press": "nuxt press" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/header.tmpl.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 112 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/home.vue: -------------------------------------------------------------------------------- 1 | // Credit: this component is largely adapted 2 | // from VuePress to maintain commonality 3 | 4 | 63 | 64 | 110 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/nav-link.vue: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/outbound-link-icon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/sidebar-section.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 120 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/sidebar-sections.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 47 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/components/sidebar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 141 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/data.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { walk } from '@nuxt/blueprints' 3 | import { 4 | readTextFile, 5 | getDirsAsArray, 6 | createJobsFromConfig, 7 | filePathToWebpath, 8 | PromisePool 9 | } from '@nuxt-press/utils' 10 | 11 | // DOCS MODE 12 | // Markdown files can be placed in 13 | // Nuxt's srcDir or the docs/ directory. 14 | // Directory configurable via press.docs.dir 15 | 16 | export async function _parsePage ({ root, prefix = '', path: sourcePath }, mdProcessor) { 17 | let raw = await readTextFile(root, sourcePath) 18 | 19 | const { content, meta } = this.config.source.metadata(raw) 20 | const { toc, html: body } = await this.config.source.markdown(content || raw, mdProcessor) 21 | const title = await this.config.source.title(body, sourcePath, toc) 22 | const webpath = filePathToWebpath(sourcePath, { prefix }) 23 | 24 | let locale = '' 25 | const locales = this.config.locales 26 | if (locales) { 27 | ({ code: locale } = locales.find(l => webpath.startsWith(`/${l.code}/`)) || {}) 28 | } 29 | 30 | const source = { 31 | type: 'topic', 32 | locale, 33 | title, 34 | body, 35 | src: this.nuxt.options.dev ? path.join(root, prefix, sourcePath) : undefined, 36 | path: `${this.config.prefix}${webpath}`, 37 | } 38 | 39 | return { 40 | toc: toc.map((h) => { 41 | // convert toc links to full but prefix-less urls 42 | if (h[2].substr(0, 1) === '#') { 43 | h[2] = `${webpath}${h[2]}` 44 | } 45 | return h 46 | }), 47 | meta, 48 | source 49 | } 50 | } 51 | 52 | export default async function docsData () { 53 | const jobs = await createJobsFromConfig(this.nuxt.options, this.config) 54 | 55 | const pages = {} 56 | const sources = {} 57 | const mdProcessor = await this.config.source.processor() 58 | 59 | const parsePage = _parsePage.bind(this) 60 | const handler = async (page) => { 61 | const { toc, meta, source } = await parsePage(page, mdProcessor) 62 | 63 | // Clarification: 64 | // - source.path is the full webpath including configured prefix 65 | // - sourcePath is the path without prefix 66 | // This is to make it easier to eg match sidebar stuff which 67 | // is based on paths without prefix 68 | const sourcePath = source.path.slice(this.config.prefix.length) 69 | 70 | this.nuxt.callHook('press:docs:page', { 71 | toc, 72 | meta, 73 | sourcePath, 74 | source 75 | }) 76 | 77 | pages[sourcePath] = { 78 | meta, 79 | toc 80 | } 81 | sources[sourcePath] = source 82 | } 83 | 84 | const queue = new PromisePool(jobs, handler) 85 | await queue.done() 86 | 87 | return { 88 | pages, 89 | sources 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/layouts/docs.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 83 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/mixins/docs.tmpl.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | computed: { 4 | <% if (options.dev) { %> 5 | $press_DEV_ONLY () { 6 | return this.$press 7 | }, 8 | <% } %> 9 | locale () { 10 | return this.$press.locale 11 | }, 12 | normalizedPath () { 13 | return this.$press.path 14 | }, 15 | $docs () { 16 | const docsId = this.$press.id 17 | const docsConfig = this.$press[docsId] 18 | 19 | if (docsId === 'docs' || (docsConfig && docsConfig.blueprint === 'docs')) { 20 | return docsConfig 21 | } 22 | 23 | // TODO: find a better way for this 24 | // return empty placeholder to prevent errors 25 | // as observers in lower components are triggered 26 | // before eg the layout had time to disable the sidebar 27 | return {} 28 | }, 29 | $nav () { 30 | if (this.$docs.configPerLocale) { 31 | return this.$docs.nav[this.locale] 32 | } 33 | 34 | return this.$docs.nav 35 | }, 36 | $page () { 37 | // return empty placeholder to prevent errors 38 | if (!this.$docs || !this.$docs.pages) { 39 | return { meta: {} } 40 | } 41 | 42 | const path = this.normalizedPath.toLowerCase() 43 | 44 | let page 45 | if (this.$docs.configPerLocale) { 46 | page = this.$docs.pages[this.locale][path] 47 | } else { 48 | page = this.$docs.pages[path] 49 | } 50 | 51 | if (page) { 52 | return page 53 | } 54 | 55 | const fallbackPath = this.locale ? `/${this.locale}/` : '/' 56 | if (this.$docs.pages[fallbackPath]) { 57 | return this.$docs.pages[fallbackPath] 58 | } 59 | 60 | // return empty placeholder to prevent errors 61 | return { meta: {} } 62 | }, 63 | $isHome () { 64 | if (!this.$docs.home) { 65 | return false 66 | } 67 | 68 | const path = this.normalizedPath 69 | 70 | if (this.locale) { 71 | return [`/${this.locale}/`, '/'].includes(path) 72 | } 73 | 74 | return path === '/' 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/plugins/press.$tmpl.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import OutboundLink from 'press/docs/components/outbound-link-icon' 3 | import { 4 | createPlugin, 5 | <% if (options.options.$hasLocales) { %>prepareLocalePath,<% } %> 6 | normalizedPath, 7 | getRouteMeta 8 | } from 'press/core/utils' 9 | 10 | Vue.component('OutboundLink', OutboundLink) 11 | 12 | async function docsMiddleware ({ route, params, $press }, middlewareContext = {}) { 13 | const meta = middlewareContext.meta 14 | <% if (options.options.$hasLocales) { 15 | // nothing todo when locale hasnt changed 16 | // TODO: maybe validate params.locale again, shouldnt be necessary though 17 | %> 18 | const locale = params.locale || `<%= options.options.$locales[0].code %>` 19 | <% } %> 20 | 21 | if ($press.id === meta.id<% if (options.options.$hasLocales) { %> && locale === $press.locale<% } %>) { 22 | return 23 | } 24 | 25 | const options = $press[`<%= options.id %>`] 26 | 27 | const shouldLoadConfig = !options<% if (options.options.configPerLocale) { %> || !options.pages[locale]<% } %> 28 | 29 | let config 30 | if (shouldLoadConfig) { 31 | config = await import( 32 | /* webpackPreload: true */ 33 | `./config.<%= options.id %><%= options.options.configPerLocale ? '.${locale}': '' %>` 34 | ) 35 | } 36 | 37 | let home 38 | const homePath = <% if (options.options.$hasLocales) { %>locale ? `/${locale}/` : <% } %>'/' 39 | const homePage = (config ? config.pages : options.pages<%= options.options.configPerLocale ? '[locale]': '' %>)[homePath] 40 | if (homePage && homePage.meta && homePage.meta.home) { 41 | home = homePage.meta 42 | } 43 | 44 | // return if docs has already been set, just set home as it 45 | // might have changed due to the locale 46 | if (options) { 47 | options.home = home 48 | <% if (options.options.configPerLocale) { %> 49 | if (!config) { 50 | return 51 | } 52 | if (config.nav) { 53 | options.nav[locale] = config.nav 54 | } 55 | if (config.pages) { 56 | options.pages[locale] = config.pages 57 | options.sidebars[locale] = config.sidebars 58 | } 59 | <% } %> 60 | return 61 | } 62 | 63 | const { nav, pages, sidebars } = config 64 | const docs = { 65 | ready: true, 66 | blueprint: '<%= options.options.blueprint %>', 67 | title: `<%= options.options.title %>`, 68 | configPerLocale: <%= options.options.configPerLocale ? 'true' : 'false' %>, 69 | prefix: '<%= options.options.prefix %>', 70 | home, 71 | <% if (options.options.$hasLocales) { %> 72 | locales: <%= JSON.stringify(options.options.$locales).replace(/"/g, '\'') %>, 73 | <% } %> 74 | nav<%= options.options.configPerLocale ? `: { [locale]: nav }` : '' %>, 75 | pages<%= options.options.configPerLocale ? `: { [locale]: pages }` : '' %>, 76 | sidebars<%= options.options.configPerLocale ? `: { [locale]: sidebars }` : '' %> 77 | } 78 | 79 | $press[`<%= options.id %>`] = docs 80 | } 81 | 82 | export default createPlugin('press', (plugin, context) => { 83 | return plugin.register({ 84 | id: `<%= options.id %>`, 85 | middleware: (middlewareContext) => docsMiddleware(context, middlewareContext), 86 | <% if (options.options.$hasLocales) { %> 87 | preparePath: (middlewareContext) => prepareLocalePath(context, middlewareContext), 88 | <% } %> 89 | done: ({ locale }) => { 90 | const { $press, route, params } = context 91 | <% if (options.options.$hasLocales) { %> 92 | $press.locale = params.locale || locale 93 | <% } %> 94 | $press.path = normalizedPath(route.path, $press[`<%= options.id %>`].prefix, $press.locale) 95 | } 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/source.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import Markdown from '@nuxt/markdown' 3 | import customContainer from 'remark-container' 4 | import graymatter from 'gray-matter' 5 | import defu from 'defu' 6 | 7 | const source = { 8 | processor () { 9 | return new Markdown({ 10 | toc: true, 11 | sanitize: false, 12 | extend ({ layers }) { 13 | layers['remark-container'] = customContainer 14 | } 15 | }) 16 | }, 17 | markdown (source, processor) { 18 | return processor.toMarkup(source) 19 | }, 20 | metadata (source) { 21 | const defaultMetaSettings = this.constructor.defaultConfig.metaSettings 22 | 23 | if (source.trimLeft().startsWith('---')) { 24 | const { content, data } = graymatter(source) 25 | 26 | const meta = defu(data, defaultMetaSettings) 27 | 28 | if (meta.sidebar === 'auto') { 29 | meta.sidebarDepth = this.constructor.defaultConfig.maxSidebarDepth 30 | } 31 | 32 | return { 33 | content, 34 | meta 35 | } 36 | } 37 | 38 | return { 39 | meta: { 40 | ...defaultMetaSettings 41 | } 42 | } 43 | }, 44 | title (body, sourcePath, toc) { 45 | if (toc && toc[0]) { 46 | return toc[0][1] 47 | } 48 | 49 | const titleMatch = body.substr(body.indexOf('#')).match(/^#+\s+(.*)/) 50 | 51 | if (titleMatch) { 52 | return titleMatch[1] 53 | } 54 | 55 | const { name: fileName } = path.parse(sourcePath) 56 | return fileName 57 | } 58 | } 59 | 60 | export default source 61 | -------------------------------------------------------------------------------- /packages/docs/src/blueprint/utils/index.js: -------------------------------------------------------------------------------- 1 | export const externalRE = /^(https?:|mailto:|tel:|[a-z]{3,}:)/i 2 | 3 | export function isExternal (url) { 4 | return externalRE.test(url) || url.startsWith('//') 5 | } 6 | 7 | export function isMailto (url) { 8 | return url.startsWith('mailto:') 9 | } 10 | 11 | export function isTel (url) { 12 | return url.startsWith('tel:') 13 | } 14 | -------------------------------------------------------------------------------- /packages/docs/src/index.js: -------------------------------------------------------------------------------- 1 | import { autoregister as _autoregister } from '@nuxt-press/core' 2 | import Blueprint from './blueprint' 3 | 4 | export { Blueprint } 5 | 6 | export function autoregister (options) { 7 | return _autoregister.call(this, options, [Blueprint.id]) 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/test/e2e/locales.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('locales', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/locales/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testPageHomeEN () { 21 | expect(await page.getText('h1')).toBe('Hello World') 22 | 23 | const expectedLinks = [ 'Hello World', 'Guide' ] 24 | const sidebarLinks = await page.getTexts('.sidebar-link', true) 25 | expect(sidebarLinks).toEqual(expectedLinks) 26 | } 27 | 28 | async function testPageHomeNL () { 29 | expect(await page.getText('h1')).toBe('Hallo Wereld') 30 | 31 | const expectedLinks = [ 'Hallo Wereld', 'Gids' ] 32 | const sidebarLinks = await page.getTexts('.sidebar-link', true) 33 | expect(sidebarLinks).toEqual(expectedLinks) 34 | } 35 | 36 | async function testPageGuideEN () { 37 | expect(await page.getText('h1')).toBe('Guide') 38 | 39 | const expectedLinks = [ 'Guide' ] 40 | const sidebarLinks = await page.getTexts('.sidebar-link.active', true) 41 | expect(sidebarLinks).toEqual(expectedLinks) 42 | } 43 | 44 | async function testPageGuideNL () { 45 | expect(await page.getText('h1')).toBe('Gids') 46 | 47 | const expectedLinks = [ 'Gids' ] 48 | const sidebarLinks = await page.getTexts('.sidebar-link.active', true) 49 | expect(sidebarLinks).toEqual(expectedLinks) 50 | } 51 | 52 | test('open home', async () => { 53 | const url = browser.getUrl('/') 54 | 55 | page = await browser.page(url) 56 | 57 | await testPageHomeEN() 58 | }) 59 | 60 | test('nav /nl', async () => { 61 | await page.navigate('/nl/') 62 | 63 | await testPageHomeNL() 64 | }) 65 | 66 | test('open /nl', async () => { 67 | const url = browser.getUrl('/nl') 68 | 69 | page = await browser.page(url) 70 | 71 | await testPageHomeNL() 72 | }) 73 | 74 | test('nav /nl/guide', async () => { 75 | await page.navigate('/nl/guide/') 76 | 77 | await testPageGuideNL() 78 | }) 79 | 80 | test('open /nl/guide', async () => { 81 | const url = browser.getUrl('/nl/guide') 82 | 83 | page = await browser.page(url) 84 | 85 | await testPageGuideNL() 86 | }) 87 | 88 | test('nav /en', async () => { 89 | await page.navigate('/en/') 90 | 91 | await testPageHomeEN() 92 | }) 93 | 94 | test('open /en', async () => { 95 | const url = browser.getUrl('/en') 96 | page = await browser.page(url) 97 | 98 | await testPageHomeEN() 99 | }) 100 | 101 | test('nav /en/guide', async () => { 102 | await page.navigate('/en/guide/') 103 | 104 | await testPageGuideEN() 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /packages/docs/test/e2e/prefix-locales.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('prefix-locales', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/prefix-locales/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testPageHomeEN () { 21 | expect(await page.getText('h1')).toBe('Hello World') 22 | 23 | const expectedLinks = [ 'Hello World', 'Guide' ] 24 | const sidebarLinks = await page.getTexts('.sidebar-link', true) 25 | expect(sidebarLinks).toEqual(expectedLinks) 26 | } 27 | 28 | async function testPageHomeNL () { 29 | expect(await page.getText('h1')).toBe('Hallo Wereld') 30 | 31 | const expectedLinks = [ 'Hallo Wereld', 'Gids' ] 32 | const sidebarLinks = await page.getTexts('.sidebar-link', true) 33 | expect(sidebarLinks).toEqual(expectedLinks) 34 | } 35 | 36 | async function testPageGuideEN () { 37 | expect(await page.getText('h1')).toBe('Guide') 38 | 39 | const expectedLinks = [ 'Guide' ] 40 | const sidebarLinks = await page.getTexts('.sidebar-link.active', true) 41 | expect(sidebarLinks).toEqual(expectedLinks) 42 | } 43 | 44 | async function testPageGuideNL () { 45 | expect(await page.getText('h1')).toBe('Gids') 46 | 47 | const expectedLinks = [ 'Gids' ] 48 | const sidebarLinks = await page.getTexts('.sidebar-link.active', true) 49 | expect(sidebarLinks).toEqual(expectedLinks) 50 | } 51 | 52 | test('open home', async () => { 53 | const url = browser.getUrl('/prefixed') 54 | 55 | page = await browser.page(url) 56 | 57 | await testPageHomeEN() 58 | }) 59 | 60 | test('nav /nl', async () => { 61 | await page.navigate('/prefixed/nl/') 62 | 63 | await testPageHomeNL() 64 | }) 65 | 66 | test('open /nl', async () => { 67 | const url = browser.getUrl('/prefixed/nl') 68 | 69 | page = await browser.page(url) 70 | 71 | await testPageHomeNL() 72 | }) 73 | 74 | test('nav /nl/guide', async () => { 75 | await page.navigate('/prefixed/nl/guide/') 76 | 77 | await testPageGuideNL() 78 | }) 79 | 80 | test('open /nl/guide', async () => { 81 | const url = browser.getUrl('/prefixed/nl/guide') 82 | 83 | page = await browser.page(url) 84 | 85 | await testPageGuideNL() 86 | }) 87 | 88 | test('nav /en', async () => { 89 | await page.navigate('/prefixed/en/') 90 | 91 | await testPageHomeEN() 92 | }) 93 | 94 | test('open /en', async () => { 95 | const url = browser.getUrl('/prefixed/en') 96 | page = await browser.page(url) 97 | 98 | await testPageHomeEN() 99 | }) 100 | 101 | test('nav /en/guide', async () => { 102 | await page.navigate('/prefixed/en/guide/') 103 | 104 | await testPageGuideEN() 105 | }) 106 | 107 | test('open /en/guide', async () => { 108 | const url = browser.getUrl('/prefixed/en/guide') 109 | page = await browser.page(url) 110 | 111 | await testPageGuideEN() 112 | }) 113 | 114 | test('nav /', async () => { 115 | await page.navigate('/prefixed/') 116 | 117 | await testPageHomeEN() 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/a/README.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/a/first.md: -------------------------------------------------------------------------------- 1 | # First Header 1 2 | 3 | Text 4 | 5 | ## First Header 2 6 | 7 | Text 8 | 9 | ### First Header 3 10 | 11 | Text 12 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/a/second.md: -------------------------------------------------------------------------------- 1 | # Second Header 1 2 | 3 | Text 4 | 5 | ## Second Header 1.1 6 | 7 | Text 8 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/b-ext.md: -------------------------------------------------------------------------------- 1 | # B-Extended 2 | 3 | You should not see this when this page is generated 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/b.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # B1 6 | 7 | # B2 8 | 9 | ## B2.1 10 | 11 | ### B2.1.1 12 | 13 | ## B2.2 14 | 15 | # B3 16 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/basic.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/c.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: C1 Meta 3 | sidebarDepth: 2 4 | --- 5 | 6 | # C1 7 | 8 | Text 9 | 10 | ## C1.1 11 | 12 | Text 13 | 14 | ### C1.1.1 15 | 16 | Text 17 | 18 | #### C1.1.1.1 19 | 20 | Text 21 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: Home 4 | header: true 5 | heroImage: /hero.png 6 | actionText: Get Started 7 | actionLink: /a/ 8 | features: 9 | - title: Feat 1 10 | details: Details 1 11 | - title: Feat 2 12 | details: Details 2 13 | - title: Feat 3 14 | details: Details 3 15 | footer: MIT Licensed 16 | --- 17 | 18 | # Lorem Ipsum 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum 21 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/basic/nuxt.press.js: -------------------------------------------------------------------------------- 1 | export default { 2 | docs: { 3 | dir: '', 4 | prefix: '/', 5 | title: 'Test docs', 6 | search: true, 7 | async extendStaticRoutes (routes, staticImport) { 8 | const path = '/b-ext/' 9 | const payload = { 10 | ...await staticImport('b'), 11 | path 12 | } 13 | 14 | routes[path] = payload 15 | }, 16 | nav: [ 17 | { 18 | 'text': 'A test', 19 | 'link': '/a/' 20 | }, 21 | { 22 | 'text': 'B test', 23 | 'link': '/b/' 24 | }, 25 | { 26 | 'text': 'C test', 27 | 'link': '/c/' 28 | }, 29 | { 30 | 'GitHub': 'https://github.com/nuxt/press' 31 | } 32 | ], 33 | sidebar: { 34 | '/': [ 35 | '/', 36 | { 37 | 'title': 'A test', 38 | 'children': [ 39 | '/a', 40 | '/a/first', 41 | '/a/second' 42 | ] 43 | } 44 | ], 45 | '/c': [ 46 | '/c', 47 | '/a/second' 48 | ] 49 | } 50 | }, 51 | mode: 'docs' 52 | } 53 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/docs-en/index.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/docs-guide/en/index.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | Guide 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/docs-guide/nl/index.md: -------------------------------------------------------------------------------- 1 | # Gids 2 | 3 | Gids 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/docs-nl/index.md: -------------------------------------------------------------------------------- 1 | # Hallo Wereld 2 | 3 | Tekst 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/full.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "guide-docs": { 3 | "dir": "docs-guide", 4 | "prefix": "guide-docs", 5 | "title": "Guide Docs Test", 6 | "search": false, 7 | "localizedSidebar": true, 8 | "configPerLocale": false, 9 | "nav": [], 10 | "blueprint": "docs", 11 | "sidebar": { 12 | "en": [ 13 | { 14 | "title": "Guide", 15 | "children": [ 16 | "/" 17 | ] 18 | } 19 | ], 20 | "nl": [ 21 | { 22 | "title": "Gids", 23 | "children": [ 24 | "/" 25 | ] 26 | } 27 | ] 28 | } 29 | }, 30 | "docs": { 31 | "dir": { 32 | "docs-en": "en", 33 | "docs-nl": "nl" 34 | }, 35 | "prefix": "docs", 36 | "title": "Localized Docs Test", 37 | "search": false, 38 | "localizedSidebar": false, 39 | "configPerLocale": true, 40 | "nav": [], 41 | "blueprint": "docs", 42 | "sidebar": [ 43 | "/" 44 | ] 45 | }, 46 | "locales": [ 47 | { 48 | "code": "en", 49 | "name": "English" 50 | }, 51 | { 52 | "code": "nl", 53 | "name": "Nederlands" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/full/pages/index.md: -------------------------------------------------------------------------------- 1 | # Full Nuxt Press Docs Test 2 | 3 | Go to: 4 | - [Default Docs](/docs) 5 | - [Guide Docs](/guide-docs) 6 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/en/guide.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | Guide 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/en/index.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/locales.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/nl/guide.md: -------------------------------------------------------------------------------- 1 | # Gids 2 | 3 | Gids 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/nl/index.md: -------------------------------------------------------------------------------- 1 | # Hallo Wereld 2 | 3 | Tekst 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/locales/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "dir": "", 4 | "prefix": "/", 5 | "title": "Docs Test Localization", 6 | "search": false, 7 | "localizedSidebar": false, 8 | "configPerLocale": false, 9 | "nav": [], 10 | "sidebar": [ 11 | "/", 12 | "/guide" 13 | ], 14 | "locales": [ 15 | { 16 | "code": "en", 17 | "name": "English" 18 | }, 19 | { 20 | "code": "nl", 21 | "name": "Nederlands" 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/en/guide.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | Guide 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/en/index.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/nl/guide.md: -------------------------------------------------------------------------------- 1 | # Gids 2 | 3 | Gids 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/nl/index.md: -------------------------------------------------------------------------------- 1 | # Hallo Wereld 2 | 3 | Tekst 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "dir": "", 4 | "prefix": "prefixed", 5 | "title": "Docs Test Localization", 6 | "search": false, 7 | "localizedSidebar": false, 8 | "configPerLocale": false, 9 | "nav": [], 10 | "sidebar": [ 11 | "/", 12 | "/guide" 13 | ] 14 | }, 15 | "locales": [ 16 | { 17 | "code": "en", 18 | "name": "English" 19 | }, 20 | { 21 | "code": "nl", 22 | "name": "Nederlands" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix-locales/prefix-locales.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/a/README.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/a/first.md: -------------------------------------------------------------------------------- 1 | # First Header 1 2 | 3 | Text 4 | 5 | ## First Header 2 6 | 7 | Text 8 | 9 | ### First Header 3 10 | 11 | Text 12 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/a/second.md: -------------------------------------------------------------------------------- 1 | # Second Header 1 2 | 3 | Text 4 | 5 | ## Second Header 1.1 6 | 7 | Text 8 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/b.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # B1 6 | 7 | # B2 8 | 9 | ## B2.1 10 | 11 | ### B2.1.1 12 | 13 | ## B2.2 14 | 15 | # B3 16 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/c.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: C1 Meta 3 | sidebarDepth: 2 4 | --- 5 | 6 | # C1 7 | 8 | Text 9 | 10 | ## C1.1 11 | 12 | Text 13 | 14 | ### C1.1.1 15 | 16 | Text 17 | 18 | #### C1.1.1.1 19 | 20 | Text 21 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "dir": "", 4 | "prefix": "/custom/", 5 | "title": "Test docs", 6 | "search": true, 7 | "localizedSidebar": false, 8 | "configPerLocale": true, 9 | "nav": [ 10 | { 11 | "text": "A test", 12 | "link": "/a" 13 | }, 14 | { 15 | "text": "B test", 16 | "link": "/b" 17 | }, 18 | { 19 | "text": "C test", 20 | "link": "/c" 21 | }, 22 | { 23 | "GitHub": "https://github.com/nuxt/press" 24 | } 25 | ], 26 | "sidebar": { 27 | "/": [ 28 | "/", 29 | { 30 | "title": "A test", 31 | "children": [ 32 | "/a", 33 | "/a/first", 34 | "/a/second" 35 | ] 36 | } 37 | ], 38 | "/c": [ 39 | "/c", 40 | "/a/second" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/prefix.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/docs/test/fixtures/prefix/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | header: true 4 | heroImage: /hero.png 5 | actionText: Get Started 6 | actionLink: /a/ 7 | features: 8 | - title: Feat 1 9 | details: Details 1 10 | - title: Feat 2 11 | details: Details 2 12 | - title: Feat 3 13 | details: Details 3 14 | footer: MIT Licensed 15 | --- 16 | 17 | # Custom Prefix 18 | 19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum 20 | -------------------------------------------------------------------------------- /packages/docs/test/unit/blueprint.generateRoutes.test.js: -------------------------------------------------------------------------------- 1 | import defu from 'defu' 2 | import * as utils from '@nuxt-press/utils' 3 | import Blueprint from '../../src/blueprint' 4 | 5 | jest.mock('@nuxt-press/utils') 6 | 7 | async function createInstance (config = {}, options) { 8 | Blueprint._runGuards = undefined 9 | Blueprint.templates = undefined 10 | 11 | const nuxt = { options: { css: [] } } 12 | options = defu({ id: 'my-test' }, options) 13 | 14 | const bp = new Blueprint(nuxt, options) 15 | bp.nuxt = nuxt 16 | bp.blueprintOptions = {} 17 | bp.loadConfig = jest.fn().mockReturnValue(config) 18 | bp.setLocales = _ => _ 19 | bp.coreSetup = _ => _ 20 | bp.rootConfig = {} 21 | await bp.setup() 22 | 23 | return bp 24 | } 25 | 26 | describe('docs blueprint', () => { 27 | test('createGenerateRoutes', async () => { 28 | const bp = await createInstance() 29 | 30 | bp.data = { 31 | sources: [ 32 | { path: '/my-path/' } 33 | ] 34 | } 35 | 36 | const prefix = jest.fn(p => p) 37 | const routes = await bp.createGenerateRoutes('/var/nuxt', prefix) 38 | 39 | await Promise.all(routes.map(route => route.payload)) 40 | 41 | expect(routes).toEqual([ 42 | { route: '/', payload: undefined }, 43 | { route: '/my-path/', payload: undefined } 44 | ]) 45 | }) 46 | 47 | test('generateExtendRoutes', async () => { 48 | const { normalizePathPrefix } = jest.requireActual('@nuxt-press/utils') 49 | utils.normalizePathPrefix.mockImplementation(normalizePathPrefix) 50 | 51 | const bp = await createInstance({ 52 | prefix: 'my-prefix' 53 | }) 54 | 55 | bp.getGenerateRoot = jest.fn().mockReturnValue('/var/nuxt/_press') 56 | bp.data = { 57 | sources: [ 58 | { path: '/my-path/' } 59 | ] 60 | } 61 | 62 | const routes = await bp.generateExtendRoutes() 63 | 64 | await Promise.all(routes.map(route => route.payload)) 65 | 66 | expect(routes).toEqual([ 67 | { route: '/my-prefix/', payload: undefined }, 68 | { route: '/my-path/', payload: undefined } 69 | ]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /packages/docs/test/unit/data.test.js: -------------------------------------------------------------------------------- 1 | import 'gray-matter' 2 | import fs from 'fs-extra' 3 | import { createBlueprintContext } from 'test-utils' 4 | import { _parsePage } from '../../src/blueprint/data' 5 | 6 | jest.mock('gray-matter') 7 | jest.mock('fs-extra') 8 | 9 | describe('parsePage', () => { 10 | afterEach(() => jest.resetAllMocks()) 11 | 12 | test('basic functionality', async () => { 13 | fs.readFile.mockReturnValue('') 14 | 15 | const thisContext = createBlueprintContext('docs') 16 | const pathInfo = { 17 | root: '/var/nuxt', 18 | prefix: '', 19 | path: 'index.md' 20 | } 21 | 22 | const source = await _parsePage.call(thisContext, pathInfo) 23 | 24 | expect(source.meta).toEqual({ metaTest: true }) 25 | expect(thisContext.config.source.markdown).toHaveBeenCalledTimes(1) 26 | expect(source.toc).toEqual(['the toc']) 27 | 28 | expect(source.source.type).toEqual('topic') 29 | expect(thisContext.config.source.title).toHaveBeenCalledTimes(1) 30 | expect(source.source.title).toEqual('the title') 31 | expect(source.source.body).toEqual('the html') 32 | 33 | expect(source.source.path).toEqual('/') 34 | expect(source.source.src).toEqual('/var/nuxt/index.md') 35 | }) 36 | 37 | test('doesnt return src path in production', async () => { 38 | fs.readFile.mockReturnValue('') 39 | 40 | const thisContext = createBlueprintContext('docs', { nuxt: { options: { dev: false } } }) 41 | const pathInfo = { 42 | root: '/var/nuxt', 43 | prefix: '', 44 | path: 'index.md' 45 | } 46 | 47 | const source = await _parsePage.call(thisContext, pathInfo) 48 | 49 | expect(source.source.src).toBeUndefined() 50 | }) 51 | 52 | test('adds correct locale from source path', async () => { 53 | fs.readFile.mockReturnValue('') 54 | 55 | const thisContext = createBlueprintContext('docs', { config: { locales: [{ code: 'en' }] } }) 56 | const pathInfo = { 57 | root: '/var/nuxt', 58 | prefix: '', 59 | path: 'en/index.md' 60 | } 61 | 62 | const source = await _parsePage.call(thisContext, pathInfo) 63 | 64 | expect(source.source.locale).toEqual('en') 65 | expect(source.source.path).toEqual('/en/') 66 | expect(source.source.src).toEqual('/var/nuxt/en/index.md') 67 | }) 68 | 69 | test('adds correct locale from prefix', async () => { 70 | fs.readFile.mockReturnValue('') 71 | 72 | const thisContext = createBlueprintContext('docs', { config: { locales: [{ code: 'en' }] } }) 73 | const pathInfo = { 74 | root: '/var/nuxt', 75 | prefix: 'en', 76 | path: 'index.md' 77 | } 78 | 79 | const source = await _parsePage.call(thisContext, pathInfo) 80 | 81 | expect(source.source.locale).toEqual('en') 82 | expect(source.source.path).toEqual('/en/') 83 | expect(source.source.src).toEqual('/var/nuxt/en/index.md') 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/pages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/pages", 3 | "version": "0.2.0-beta.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/nuxt/press", 6 | "license": "MIT", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@nuxt-press/core": "0.2.0-beta.0" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "scripts": { 17 | "dev": "nuxt dev", 18 | "build": "nuxt build", 19 | "start": "nuxt start", 20 | "press": "nuxt press" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/pages/src/blueprint.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import chokidar from 'chokidar' 3 | import { Blueprint as PressBlueprint } from '@nuxt-press/core' 4 | import { exists } from '@nuxt/blueprints' 5 | import { 6 | normalizeConfig, 7 | importModule 8 | } from '@nuxt-press/utils' 9 | 10 | import loadSources, { _parsePage } from './data' 11 | 12 | export default class PressPagesBlueprint extends PressBlueprint { 13 | static id = 'pages' 14 | 15 | static features = { 16 | singleton: true, 17 | localization: false 18 | } 19 | 20 | constructor (nuxt, options = {}) { 21 | super(nuxt, normalizeConfig(options)) 22 | } 23 | 24 | static async register (moduleContainer, config) { 25 | const nuxt = moduleContainer.nuxt 26 | 27 | const pagesRoot = path.join( 28 | nuxt.options.srcDir, 29 | nuxt.options.dir.pages 30 | ) 31 | 32 | if (await exists(pagesRoot)) { 33 | return { [this.id]: new this(nuxt, { id: this.id }) } 34 | } 35 | } 36 | 37 | loadData () { 38 | // this method is externalized to improve readability 39 | return loadSources.call(this) 40 | } 41 | 42 | buildDone () { 43 | if (!this.nuxt.options.dev) { 44 | return 45 | } 46 | 47 | const parsePage = _parsePage.bind(this) 48 | 49 | const watcher = chokidar.watch(['pages/**/*.md'], { 50 | cwd: this.options.srcDir, 51 | ignoreInitial: true, 52 | ignored: 'node_modules/**/*' 53 | }) 54 | watcher.on('change', async path => this.sseSourceEvent('change', await parsePage(path))) 55 | watcher.on('add', async path => this.sseSourceEvent('add', await parsePage(path))) 56 | watcher.on('unlink', path => this.sseSourceEvent('unlink', { path })) 57 | } 58 | 59 | createGenerateRoutes (rootDir, prefix) { 60 | if (!this.data || !this.data.sources) { 61 | return [] 62 | } 63 | 64 | return Object.keys(this.data.sources).map((route) => { 65 | return { 66 | route, 67 | payload: importModule(rootDir, 'sources', route) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/pages/src/data.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { exists, walk } from '@nuxt/blueprints' 3 | import { 4 | stripParagraph, 5 | filePathToWebpath, 6 | readTextFile, 7 | PromisePool 8 | } from '@nuxt-press/utils' 9 | 10 | // SLIDES MODE 11 | // Markdown files are loaded from the slides/ directory. 12 | // Configurable via press.slides.dir 13 | 14 | export async function _parsePage ({ root, path: sourcePath }, mdProcessor) { 15 | const raw = await readTextFile(root, sourcePath) 16 | 17 | const { meta, content } = await this.config.source.metadata(raw) 18 | 19 | // Use meta.title if given, otherwise use first H1 from body 20 | let title = meta.title 21 | if (!title) { 22 | [, title] = raw.match(/^#\s+(.*)/) || [] 23 | } 24 | 25 | if (title) { 26 | // TODO: why calling markdown on title? 27 | title = await this.config.source.markdown(title, mdProcessor) 28 | title = stripParagraph(title) 29 | } 30 | 31 | // Use metadata.body if given 32 | const body = await this.config.source.markdown(content || raw, mdProcessor) 33 | 34 | // Create the proper source path 35 | const webpath = filePathToWebpath(sourcePath) 36 | 37 | let src 38 | if (this.nuxt.options.dev) { 39 | src = path.join(root, sourcePath) 40 | } 41 | 42 | return { 43 | ...meta, 44 | title, 45 | body, 46 | src, 47 | path: webpath 48 | } 49 | } 50 | 51 | export default async function pagesData () { 52 | const pagesRoot = path.join( 53 | this.options.srcDir, 54 | this.options.dir.pages 55 | ) 56 | 57 | if (!await exists(pagesRoot)) { 58 | return {} 59 | } 60 | 61 | const sources = {} 62 | 63 | const parsePage = _parsePage.bind(this) 64 | const mdProcessor = await this.config.source.processor() 65 | const validate = filePath => filePath.endsWith('.md') 66 | const jobs = await walk(pagesRoot, { validate }) 67 | 68 | const handler = async (filePath) => { 69 | const page = await parsePage({ root: pagesRoot, path: filePath }, mdProcessor) 70 | sources[page.path] = page 71 | } 72 | 73 | const pool = new PromisePool(jobs, handler) 74 | await pool.done() 75 | 76 | return { 77 | sources 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/pages/src/index.js: -------------------------------------------------------------------------------- 1 | import { autoregister as _autoregister } from '@nuxt-press/core' 2 | import Blueprint from './blueprint' 3 | 4 | export { Blueprint } 5 | 6 | export function autoregister (options) { 7 | return _autoregister.call(this, options, [Blueprint.id]) 8 | } 9 | -------------------------------------------------------------------------------- /packages/pages/test/e2e/basic.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('basic', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/basic/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testHome () { 21 | expect(await page.getText('h1')).toBe('Index') 22 | expect(await page.getText('main p')).toBe('Lorem ipsum') 23 | } 24 | 25 | async function testPageContact () { 26 | expect(await page.getText('h1')).toBe('Contact you') 27 | expect(await page.getText('main p')).toBe('Contact me') 28 | } 29 | 30 | async function testPageAbout () { 31 | expect(await page.getText('h1')).toBe('About') 32 | expect(await page.getText('main p')).toBe('Well, ok then.') 33 | } 34 | 35 | test('open home', async () => { 36 | const url = browser.getUrl('/') 37 | 38 | page = await browser.page(url) 39 | 40 | await testHome() 41 | }) 42 | 43 | test('nav /contact/', async () => { 44 | await page.navigate('/contact/') 45 | 46 | await testPageContact() 47 | }) 48 | 49 | test('open contact', async () => { 50 | const url = browser.getUrl('/contact') 51 | 52 | page = await browser.page(url) 53 | 54 | await testPageContact() 55 | }) 56 | 57 | test('nav /about/', async () => { 58 | await page.navigate('/about/') 59 | 60 | await testPageAbout() 61 | }) 62 | 63 | test('open about', async () => { 64 | const url = browser.getUrl('/about') 65 | 66 | page = await browser.page(url) 67 | 68 | await testPageAbout() 69 | }) 70 | 71 | test('nav /', async () => { 72 | await page.navigate('/') 73 | 74 | await testHome() 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /packages/pages/test/fixtures/basic/basic.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/pages/test/fixtures/basic/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/pages/test/fixtures/basic/pages/about/index.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Well, ok then. 4 | -------------------------------------------------------------------------------- /packages/pages/test/fixtures/basic/pages/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contact me 3 | --- 4 | 5 | # Contact you 6 | 7 | Contact me 8 | -------------------------------------------------------------------------------- /packages/pages/test/fixtures/basic/pages/index.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | Lorem ipsum 4 | -------------------------------------------------------------------------------- /packages/pages/test/unit/blueprint.generateRoutes.test.js: -------------------------------------------------------------------------------- 1 | import defu from 'defu' 2 | import * as utils from '@nuxt-press/utils' 3 | import Blueprint from '../../src/blueprint' 4 | 5 | jest.mock('@nuxt-press/utils') 6 | 7 | async function createInstance (config = {}, options) { 8 | Blueprint._runGuards = undefined 9 | Blueprint.templates = undefined 10 | 11 | const nuxt = { options: { css: [] } } 12 | options = defu({ id: 'my-test' }, options) 13 | 14 | const bp = new Blueprint(nuxt, options) 15 | bp.nuxt = nuxt 16 | bp.blueprintOptions = {} 17 | bp.loadConfig = jest.fn().mockReturnValue(config) 18 | bp.setLocales = _ => _ 19 | bp.coreSetup = _ => _ 20 | bp.createApi = _ => _ 21 | bp.rootConfig = {} 22 | await bp.setup() 23 | 24 | return bp 25 | } 26 | 27 | describe('pages blueprint', () => { 28 | test('createGenerateRoutes', async () => { 29 | const { normalizePath } = jest.requireActual('@nuxt-press/utils') 30 | utils.normalizePath.mockImplementation(normalizePath) 31 | 32 | const bp = await createInstance() 33 | 34 | bp.data = { 35 | sources: { 36 | '/my-page/': {} 37 | } 38 | } 39 | 40 | const prefix = jest.fn(p => p) 41 | const routes = await bp.createGenerateRoutes('/var/nuxt', prefix) 42 | 43 | await Promise.all(routes.map(route => route.payload)) 44 | 45 | expect(routes).toEqual([ 46 | { route: '/my-page/', payload: undefined } 47 | ]) 48 | }) 49 | 50 | test('generateExtendRoutes', async () => { 51 | const { normalizePathPrefix } = jest.requireActual('@nuxt-press/utils') 52 | utils.normalizePathPrefix.mockImplementation(normalizePathPrefix) 53 | 54 | const bp = await createInstance() 55 | 56 | bp.getGenerateRoot = jest.fn().mockReturnValue('/var/nuxt/_press') 57 | bp.data = { 58 | sources: { 59 | '/my-page/': {} 60 | } 61 | } 62 | 63 | const routes = await bp.generateExtendRoutes() 64 | 65 | await Promise.all(routes.map(route => route.payload)) 66 | 67 | expect(routes).toEqual([ 68 | { route: '/my-page/', payload: undefined } 69 | ]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /packages/pages/test/unit/data.test.js: -------------------------------------------------------------------------------- 1 | import 'gray-matter' 2 | import fs from 'fs-extra' 3 | import { createBlueprintContext } from 'test-utils' 4 | import { _parsePage } from '../../src/data' 5 | 6 | jest.mock('gray-matter') 7 | jest.mock('fs-extra') 8 | 9 | describe('parsePage', () => { 10 | afterEach(() => jest.resetAllMocks()) 11 | 12 | test('basic functionality', async () => { 13 | fs.readFile.mockReturnValue(`# Slide 1 14 | 15 | Text 16 | 17 | # Slide 2 18 | 19 | Text`) 20 | 21 | const thisContext = createBlueprintContext('pages') 22 | const pathInfo = { 23 | root: '/var/nuxt', 24 | prefix: '', 25 | path: 'index.md' 26 | } 27 | 28 | const source = await _parsePage.call(thisContext, pathInfo) 29 | 30 | expect(source.meta).toBeUndefined() 31 | expect(source.metaTest).toBe(true) 32 | expect(thisContext.config.source.markdown).toHaveBeenCalledTimes(2) 33 | 34 | expect(source.type).toBeUndefined() 35 | expect(thisContext.config.source.title).not.toHaveBeenCalled() 36 | expect(source.title).toEqual('the html') 37 | expect(source.body).toEqual('the html') 38 | 39 | expect(source.path).toEqual('/') 40 | expect(source.src).toEqual('/var/nuxt/index.md') 41 | }) 42 | 43 | test('doesnt return src path in production', async () => { 44 | fs.readFile.mockReturnValue('') 45 | 46 | const thisContext = createBlueprintContext('pages', { nuxt: { options: { dev: false } } }) 47 | const pathInfo = { 48 | root: '/var/nuxt', 49 | prefix: '', 50 | path: 'index.md' 51 | } 52 | 53 | const source = await _parsePage.call(thisContext, pathInfo) 54 | 55 | expect(source.src).toBeUndefined() 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/slides/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/slides", 3 | "version": "0.2.0-beta.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/nuxt/press", 6 | "license": "MIT", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@nuxt-press/core": "0.2.0-beta.0", 12 | "keymaster": "^1.6.2", 13 | "swiper": "^5.1.0", 14 | "vue-awesome-swiper": "^3.1.3" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/api.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readJsonSync } from 'fs-extra' 3 | 4 | const cache = {} 5 | 6 | export default function slidesApi ({ rootDir, id, prefix, dev }) { 7 | return { 8 | index: (req, res, next) => { 9 | if (dev || !cache.index) { 10 | cache.index = readJsonSync(path.join(rootDir, id, prefix, 'index.json')) 11 | } 12 | res.json(cache.index) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/components/slides.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 71 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/data.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { 3 | readTextFile, 4 | createJobsFromConfig, 5 | filePathToWebpath, 6 | normalizePath, 7 | PromisePool 8 | } from '@nuxt-press/utils' 9 | 10 | // SLIDES MODE 11 | // Markdown files are loaded from the slides/ directory. 12 | // Configurable via press.slides.dir 13 | 14 | export async function _parseSlides ({ root, prefix = '', path: sourcePath }, mdProcessor) { 15 | const raw = await readTextFile(root, sourcePath) 16 | 17 | let slides = [] 18 | let c 19 | let i = 0 20 | let s = 0 21 | let escaped = false 22 | for (i = 0; i < raw.length; i++) { 23 | c = raw.charAt(i) 24 | if (c === '\n') { 25 | if (raw.charAt(i + 1) === '`' && raw.slice(i + 1, i + 4) === '```') { 26 | escaped = !escaped 27 | i = i + 3 28 | continue 29 | } 30 | if (escaped) { 31 | continue 32 | } 33 | if (raw.charAt(i + 1) === '#') { 34 | if (raw.slice(i + 2, i + 3) !== '#') { 35 | slides.push(raw.slice(s, i).trimStart()) 36 | s = i 37 | } 38 | } 39 | } 40 | } 41 | 42 | slides.push(slides.length > 0 43 | ? raw.slice(s, i).trimStart() 44 | : raw 45 | ) 46 | 47 | slides = await Promise.all( 48 | slides.filter(Boolean).map(slide => this.config.source.markdown(slide, mdProcessor)) 49 | ) 50 | 51 | const { name: fileName } = path.parse(sourcePath) 52 | const webpath = `${this.config.prefix}${filePathToWebpath(sourcePath, { prefix })}` 53 | 54 | const source = { 55 | type: 'slides', 56 | slides, 57 | path: webpath, 58 | ...this.nuxt.options.dev && { src: path.join(root, prefix, sourcePath) } 59 | } 60 | 61 | return source 62 | } 63 | 64 | export default async function slidesData () { 65 | const sources = {} 66 | 67 | const parseSlides = _parseSlides.bind(this) 68 | const mdProcessor = await this.config.source.processor() 69 | const jobs = await createJobsFromConfig(this.nuxt.options, this.config) 70 | 71 | const handler = async (path) => { 72 | const slides = await parseSlides(path, mdProcessor) 73 | sources[slides.path] = slides 74 | } 75 | 76 | const pool = new PromisePool(jobs, handler) 77 | await pool.done() 78 | 79 | const index = Object.values(sources) 80 | 81 | return { 82 | topLevel: { 83 | [`${this.config.prefix}/index`]: index 84 | }, 85 | sources 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import chokidar from 'chokidar' 3 | import { Blueprint as PressBlueprint } from '@nuxt-press/core' 4 | import { 5 | normalizeConfig, 6 | getDirsAsArray, 7 | importModule, 8 | normalizePath 9 | } from '@nuxt-press/utils' 10 | 11 | import loadSources, { _parseSlides } from './data' 12 | import api from './api' 13 | 14 | export default class PressSlidesBlueprint extends PressBlueprint { 15 | static id = 'slides' 16 | 17 | static features = { 18 | singleton: true, 19 | localization: false 20 | } 21 | 22 | static defaultConfig = { 23 | dir: 'slides', 24 | prefix: '/slides/' 25 | } 26 | 27 | constructor (nuxt, options = {}) { 28 | options = { 29 | dir: __dirname, 30 | ...normalizeConfig(options) 31 | } 32 | 33 | super(nuxt, options) 34 | } 35 | 36 | async setup () { 37 | await super.setup() 38 | 39 | this.addTheme(path.join(__dirname, '..', 'theme.css')) 40 | 41 | // we need this for index pages 42 | // see core/blueprint/plugins/press 43 | this.rootConfig.slidesPrefixes = this.rootConfig.slidesPrefixes || {} 44 | this.rootConfig.slidesPrefixes[this.id] = this.config.prefix 45 | 46 | const api = this.createApi() 47 | 48 | this.addServerMiddleware({ 49 | path: `/_press/slides${this.config.prefix}`, 50 | handler: (req, res, next) => { 51 | if (req.url === '/index.json') { 52 | api.index(req, res, next) 53 | return 54 | } 55 | 56 | next() 57 | } 58 | }) 59 | } 60 | 61 | async loadConfig (extraConfig) { 62 | const config = await super.loadConfig(extraConfig) 63 | 64 | config.api = api 65 | return config 66 | } 67 | 68 | loadData () { 69 | // this method is externalized to improve readability 70 | return loadSources.call(this) 71 | } 72 | 73 | createRoutes () { 74 | const routeName = `source-${this.id.toLowerCase()}` 75 | 76 | return [{ 77 | name: `${routeName}-index`, 78 | path: `${this.config.prefix}/`, 79 | component: this.templates['pages/index.vue'], 80 | meta: { id: this.id, bp: this.constructor.id } 81 | }, 82 | ...super.createRoutes() 83 | ] 84 | } 85 | 86 | createGenerateRoutes (rootDir, prefix) { 87 | return [ 88 | ...Object.keys(this.data.topLevel).map(route => ({ 89 | route: normalizePath(route, { index: false }), 90 | payload: importModule(rootDir, this.id, `${route}.json`) 91 | })), 92 | ...Object.keys(this.data.sources).map(route => ({ 93 | route, 94 | payload: importModule(rootDir, 'sources', route) 95 | })) 96 | ] 97 | } 98 | 99 | async buildDone () { 100 | if (!this.nuxt.options.dev) { 101 | return 102 | } 103 | 104 | const mdProcessor = await this.config.source.processor() 105 | 106 | // make sure watchPaths is an array 107 | const watchPaths = getDirsAsArray(this.config.dir) 108 | 109 | const watcher = chokidar.watch(watchPaths.map(path => `${path}${path ? '/' : ''}**/*.md`), { 110 | cwd: this.options.srcDir, 111 | ignoreInitial: true, 112 | ignored: 'node_modules/**/*' 113 | }) 114 | 115 | const parseSlides = _parseSlides.bind(this) 116 | 117 | watcher.on('change', async (path) => { 118 | const updatedSlides = await parseSlides.call(this, path, mdProcessor) 119 | this.sseSourceEvent('change', updatedSlides) 120 | }) 121 | watcher.on('add', async (path) => { 122 | const updatedSlides = await parseSlides.call(this, path, mdProcessor) 123 | this.sseSourceEvent('add', updatedSlides) 124 | }) 125 | watcher.on('unlink', path => this.sseSourceEvent('unlink', { path })) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/layouts/slides.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/pages/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/plugins/slides.client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import 'swiper/css/swiper.css' 3 | import VueAwesomeSwiper from 'vue-awesome-swiper' 4 | 5 | Vue.use(VueAwesomeSwiper) 6 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/slides/src/blueprint/svg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/slides/src/index.js: -------------------------------------------------------------------------------- 1 | import { autoregister as _autoregister } from '@nuxt-press/core' 2 | import Blueprint from './blueprint' 3 | 4 | export { Blueprint } 5 | 6 | export function autoregister (options) { 7 | return _autoregister.call(this, options, [Blueprint.id]) 8 | } 9 | -------------------------------------------------------------------------------- /packages/slides/src/theme.css: -------------------------------------------------------------------------------- 1 | #nuxt-press.slides { 2 | font-family: sans-serif; 3 | width: 100vw; 4 | height: 100vh; 5 | top: 0px; 6 | right: 0px; 7 | left: 0px; 8 | bottom: 0px; 9 | position: fixed; 10 | & .swiper-container { 11 | width: 100vw; 12 | height: 100vh; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | & .swiper-pagination-progressbar { 17 | top: auto; 18 | bottom: 0px; 19 | & .swiper-pagination-progressbar-fill { 20 | background-color: #42b883; 21 | } 22 | } 23 | & .swiper-button-prev { 24 | left: 10px; 25 | right: auto; 26 | background-image: url(~press/slides/svg/arrow-left.svg); 27 | } 28 | & .swiper-button-next { 29 | right: 10px; 30 | left: auto; 31 | background-image: url(~press/slides/svg/arrow-right.svg); 32 | } 33 | } 34 | & .swiper-slide { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | height: 100%; 39 | & > * { 40 | font-size: 2em; 41 | margin-bottom: 1.2em; 42 | } 43 | } 44 | & .swiper-slide:first-of-type { 45 | transform: scale(3.0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/slides/test/e2e/basic.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('basic', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/basic/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testHome () { 21 | expect(await page.getElementCount('li a')).toBe(1) 22 | expect(await page.getText('li a')).toBe('/presentation/') 23 | } 24 | 25 | async function testPagePresentation () { 26 | expect(await page.getElementCount('.slides .swiper-slide')).toBe(3) 27 | 28 | expect(await page.getText('.slides .slide-1 p')).toBe(`A 29 | Presentation`) 30 | 31 | expect(await page.getText('.slides .slide-2 h1')).toBe('Slide 1') 32 | expect(await page.getText('.slides .slide-2 p')).toBe('Text 1') 33 | 34 | expect(await page.getText('.slides .slide-3 h1')).toBe('Slide 2') 35 | expect(await page.getText('.slides .slide-3 p')).toBe('Text 2') 36 | } 37 | 38 | test('open home', async () => { 39 | const url = browser.getUrl('/') 40 | 41 | page = await browser.page(url) 42 | 43 | await testHome() 44 | }) 45 | 46 | test('nav /presentation/', async () => { 47 | await page.navigate('/presentation/') 48 | 49 | await testPagePresentation() 50 | }) 51 | 52 | test('open home', async () => { 53 | const url = browser.getUrl('/presentation') 54 | 55 | page = await browser.page(url) 56 | 57 | await testPagePresentation() 58 | }) 59 | 60 | test('nav /', async () => { 61 | await page.navigate('/') 62 | 63 | await testHome() 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /packages/slides/test/e2e/prefix.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { getPort, startBrowser } from 'test-utils' 3 | 4 | describe('prefix', () => { 5 | let browser 6 | let page 7 | 8 | beforeAll(async () => { 9 | const folder = path.resolve(__dirname, '..', 'fixtures/prefix/dist/') 10 | const port = await getPort() 11 | 12 | browser = await startBrowser({ folder, port }) 13 | 14 | // pass through browser errors, only works with chrome/puppeteer 15 | browser.setLogLevel(['log', 'info', 'warn', 'error']) 16 | }) 17 | 18 | afterAll(() => browser.close()) 19 | 20 | async function testHome () { 21 | expect(await page.getElementCount('li a')).toBe(1) 22 | expect(await page.getText('li a')).toBe('/my-presentations-archive/presentation/') 23 | } 24 | 25 | async function testPagePresentation () { 26 | expect(await page.getElementCount('.slides .swiper-slide')).toBe(3) 27 | 28 | expect(await page.getText('.slides .slide-1 p')).toBe('A Presentation') 29 | 30 | expect(await page.getText('.slides .slide-2 h1')).toBe('Slide 1') 31 | expect(await page.getText('.slides .slide-2 p')).toBe('Text 1') 32 | 33 | expect(await page.getText('.slides .slide-3 h1')).toBe('Slide 2') 34 | expect(await page.getText('.slides .slide-3 p')).toBe('Text 2') 35 | } 36 | 37 | test('open home', async () => { 38 | const url = browser.getUrl('/my-presentations-archive') 39 | 40 | page = await browser.page(url) 41 | 42 | await testHome() 43 | }) 44 | 45 | test('nav /presentation/', async () => { 46 | await page.navigate('/my-presentations-archive/presentation/') 47 | 48 | await testPagePresentation() 49 | }) 50 | 51 | test('open home', async () => { 52 | const url = browser.getUrl('/my-presentations-archive/presentation') 53 | 54 | page = await browser.page(url) 55 | 56 | await testPagePresentation() 57 | }) 58 | 59 | test('nav /', async () => { 60 | await page.navigate('/my-presentations-archive/') 61 | 62 | await testHome() 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/basic/basic.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/basic/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/basic/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "slides": { 3 | "dir": "", 4 | "prefix": "/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/basic/presentation.md: -------------------------------------------------------------------------------- 1 | A
2 | Presentation 3 | 4 | # Slide 1 5 | 6 | Text 1 7 | 8 | # Slide 2 9 | 10 | Text 2 11 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/prefix/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import NuxtPress from '@nuxt/press' 2 | 3 | export default { 4 | modules: [NuxtPress] 5 | } 6 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/prefix/nuxt.press.json: -------------------------------------------------------------------------------- 1 | { 2 | "slides": { 3 | "dir": "", 4 | "prefix": "/my-presentations-archive" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/prefix/prefix.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from 'test-utils/build' 2 | 3 | buildFixture({ dir: __dirname }) 4 | -------------------------------------------------------------------------------- /packages/slides/test/fixtures/prefix/presentation.md: -------------------------------------------------------------------------------- 1 | A Presentation 2 | 3 | # Slide 1 4 | 5 | Text 1 6 | 7 | # Slide 2 8 | 9 | Text 2 10 | -------------------------------------------------------------------------------- /packages/slides/test/unit/blueprint.generateRoutes.test.js: -------------------------------------------------------------------------------- 1 | import defu from 'defu' 2 | import * as utils from '@nuxt-press/utils' 3 | import Blueprint from '../../src/blueprint' 4 | 5 | jest.mock('@nuxt-press/utils') 6 | 7 | async function createInstance (config = {}, options) { 8 | Blueprint._runGuards = undefined 9 | Blueprint.templates = undefined 10 | 11 | const nuxt = { options: { css: [] } } 12 | options = defu({ id: 'my-test' }, options) 13 | 14 | const bp = new Blueprint(nuxt, options) 15 | bp.nuxt = nuxt 16 | bp.blueprintOptions = {} 17 | bp.loadConfig = jest.fn().mockReturnValue(config) 18 | bp.setLocales = _ => _ 19 | bp.coreSetup = _ => _ 20 | bp.createApi = _ => _ 21 | bp.addServerMiddleware = _ => _ 22 | bp.rootConfig = {} 23 | await bp.setup() 24 | 25 | return bp 26 | } 27 | 28 | describe('slides blueprint', () => { 29 | test('createGenerateRoutes', async () => { 30 | const { normalizePath } = jest.requireActual('@nuxt-press/utils') 31 | utils.normalizePath.mockImplementation(normalizePath) 32 | 33 | const bp = await createInstance() 34 | 35 | bp.data = { 36 | topLevel: { 37 | '/index': 'index' 38 | }, 39 | sources: { 40 | '/my-slides/': {} 41 | } 42 | } 43 | 44 | const prefix = jest.fn(p => p) 45 | const routes = await bp.createGenerateRoutes('/var/nuxt', prefix) 46 | 47 | await Promise.all(routes.map(route => route.payload)) 48 | 49 | expect(routes).toEqual([ 50 | { route: '/', payload: undefined }, 51 | { route: '/my-slides/', payload: undefined } 52 | ]) 53 | }) 54 | 55 | test('generateExtendRoutes', async () => { 56 | const { normalizePathPrefix } = jest.requireActual('@nuxt-press/utils') 57 | utils.normalizePathPrefix.mockImplementation(normalizePathPrefix) 58 | 59 | const bp = await createInstance({ 60 | prefix: 'my-prefix' 61 | }) 62 | 63 | bp.getGenerateRoot = jest.fn().mockReturnValue('/var/nuxt/_press') 64 | bp.data = { 65 | topLevel: { 66 | '/my-prefix/index': 'index' 67 | }, 68 | sources: { 69 | '/my-slides/': {} 70 | } 71 | } 72 | 73 | const routes = await bp.generateExtendRoutes() 74 | 75 | await Promise.all(routes.map(route => route.payload)) 76 | 77 | expect(routes).toEqual([ 78 | { route: '/my-prefix/', payload: undefined }, 79 | { route: '/my-slides/', payload: undefined } 80 | ]) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /packages/slides/test/unit/data.test.js: -------------------------------------------------------------------------------- 1 | import 'gray-matter' 2 | import fs from 'fs-extra' 3 | import { createBlueprintContext } from 'test-utils' 4 | import { _parseSlides } from '../../src/blueprint/data' 5 | 6 | jest.mock('gray-matter') 7 | jest.mock('fs-extra') 8 | 9 | describe('parsePage', () => { 10 | afterEach(() => jest.resetAllMocks()) 11 | 12 | test('basic functionality', async () => { 13 | fs.readFile.mockReturnValue(`# Slide 1 14 | 15 | Text 16 | 17 | # Slide 2 18 | 19 | Text`) 20 | 21 | const thisContext = createBlueprintContext('slides') 22 | const pathInfo = { 23 | root: '/var/nuxt', 24 | prefix: '', 25 | path: 'index.md' 26 | } 27 | 28 | const source = await _parseSlides.call(thisContext, pathInfo) 29 | 30 | expect(source.meta).toBeUndefined() 31 | expect(source.metaTest).toBeUndefined() 32 | expect(thisContext.config.source.markdown).toHaveBeenCalledTimes(2) 33 | 34 | expect(source.type).toEqual('slides') 35 | expect(thisContext.config.source.title).not.toHaveBeenCalled() 36 | 37 | expect(source.path).toEqual('/') 38 | expect(source.src).toEqual('/var/nuxt/index.md') 39 | 40 | expect(source.slides).toBeInstanceOf(Array) 41 | expect(source.slides).toEqual([ 42 | 'the html', 43 | 'the html' 44 | ]) 45 | }) 46 | 47 | test('doesnt return src path in production', async () => { 48 | fs.readFile.mockReturnValue('') 49 | 50 | const thisContext = createBlueprintContext('slides', { nuxt: { options: { dev: false } } }) 51 | const pathInfo = { 52 | root: '/var/nuxt', 53 | prefix: '', 54 | path: 'index.md' 55 | } 56 | 57 | const source = await _parseSlides.call(thisContext, pathInfo) 58 | 59 | expect(source.src).toBeUndefined() 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt-press/utils", 3 | "version": "0.2.0-beta.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/nuxt/press", 6 | "license": "MIT", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@nuxt/utils": "^2.10.1", 12 | "consola": "^2.10.1", 13 | "defu": "^0.0.3", 14 | "fs-extra": "^8.1.0", 15 | "klaw": "^3.0.0", 16 | "node-res": "^5.0.1", 17 | "slug": "^1.1.0" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "scripts": { 23 | "dev": "nuxt dev", 24 | "build": "nuxt build", 25 | "start": "nuxt start", 26 | "press": "nuxt press" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/utils/src/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import defu from 'defu' 3 | import { exists } from '@nuxt/blueprints' 4 | import { importModule } from './module' 5 | import { readJson, writeJson, ensureDir } from './fs' 6 | 7 | export const isPureObject = value => typeof value === 'object' && value !== null && !Array.isArray(value) 8 | 9 | function removePrivateKeys (source, target = {}) { 10 | target = target || {} 11 | 12 | for (const prop in source) { 13 | if (prop === '__proto__' || prop === 'constructor') { 14 | continue 15 | } 16 | 17 | // props starting with a $ are private 18 | if (prop.startsWith('$')) { 19 | continue 20 | } 21 | 22 | // we dont want to save source or api 23 | if (prop === 'source' || prop === 'api') { 24 | continue 25 | } 26 | 27 | const value = source[prop] 28 | // recursively check value if its an object 29 | if (isPureObject(value)) { 30 | target[prop] = {} 31 | 32 | removePrivateKeys(value, target[prop]) 33 | continue 34 | } 35 | 36 | target[prop] = value 37 | } 38 | return target 39 | } 40 | 41 | export function normalizeConfig (config) { 42 | // TODO: improve this 43 | if (typeof config === 'string') { 44 | config = { mode: config } 45 | } 46 | 47 | return config 48 | } 49 | 50 | export async function loadConfig ({ rootId, rootDir, config }) { 51 | const fileExtensions = ['js', 'json'] 52 | 53 | for (const fileExtension of fileExtensions) { 54 | const jsConfigPath = path.join(rootDir, `nuxt.${rootId}.${fileExtension}`) 55 | 56 | // JavaScript config has precedence over JSON config 57 | if (await exists(jsConfigPath)) { 58 | // load external config 59 | const externalConfig = await importModule(jsConfigPath) 60 | 61 | // apply defaults 62 | config = defu(externalConfig, config) 63 | config.configPath = jsConfigPath 64 | break 65 | } 66 | } 67 | 68 | return config 69 | } 70 | 71 | export async function saveConfig ({ rootId, id, options }) { 72 | // Copy object and remove props that start with $ 73 | // (These can be used for internal template pre-processing) 74 | const cleanedOptions = removePrivateKeys(options) 75 | 76 | // dont update when cleaned config is empty 77 | if (!Object.keys(cleanedOptions).length) { 78 | return 79 | } 80 | 81 | const config = { [id]: cleanedOptions } 82 | 83 | // ensure a rootId folder exists in buildDir 84 | const buildDirRoot = path.join(this.options.buildDir, rootId) 85 | await ensureDir(buildDirRoot) 86 | 87 | // If .js config found, do nothing 88 | // we only update JSON files, not JavaScript 89 | if (await exists(path.join(this.options.rootDir, `nuxt.${rootId}.js`))) { 90 | const config = await importModule(path.join(this.options.rootDir, `nuxt.${rootId}.js`)) 91 | 92 | await writeJson(path.join(buildDirRoot, 'config.json'), config, { spaces: 2 }) 93 | return 94 | } 95 | 96 | const configPath = path.join(this.options.rootDir, `nuxt.${rootId}.json`) 97 | if (!await exists(configPath)) { 98 | await writeJson(configPath, config, { spaces: 2 }) 99 | return 100 | } 101 | 102 | try { 103 | const existingConfig = await readJson(configPath, { throws: true }) 104 | 105 | const updated = defu(existingConfig || {}, config) 106 | 107 | await writeJson(configPath, updated, { spaces: 2 }) 108 | await writeJson(path.join(buildDirRoot, 'config.json'), updated, { spaces: 2 }) 109 | } catch (err) { 110 | // eslint-disable-next-line no-console 111 | console.warn(err) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/utils/src/fs.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import PromisePool from './pool' 4 | 5 | export { 6 | readFile, 7 | writeFile, 8 | readJson, 9 | writeJson, 10 | ensureDir 11 | } from 'fs-extra' 12 | 13 | export const readTextFile = (...paths) => fs.readFile(path.join(...paths), { encoding: 'utf8' }) 14 | 15 | export function resolve (...paths) { 16 | return path.resolve(__dirname, '..', path.join(...paths)) 17 | } 18 | 19 | export function getDirsAsArray (dirs) { 20 | if (Array.isArray(dirs)) { 21 | return dirs 22 | } 23 | 24 | if (typeof dirs === 'object') { 25 | return Object.keys(dirs) 26 | } 27 | 28 | return [dirs] 29 | } 30 | 31 | export async function saveFiles (files, rootDir, prepareFilepath, isJson) { 32 | const pool = new PromisePool(Object.keys(files), async (fileName) => { 33 | let filePath = path.join(rootDir, fileName) 34 | if (typeof prepareFilepath === 'function') { 35 | filePath = prepareFilepath(filePath, files[fileName]) 36 | } 37 | 38 | const fileDir = path.dirname(filePath) 39 | 40 | await fs.ensureDir(fileDir) 41 | const content = await files[fileName] 42 | 43 | if (isJson) { 44 | await fs.writeJson(filePath, content) 45 | } else { 46 | await fs.writeFile(filePath, content) 47 | } 48 | }) 49 | 50 | await pool.done() 51 | } 52 | 53 | export function saveJsonFiles (files, rootDir, prepareFilepath) { 54 | return saveFiles(files, rootDir, prepareFilepath, true) 55 | } 56 | -------------------------------------------------------------------------------- /packages/utils/src/index.js: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './fs' 3 | export * from './jobs' 4 | export * from './module' 5 | export * from './normalize' 6 | export * from './route' 7 | export * from './string' 8 | 9 | export { default as PromisePool } from './pool' 10 | export { default as SSE } from './sse' 11 | -------------------------------------------------------------------------------- /packages/utils/src/jobs.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { exists, walk } from '@nuxt/blueprints' 3 | import { getDirsAsArray } from './fs' 4 | 5 | export async function createJobsFromConfig (nuxtOptions, config) { 6 | const srcRoots = getDirsAsArray(config.dir) 7 | 8 | for (const key in srcRoots) { 9 | if (!await exists(nuxtOptions.srcDir, srcRoots[key])) { 10 | // eslint-disable-next-line no-console 11 | console.warn(`Source Folder ${srcRoots[key]} doesnt exist, ignoring it`) 12 | srcRoots.splice(key, 1) 13 | } 14 | } 15 | 16 | if (!srcRoots.length) { 17 | srcRoots.push(nuxtOptions.srcDir) 18 | } 19 | 20 | let srcPrefixes = null 21 | if (typeof config.dir === 'object') { 22 | srcPrefixes = config.dir 23 | } 24 | 25 | const validate = (path) => { 26 | // ignore pages folder 27 | if (path.startsWith(nuxtOptions.dir.pages)) { 28 | return false 29 | } 30 | 31 | return path.endsWith('.md') 32 | } 33 | 34 | const jobs = [] 35 | for (const srcRoot of srcRoots) { 36 | const srcPath = path.join(nuxtOptions.srcDir, srcRoot) 37 | const paths = await walk(srcPath, { validate }) 38 | 39 | jobs.push(...paths.map((path) => { 40 | let prefix = '' 41 | if (srcPrefixes && srcPrefixes[srcRoot]) { 42 | prefix = srcPrefixes[srcRoot] 43 | } 44 | 45 | return { 46 | root: srcPath, 47 | prefix, 48 | path 49 | } 50 | })) 51 | } 52 | 53 | return jobs 54 | } 55 | -------------------------------------------------------------------------------- /packages/utils/src/module.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export function interopDefault (m) { 4 | return m.default || m 5 | } 6 | 7 | export async function importModule (modulePath, ...modulePaths) { 8 | if (Array.isArray(modulePaths)) { 9 | modulePath = path.join(modulePath, ...modulePaths) 10 | } 11 | 12 | return interopDefault(await import(modulePath)) 13 | } 14 | -------------------------------------------------------------------------------- /packages/utils/src/normalize.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { trimSlashStart, trimSlashEnd, trimEnd } from './string' 3 | 4 | /* export function normalizeSourcePath (input, prefix) { 5 | // just convert any falsy value to 6 | // string without type checking 7 | if (!input) { 8 | input = '' 9 | } 10 | 11 | if (prefix && input.startsWith(prefix)) { 12 | input = input.substr(prefix.length) 13 | } 14 | 15 | if (input.endsWith('/index')) { 16 | return input.slice(0, input.indexOf('/index')) 17 | } 18 | 19 | if (input === 'index') { 20 | return '/' 21 | } 22 | 23 | return input || '/' 24 | } */ 25 | 26 | export const normalizePathPrefix = (prefix) => { 27 | return normalizePath(prefix, { start: true, end: false, empty: false }) 28 | } 29 | 30 | export const normalizePathSuffix = (prefix, end = true) => { 31 | return normalizePath(prefix, { start: false, end, empty: false }) 32 | } 33 | 34 | export function normalizePath (input, opts = {}) { 35 | const { 36 | start = true, // should start with slash? 37 | end = true, // should end with slash? 38 | index = undefined, // should end with 'index' 39 | empty = true // do something when input is empty? 40 | } = opts 41 | 42 | if (!input || (!empty && input === '/')) { 43 | // just convert any falsy value to 44 | // string without type checking 45 | input = '' 46 | 47 | if (!empty) { 48 | return input 49 | } 50 | } 51 | 52 | const endsWithSlash = input.endsWith('/') 53 | if (end & !endsWithSlash) { 54 | input = `${input}/` 55 | } else if (!end && endsWithSlash) { 56 | input = trimSlashEnd(input) 57 | } 58 | 59 | const startsWithSlash = input.startsWith('/') 60 | if (start && !startsWithSlash) { 61 | input = `/${input}` 62 | } else if (!start && startsWithSlash) { 63 | input = trimSlashStart(input) 64 | } 65 | 66 | if (index !== undefined) { 67 | const endsWithIndex = input.endsWith(`/index${end ? '/' : ''}`) 68 | if (index && !endsWithIndex) { 69 | input = `${input}${input === '/' || end ? '' : '/'}index${end ? '/' : ''}` 70 | } else if (!index && endsWithIndex) { 71 | input = trimEnd(input, `${end ? '' : '/'}index/?`) 72 | } 73 | } 74 | 75 | return input 76 | } 77 | 78 | // URL doesnt work properly without a base, set a random one 79 | export function normalizeURL (uri, { base = 'http://1ff0418ec8262bb2654d4108c436015d.nl', ...opts } = {}) { 80 | const url = new URL(uri, base) 81 | 82 | url.pathname = normalizePath(url.pathname, { 83 | index: false, 84 | ...opts 85 | }) 86 | 87 | return `${url.pathname}${url.search}${url.hash}` 88 | } 89 | 90 | export function normalizePaths (paths, opts) { 91 | if (Array.isArray(paths)) { 92 | for (const key in paths) { 93 | paths[key] = normalizePaths(paths[key], opts) 94 | } 95 | return paths 96 | } 97 | 98 | if (typeof paths === 'object') { 99 | if (paths.children) { 100 | paths.children = normalizePaths(paths.children) 101 | return paths 102 | } 103 | 104 | for (const key in paths) { 105 | const normalizedKey = normalizePath(key, opts) 106 | paths[normalizedKey] = normalizePaths(paths[key]) 107 | 108 | if (key !== normalizedKey) { 109 | delete paths[key] 110 | } 111 | } 112 | 113 | return paths 114 | } 115 | 116 | return normalizePath(paths, opts) 117 | } 118 | -------------------------------------------------------------------------------- /packages/utils/src/pool.js: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import consola from 'consola' 3 | import { waitFor } from '@nuxt/utils' 4 | 5 | const failureInterval = 3000 6 | const maxRetries = 0 // use 0 for debugging 7 | const pool = new Array(os.cpus().length).fill(null) 8 | 9 | export default class PromisePool { 10 | constructor (jobs, handler) { 11 | this.handler = handler 12 | this.jobs = jobs.map(payload => ({ payload })) 13 | } 14 | 15 | async done (before) { 16 | if (before) { 17 | await before() 18 | } 19 | 20 | await Promise.all(pool.map(() => { 21 | return new Promise(async (resolve) => { 22 | while (this.jobs.length) { 23 | let job 24 | try { 25 | job = this.jobs.pop() 26 | await this.handler(job.payload) 27 | } catch (err) { 28 | if (job.retries && job.retries === maxRetries) { 29 | consola.warn('Job exceeded retry limit: ', job) 30 | break 31 | } 32 | 33 | if (maxRetries > 0) { 34 | job.retries = job.retries ? job.retries + 1 : 1 35 | 36 | await waitFor(failureInterval) 37 | this.jobs.unshift(job) 38 | 39 | consola.warn('Requeued job due to failure: ', job, err) 40 | break 41 | } 42 | 43 | consola.warn('Job failed: ', job, err) 44 | } 45 | } 46 | resolve() 47 | }) 48 | })) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/utils/src/route.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { normalizePathPrefix, normalizePath } from './normalize' 3 | 4 | export const indexKeys = ['index', 'readme'] 5 | 6 | export const indexKeysRE = new RegExp(`(^|/)(${indexKeys.join('|')})/?$`, 'i') 7 | 8 | export function filePathToWebpath (filePath, opts = {}) { 9 | const { 10 | extension = '', 11 | prefix, 12 | strip = indexKeysRE, 13 | sep = path.sep 14 | } = opts 15 | 16 | let webpath = filePath 17 | 18 | if (sep === '\\') { 19 | webpath = webpath.replace(/\\/g, '/') 20 | } 21 | 22 | // strip extension 23 | if (extension && webpath.endsWith(extension)) { 24 | webpath = webpath.slice(0, -1 * extension.length) 25 | } else { 26 | webpath = webpath.substr(0, webpath.lastIndexOf('.')) 27 | } 28 | 29 | if (strip) { 30 | webpath = webpath.replace(strip, '') 31 | } 32 | 33 | let webprefix 34 | if (prefix) { 35 | webprefix = normalizePathPrefix(prefix) 36 | } else { 37 | webprefix = '' 38 | } 39 | 40 | return `${webprefix}${normalizePath(webpath)}` 41 | } 42 | -------------------------------------------------------------------------------- /packages/utils/src/sse.js: -------------------------------------------------------------------------------- 1 | // Based on Harminder Virk's work on 2 | // https://github.com/dimerapp/cli/blob/d6554e7ffd0381f283643a54feae429d2ff01cef/src/services/SSE.ts 3 | 4 | import { status, header } from 'node-res' 5 | 6 | export default class SSE { 7 | constructor () { 8 | this.subscriptions = new Set() 9 | this.counter = 0 10 | } 11 | 12 | // Subscribe to a channel and set initial headers 13 | subscribe (req, res) { 14 | req.socket.setTimeout(0) 15 | 16 | status(res, 200) 17 | header(res, 'Content-Type', 'text/event-stream') 18 | header(res, 'Cache-Control', 'no-cache') 19 | header(res, 'Connection', 'keep-alive') 20 | 21 | this.subscriptions.add(res) 22 | res.on('close', () => this.subscriptions.delete(res)) 23 | this.broadcast('ready', {}) 24 | } 25 | 26 | // Publish event and data to all connected clients 27 | broadcast (event, data) { 28 | this.counter++ 29 | // Do console.log(this.subscriptions.size) to see, if there are any memory leaks 30 | for (const res of this.subscriptions) { 31 | this.clientBroadcast(res, event, data) 32 | } 33 | } 34 | 35 | // Publish event and data to a given response object 36 | clientBroadcast (res, event, data) { 37 | res.write(`id: ${this.counter}\n`) 38 | res.write(`event: message\n`) 39 | res.write(`data: ${JSON.stringify({ event, ...data })}\n\n`) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/utils/src/string.js: -------------------------------------------------------------------------------- 1 | import slug from 'slug' 2 | 3 | export function stripParagraph (str) { 4 | str = str.replace(/^

/, '') 5 | return str.replace(/<\/p>$/, '') 6 | } 7 | 8 | export function trimStart (str, chr = '') { 9 | if (!chr) { 10 | return str.trimStart() 11 | } 12 | 13 | return str.replace(new RegExp(`^(${chr})+`, 'i'), '') 14 | } 15 | 16 | export function trimEnd (str, chr = '') { 17 | if (!chr) { 18 | return str.trimEnd() 19 | } 20 | 21 | return str.replace(new RegExp(`(${chr})+$`, 'i'), '') 22 | } 23 | 24 | export const trimSlashStart = str => str.replace(new RegExp(`^/+`), '') 25 | export const trimSlashEnd = str => str.replace(new RegExp(`/+$`), '') 26 | 27 | const escapeREs = {} 28 | export function escapeChars (str, chars = '"') { 29 | if (Array.isArray(chars)) { 30 | chars = chars.join() 31 | } 32 | 33 | if (!escapeREs[chars]) { 34 | escapeREs[chars] = new RegExp(`([${chars}])`, 'g') 35 | } 36 | 37 | const escapeRE = escapeREs[chars] 38 | 39 | return str.replace(escapeRE, '\\$1') 40 | } 41 | 42 | export function slugify (str) { 43 | return slug(str, { lower: true }) 44 | } 45 | 46 | export function markdownToText (markdown) { 47 | // fully strip code blocks 48 | markdown = markdown.replace(/]*>[\s\S]*?<\/code>/gmi, '') 49 | 50 | // strip other html tags 51 | markdown = markdown.replace(/<\/?[^>]+(>|$)/g, '') 52 | 53 | return markdown 54 | } 55 | -------------------------------------------------------------------------------- /packages/utils/test/unit/route.test.js: -------------------------------------------------------------------------------- 1 | import { filePathToWebpath } from '../../src/route' 2 | 3 | const fileInputs = [ 4 | '/index.md', 5 | '/README.md', 6 | '/a-path.txt', 7 | '/sub.folder/sub.path.md' 8 | ] 9 | 10 | describe('route', () => { 11 | test('normalizePath { default opts }', () => { 12 | const outputs = fileInputs.map(i => filePathToWebpath(i, { sep: '/' })) 13 | 14 | expect(outputs).toEqual([ 15 | '/', 16 | '/', 17 | '/a-path/', 18 | '/sub.folder/sub.path/' 19 | ]) 20 | }) 21 | 22 | test('normalizePath { prefix: \'my-prefix\' }', () => { 23 | const outputs = fileInputs.map(i => filePathToWebpath(i, { sep: '/', prefix: 'my-prefix' })) 24 | 25 | expect(outputs).toEqual([ 26 | '/my-prefix/', 27 | '/my-prefix/', 28 | '/my-prefix/a-path/', 29 | '/my-prefix/sub.folder/sub.path/' 30 | ]) 31 | }) 32 | 33 | test('normalizePath { extension: \'.md\' }', () => { 34 | const outputs = fileInputs.map(i => filePathToWebpath(i, { sep: '/', extension: '.md' })) 35 | 36 | expect(outputs).toEqual([ 37 | '/', 38 | '/', 39 | '/a-path/', 40 | '/sub.folder/sub.path/' 41 | ]) 42 | }) 43 | 44 | test('normalizePath { strip: false }', () => { 45 | const outputs = fileInputs.map(i => filePathToWebpath(i, { sep: '/', strip: false })) 46 | 47 | expect(outputs).toEqual([ 48 | '/index/', 49 | '/README/', 50 | '/a-path/', 51 | '/sub.folder/sub.path/' 52 | ]) 53 | }) 54 | 55 | test('normalizePath { windows separator }', () => { 56 | const fileInputs = [ 57 | '\\index.md', 58 | '\\readme.md', 59 | '\\a-path.txt', 60 | '\\sub.folder\\sub.path.md' 61 | ] 62 | 63 | const outputs = fileInputs.map(i => filePathToWebpath(i, { sep: '\\' })) 64 | 65 | expect(outputs).toEqual([ 66 | '/', 67 | '/', 68 | '/a-path/', 69 | '/sub.folder/sub.path/' 70 | ]) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readJSONSync } from 'fs-extra' 3 | import commonjsPlugin from 'rollup-plugin-commonjs' 4 | import autoExternalPlugin from 'rollup-plugin-auto-external' 5 | import copyPlugin from 'rollup-plugin-copy' 6 | 7 | const rootDir = process.cwd() 8 | const inputs = { 9 | index: 'src/index.js', 10 | cli: 'src/cli.js' 11 | } 12 | const pkg = readJSONSync(path.resolve(rootDir, 'package.json')) 13 | const name = path.basename(pkg.name) 14 | 15 | export default [ 16 | { 17 | input: path.resolve(rootDir, inputs.index), 18 | output: { 19 | dir: path.resolve(rootDir, 'dist'), 20 | entryFileNames: `nuxt-${name}.js`, 21 | chunkFileNames: `nuxt-${name}-[name].js`, 22 | format: 'cjs', 23 | preferConst: true 24 | }, 25 | plugins: [ 26 | autoExternalPlugin(), 27 | commonjsPlugin(), 28 | copyPlugin({ 29 | targets: [ 30 | { src: 'src/blueprints', dest: 'dist' } 31 | ] 32 | }) 33 | ] 34 | }, 35 | { 36 | input: path.resolve(rootDir, inputs.cli), 37 | output: { 38 | dir: path.resolve(rootDir, 'dist'), 39 | entryFileNames: `nuxt-${name}-cli.js`, 40 | chunkFileNames: `nuxt-${name}-cli-[name].js`, 41 | format: 'cjs', 42 | preferConst: true 43 | }, 44 | plugins: [ 45 | autoExternalPlugin(), 46 | commonjsPlugin() 47 | ] 48 | } 49 | ] 50 | -------------------------------------------------------------------------------- /scripts/workspace-run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | for dir in packages/* distributions/* ; do 5 | # echo "$dir" 6 | pushd $dir > /dev/null 7 | $@ 8 | popd > /dev/null 9 | done 10 | 11 | -------------------------------------------------------------------------------- /test/utils/blueprint.js: -------------------------------------------------------------------------------- 1 | import defu from 'defu' 2 | 3 | export function createBlueprintContext (blueprint, overrides = {}) { 4 | return defu(overrides, { 5 | constructor: { 6 | }, 7 | nuxt: { 8 | options: { 9 | dev: true 10 | } 11 | }, 12 | config: { 13 | prefix: '', 14 | locales: undefined, 15 | source: { 16 | markdown: jest.fn().mockImplementation(() => { 17 | if (blueprint === 'docs') { 18 | return { 19 | toc: ['the toc'], 20 | html: 'the html' 21 | } 22 | } 23 | 24 | return 'the html' 25 | }), 26 | metadata: jest.fn().mockReturnValue({ 27 | meta: { 28 | metaTest: true 29 | } 30 | }), 31 | title: jest.fn().mockReturnValue('the title'), 32 | id: jest.fn().mockReturnValue('the rss id'), 33 | path: jest.fn().mockReturnValue('the_path') 34 | } 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /test/utils/browser.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import env from 'node-env-file' 4 | import { createBrowser } from 'tib' 5 | import { browserString, useBrowserstackLocal } from '.' 6 | 7 | export function startBrowser ({ folder, port, extendPage = {} }) { 8 | if (useBrowserstackLocal) { 9 | const envFile = path.resolve(__dirname, '..', '..', '.env-browserstack') 10 | 11 | if (fs.existsSync(envFile)) { 12 | env(envFile) 13 | } 14 | } 15 | 16 | return createBrowser(browserString, { 17 | staticServer: { 18 | folder, 19 | port 20 | }, 21 | extendPage (page) { 22 | return { 23 | async navigate (path) { 24 | await page.runAsyncScript((path) => { 25 | return new Promise((resolve) => { 26 | // timeout after 10s 27 | const timeout = setTimeout(function () { 28 | console.error('browser: nuxt navigation timed out') 29 | window.$nuxt.$emit('triggerScroll') 30 | }, 10000) 31 | 32 | window.$nuxt.$once('triggerScroll', () => { 33 | clearTimeout(timeout) 34 | setTimeout(resolve, 250) 35 | }) 36 | window.$nuxt.$router.push(path) 37 | }) 38 | }, path) 39 | }, 40 | routeData () { 41 | return page.runScript(() => ({ 42 | path: window.$nuxt.$route.path, 43 | query: window.$nuxt.$route.query 44 | })) 45 | }, 46 | ...extendPage 47 | } 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /test/utils/build.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { loadFixture, Nuxt, Builder, BundleBuilder, Generator, listPaths } from '.' 3 | 4 | export function buildFixture ({ dir, callback, hooks = [], changedPaths = [] }) { 5 | const pathsBefore = {} 6 | let nuxt 7 | 8 | const fixture = path.basename(dir) 9 | 10 | test(`Build ${fixture}`, async () => { 11 | const config = await loadFixture(dir, { _generate: true }) 12 | nuxt = new Nuxt(config) 13 | 14 | pathsBefore.root = await listPaths(nuxt.options.rootDir) 15 | if (nuxt.options.rootDir !== nuxt.options.srcDir) { 16 | pathsBefore.src = await listPaths(nuxt.options.srcDir) 17 | } 18 | 19 | const buildDone = jest.fn() 20 | hooks.forEach(([hook, fn]) => nuxt.hook(hook, fn)) 21 | nuxt.hook('build:done', buildDone) 22 | 23 | const builder = await new Builder(nuxt, BundleBuilder) 24 | const generator = new Generator(nuxt, builder) 25 | 26 | await generator.generate({ init: true, build: true }) 27 | 28 | // 2: BUILD_DONE 29 | expect(builder._buildStatus).toBe(2) 30 | expect(buildDone).toHaveBeenCalledTimes(1) 31 | 32 | if (typeof callback === 'function') { 33 | callback(builder) 34 | } 35 | }, 120000) 36 | 37 | test('Check changed files', async () => { 38 | expect.hasAssertions() 39 | 40 | const allowedPaths = [ 41 | nuxt.options.buildDir, 42 | nuxt.options.generate.dir, 43 | `${nuxt.options.srcDir}$`, 44 | `${nuxt.options.srcDir}/nuxt.press.json$`, 45 | path.join(nuxt.options.srcDir, nuxt.options.dir.pages), // TODO: we shouldnt always have to touch pages dir 46 | ...changedPaths.map(p => path.isAbsolute(p) ? p : path.join(nuxt.options.srcDir, p)) 47 | ] 48 | 49 | const allowedPathsRE = new RegExp(`^(${allowedPaths.join('|')})`) 50 | 51 | // When building Nuxt we only expect files to changed 52 | // within the nuxt.options.buildDir 53 | for (const key in pathsBefore) { 54 | const paths = await listPaths(nuxt.options[`${key}Dir`], pathsBefore[key]) 55 | 56 | for (const item of paths) { 57 | expect(item.path).toEqual(expect.stringMatching(allowedPathsRE)) 58 | } 59 | } 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | import klaw from 'klaw' 2 | 3 | export { default as getPort } from 'get-port' 4 | 5 | export * from './nuxt' 6 | export * from './blueprint' 7 | export * from './browser' 8 | 9 | export const browserString = process.env.BROWSER_STRING || 'puppeteer/core/staticserver' 10 | 11 | export const useBrowserstackLocal = browserString.includes('browserstack') && browserString.includes('local') 12 | 13 | export function listPaths (dir, pathsBefore = [], options = {}) { 14 | const items = [] 15 | return new Promise((resolve) => { 16 | klaw(dir, options) 17 | .on('data', (item) => { 18 | const foundItem = pathsBefore.find(itemBefore => item.path === itemBefore.path) 19 | 20 | if (typeof foundItem === 'undefined' || item.stats.mtimeMs !== foundItem.stats.mtimeMs) { 21 | items.push(item) 22 | } 23 | }) 24 | .on('end', () => resolve(items)) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /test/utils/nuxt.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defaultsDeep } from 'lodash' 3 | import NuxtPress from 'pressModule' 4 | 5 | export { Nuxt, Builder, BundleBuilder, Generator } from 'nuxt' 6 | export * from '@nuxt/utils' 7 | 8 | export async function loadFixture (fixture, overrides) { 9 | const rootDir = path.isAbsolute(fixture) ? fixture : path.resolve(__dirname, '..', 'fixtures', fixture) 10 | let config = {} 11 | 12 | try { 13 | config = await import(`${rootDir}/nuxt.config`) 14 | config = config.default || config 15 | } catch (e) { 16 | // Ignore MODULE_NOT_FOUND 17 | if (e.code !== 'MODULE_NOT_FOUND') { 18 | throw e 19 | } 20 | } 21 | 22 | if (typeof config === 'function') { 23 | config = await config() 24 | } 25 | 26 | config.rootDir = rootDir 27 | config.dev = false 28 | config.test = true 29 | 30 | // disable terser to speed-up fixture builds 31 | if (config.build) { 32 | if (!config.build.terser) { 33 | config.build.terser = false 34 | } 35 | } else { 36 | config.build = { terser: false } 37 | } 38 | 39 | config.modules = config.modules || [] 40 | const moduleName = NuxtPress.name 41 | 42 | let hasNuxtPress = false 43 | if (config.modules) { 44 | hasNuxtPress = config.modules.some((m) => { 45 | return (typeof m === 'function' && m.name === moduleName) || (Array.isArray(m) && m[0].name === moduleName) 46 | }) 47 | } 48 | 49 | if (!hasNuxtPress) { 50 | config.modules.push(NuxtPress) 51 | } 52 | 53 | return defaultsDeep({}, overrides, config) 54 | } 55 | -------------------------------------------------------------------------------- /test/utils/setup.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import chalk from 'chalk' 3 | import exit from 'exit' 4 | 5 | chalk.enabled = false 6 | 7 | jest.setTimeout(60000) 8 | 9 | consola.mockTypes(() => jest.fn()) 10 | 11 | function errorTrap (error) { 12 | process.stderr.write('\n' + error.stack + '\n') 13 | exit(1) 14 | } 15 | 16 | process.on('unhandledRejection', errorTrap) 17 | process.on('uncaughtException', errorTrap) 18 | --------------------------------------------------------------------------------