├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── blueprint │ ├── components │ │ └── hello.tmpl.vue │ ├── css │ │ └── style.tmpl.css │ ├── index.js │ └── plugins │ │ └── example.tmpl.js ├── nuxt.config.js └── pages │ └── index.vue ├── jest.config.js ├── nuxt.config.js ├── package.json ├── src ├── blueprint.js ├── cli │ ├── commands │ │ ├── eject.js │ │ └── index.js │ └── index.js ├── index.js └── utils │ ├── fs.js │ ├── guards.js │ ├── index.js │ └── string.js ├── test ├── unit │ ├── blueprint.test.js │ ├── cli.eject-helpers.test.js │ ├── cli.eject.test.js │ ├── cli.test.js │ └── utils.test.js └── utils │ ├── index.js │ └── setup.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | node: 5 | parameters: 6 | browsers: 7 | type: boolean 8 | default: false 9 | docker: 10 | - image: circleci/node:latest<<# parameters.browsers >>-browsers<> 11 | working_directory: ~/project 12 | environment: 13 | NODE_ENV: test 14 | 15 | commands: 16 | attach-project: 17 | steps: 18 | - checkout 19 | - attach_workspace: 20 | at: ~/project 21 | 22 | jobs: 23 | setup: 24 | executor: node 25 | steps: 26 | - checkout 27 | - restore_cache: 28 | key: yarn-{{ checksum "yarn.lock" }} 29 | - run: 30 | name: Install Dependencies 31 | command: NODE_ENV=dev yarn 32 | - save_cache: 33 | key: yarn-{{ checksum "yarn.lock" }} 34 | paths: 35 | - "node_modules" 36 | - persist_to_workspace: 37 | root: ~/project 38 | paths: 39 | - node_modules 40 | 41 | lint: 42 | executor: node 43 | steps: 44 | - attach-project 45 | - run: 46 | name: Lint 47 | command: yarn lint 48 | 49 | audit: 50 | executor: node 51 | steps: 52 | - attach-project 53 | - run: 54 | name: Security Audit 55 | command: yarn audit --groups dependencies 56 | 57 | build: 58 | executor: node 59 | steps: 60 | - attach-project 61 | - run: 62 | name: Type Tests 63 | command: yarn dist || true 64 | - persist_to_workspace: 65 | root: ~/project 66 | paths: 67 | - dist 68 | 69 | test-unit: 70 | executor: node 71 | steps: 72 | - attach-project 73 | - run: 74 | name: Unit Tests 75 | command: yarn test:unit --coverage && yarn coverage 76 | 77 | test-types: 78 | executor: node 79 | steps: 80 | - attach-project 81 | - run: 82 | name: Type Tests 83 | command: yarn test:types 84 | 85 | test-fixtures: 86 | executor: node 87 | steps: 88 | - attach-project 89 | - run: 90 | name: Test fixtures 91 | command: yarn test:fixtures --maxWorkers=2 --coverage && yarn coverage 92 | - persist_to_workspace: 93 | root: ~/project 94 | paths: 95 | - packages/*/test/fixtures/*/dist 96 | 97 | test-e2e: 98 | parameters: 99 | browserString: 100 | type: string 101 | executor: 102 | name: node 103 | browsers: true 104 | steps: 105 | - attach-project 106 | - run: 107 | name: E2E Tests 108 | command: yarn test:e2e 109 | environment: 110 | BROWSER_STRING: << parameters.browserString >> 111 | 112 | workflows: 113 | version : 2 114 | 115 | commit: 116 | jobs: 117 | - setup 118 | - lint: { requires: [setup] } 119 | - audit: { requires: [setup] } 120 | # - build: { requires: [setup] } 121 | - test-unit: { requires: [lint] } 122 | # - test-types: { requires: [lint] } 123 | # - test-fixtures: { requires: [setup] } 124 | # - test-e2e: 125 | # name: test-e2e-firefox 126 | # browserString: firefox/headless 127 | # requires: [test-fixtures] 128 | # - test-e2e: 129 | # name: test-e2e-chrome 130 | # browserString: puppeteer/core 131 | # requires: [test-fixtures] 132 | # - test-e2e-browser: 133 | # name: test-e2e-ie 134 | # browserString: browserstack/local/windows 7/ie:9 135 | # requires: [test-e2e-ssr] 136 | # filters: 137 | # branches: { ignore: /^pull\/.*/ } 138 | # - test-e2e-browser: 139 | # name: test-e2e-edge 140 | # browserString: browserstack/local/edge:15 141 | # requires: [test-e2e-ssr] 142 | # filters: 143 | # branches: { ignore: /^pull\/.*/ } 144 | # - test-e2e-browser: 145 | # name: test-e2e-safari 146 | # browserString: browserstack/local/os x=snow leopard/safari:5.1 147 | # requires: [test-e2e-ssr] 148 | # filters: 149 | # branches: { ignore: /^pull\/.*/ } 150 | 151 | 152 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "parser": "babel-eslint" 4 | }, 5 | "extends": [ 6 | "@nuxtjs" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Lock files 5 | package-lock.json 6 | 7 | # Logs 8 | *.log 9 | 10 | # Build/dist folders 11 | .nuxt* 12 | dist 13 | 14 | # Coverage reports 15 | reports 16 | coverage 17 | *.lcov 18 | .nyc_output 19 | 20 | # Editors 21 | *.iml 22 | .idea 23 | .vscode 24 | 25 | # OSX 26 | .DS_Store 27 | .AppleDouble 28 | .LSOverride 29 | 30 | # Env vars 31 | .env* 32 | 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.1.0-beta.4](https://github.com/nuxt/blueprints/compare/v0.1.0-beta.3...v0.1.0-beta.4) (2019-10-11) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * allow implementor to override pathPrefix, default is to use the id ([cb47cf0](https://github.com/nuxt/blueprints/commit/cb47cf01a4401611b40f130f720ad1c3cf444b9f)) 11 | 12 | ## [0.1.0-beta.3](https://github.com/nuxt/blueprints/compare/v0.1.0-beta.2...v0.1.0-beta.3) (2019-10-10) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * strip leading slash from app templates ([0672b74](https://github.com/nuxt/blueprints/commit/0672b7436c63bfc5efb8d3fc77770b3071e0c6e7)) 18 | 19 | ## [0.1.0-beta.2](https://github.com/nuxt/blueprints/compare/v0.1.0-beta.1...v0.1.0-beta.2) (2019-10-10) 20 | 21 | 22 | ### Features 23 | 24 | * use serveStatic server middleware for static files ([e8e2e4f](https://github.com/nuxt/blueprints/commit/e8e2e4f)) 25 | 26 | ## [0.1.0-beta.1](https://github.com/nuxt/blueprints/compare/v0.1.0-beta.0...v0.1.0-beta.1) (2019-10-02) 27 | 28 | ## [0.1.0-beta.0](https://github.com/nuxt/blueprints/compare/v0.1.0-alpha.1...v0.1.0-beta.0) (2019-10-02) 29 | 30 | ## [0.1.0-alpha.1](https://github.com/nuxt/blueprints/compare/v0.1.0-alpha.0...v0.1.0-alpha.1) (2019-09-11) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * support adding non-template style files ([6f3c206](https://github.com/nuxt/blueprints/commit/6f3c206)) 36 | * **pkg:** use publishConfig ([2d23c48](https://github.com/nuxt/blueprints/commit/2d23c48)) 37 | 38 | ## 0.1.0-alpha.0 (2019-09-11) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * dont add relative css ([fee1433](https://github.com/nuxt/blueprints/commit/fee1433)) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Nuxt.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @nuxt/blueprints 2 | 3 | > Module for Nuxt.js to create distributable micro-apps 4 | 5 | [![npm version][npm-version-src]][npm-version-href] 6 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 7 | [![Circle CI][circle-ci-src]][circle-ci-href] 8 | [![Codecov][codecov-src]][codecov-href] 9 | [![License][license-src]][license-href] 10 | 11 | ## :construction: WIP 12 | 13 | This module is considered experimental and a work-in-progress. 14 | 15 | ## Examples 16 | 17 | Check the [example](./example) for a simple blueprint example. 18 | 19 | If you are looking for a more advanced example, have a look at the [NuxtPress repository](https://github.com/nuxt/press) which is also build using blueprints. 20 | 21 | #### Node v12.4 required 22 | 23 | If you wish to run the example from this repo, you need to use at least [Node v12.4.0](https://node.green/#ESNEXT-candidate--stage-3--static-class-fields) due to the use of [static class features](https://github.com/tc39/proposal-static-class-features/). Those are transpiled on release using [@babel/plugin-proposal-class-properties](https://babeljs.io/docs/en/babel-plugin-proposal-class-properties), but the example runs from source. 24 | 25 | ## Quick Docs 26 | 27 | The blueprint module is a _supercharged_ module container which supports autodiscovery of folders and resolving of templates/files. 28 | 29 | Features: 30 | - Define a template by adding the template identifiers `tmpl` or `template` to the name (the template identifier is removed before Nuxt.js will build the files) 31 | - Files which are not templates are just copied if needed (small performance improvement) 32 | - Prefix template identifiers with a `$` to replace the identifier with the blueprint instance `id` (instead of removing the template identifier) 33 | 34 | ### Main methods 35 | 36 | - `autodiscover(, { validate(), filter() })` 37 | Filter and validate callbacks are more or less the same, `validate` runs during walking the fs and receives the full path as string argument . `Filter` runs when the fs walking has finished and passes the result of `path.parse` as argument. 38 | 39 | It returns the found files by _type_, where _type_ is the name of the first level folder 40 | 41 | - `resolveFiles(, ) ` 42 | 43 | This method simply checks if a `add` method exists. Eg if you have a first level folder `plugins` then it will call the `addPlugins` method with the list of files. If the corresponding method doesnt exists then its assumed it are _just_ generic files (or templates) which need to be copied. 44 | The `pathPrefix` argument is the folder in `buildDir` into which the files will be copied (i.e. if you set this to 'my-id' then all the blueprint files/templates are copied into `.nuxt/my-id`) 45 | 46 | It returns a mapping of the `src` to `dst` for all files 47 | 48 | ## Development 49 | 50 | 1. Clone this repository 51 | 2. Install dependencies using `yarn install` or `npm install` 52 | 3. Start development server using `npm run dev` 53 | 54 | ## License 55 | 56 | [MIT License](./LICENSE) 57 | 58 | Copyright (c) Nuxt.js Team 59 | 60 | 61 | [npm-version-src]: https://img.shields.io/npm/v/@nuxt/blueprints/beta.svg?style=flat-square 62 | [npm-version-href]: https://npmjs.com/package/@nuxt/blueprints 63 | 64 | [npm-downloads-src]: https://img.shields.io/npm/dt/@nuxt/blueprints.svg?style=flat-square 65 | [npm-downloads-href]: https://npmjs.com/package/@nuxt/blueprints 66 | 67 | [circle-ci-src]: https://img.shields.io/circleci/project/github/nuxt/blueprints.svg?style=flat-square 68 | [circle-ci-href]: https://circleci.com/gh/nuxt/blueprints 69 | 70 | [codecov-src]: https://img.shields.io/codecov/c/github/nuxt/blueprints.svg?style=flat-square 71 | [codecov-href]: https://codecov.io/gh/nuxt/blueprints 72 | 73 | [license-src]: https://img.shields.io/npm/l/@nuxt/blueprints.svg?style=flat-square 74 | [license-href]: https://npmjs.com/package/@nuxt/blueprints 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/blueprint/components/hello.tmpl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /example/blueprint/css/style.tmpl.css: -------------------------------------------------------------------------------- 1 | div { 2 | background-color: <%= options.backgroundColor %>; 3 | padding: 1rem; 4 | margin: 1rem auto; 5 | } 6 | -------------------------------------------------------------------------------- /example/blueprint/index.js: -------------------------------------------------------------------------------- 1 | import { Blueprint } from '../../src' 2 | 3 | export default class ExampleBlueprint extends Blueprint { 4 | constructor (nuxt, options) { 5 | options = { 6 | ...options, 7 | dir: __dirname 8 | } 9 | 10 | super(nuxt, options) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/blueprint/plugins/example.tmpl.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from '../components/hello' 3 | 4 | Vue.component('HelloBlueprint<%= options.id %>', Hello) 5 | -------------------------------------------------------------------------------- /example/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import ExampleBlueprint from './blueprint' 2 | 3 | export default { 4 | modules: [ 5 | function exampleBlueprint () { 6 | const options1 = { 7 | id: '1', 8 | backgroundColor: 'gold' 9 | } 10 | 11 | const options2 = { 12 | id: '2', 13 | backgroundColor: 'silver' 14 | } 15 | 16 | const options3 = { 17 | id: '3', 18 | backgroundColor: '#cd7f32' 19 | } 20 | 21 | new ExampleBlueprint(this.nuxt, options1).init() 22 | new ExampleBlueprint(this.nuxt, options2).init() 23 | new ExampleBlueprint(this.nuxt, options3).init() 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /example/pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | 4 | expand: true, 5 | 6 | forceExit: true, 7 | 8 | setupFilesAfterEnv: ['./test/utils/setup'], 9 | 10 | coverageDirectory: './coverage', 11 | 12 | collectCoverageFrom: [ 13 | 'src/**/*.js', 14 | ], 15 | 16 | moduleNameMapper: { 17 | "test-utils(.*)$": "/test/utils$1", 18 | "^src(.*)": "/src$1" 19 | }, 20 | 21 | transform: { 22 | '^.+\\.js$': 'babel-jest', 23 | '^.+\\.vue$': 'vue-jest' 24 | }, 25 | 26 | moduleFileExtensions: [ 27 | 'js', 28 | 'json' 29 | ], 30 | 31 | reporters: [ 32 | 'default', 33 | // ['jest-junit', { outputDirectory: 'reports/junit' }] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: ['@nuxt/press'] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt/blueprints", 3 | "version": "0.1.0-beta.4", 4 | "description": "Module for Nuxt.js to create distributable micro-apps", 5 | "main": "dist/blueprint.js", 6 | "repository": "https://github.com/nuxt/blueprints", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "license": "MIT", 11 | "files": [ 12 | "dist", 13 | "src" 14 | ], 15 | "keywords": [ 16 | "nuxt", 17 | "nuxtjs", 18 | "vue", 19 | "vuejs", 20 | "module" 21 | ], 22 | "scripts": { 23 | "clean": "rimraf ./**/.nuxt packages/**/dist test/**/dist packages/**/node_modules", 24 | "coverage": "codecov", 25 | "build": "bili --file-name blueprint.js", 26 | "example": "nuxt dev example", 27 | "lint": "eslint --fix --ext .js src test example", 28 | "release": "yarn lint && yarn test && yarn build && standard-version", 29 | "test": "yarn test:unit", 30 | "test:unit": "jest test/unit" 31 | }, 32 | "dependencies": { 33 | "consola": "^2.10.1", 34 | "defu": "^0.0.3", 35 | "fs-extra": "^8.1.0", 36 | "inquirer": "^7.0.0", 37 | "klaw": "^3.0.0" 38 | }, 39 | "peerDependencies": { 40 | "nuxt": "^2.10.1" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.6.4", 44 | "@babel/plugin-proposal-class-properties": "^7.5.5", 45 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 46 | "@babel/preset-env": "^7.6.3", 47 | "@nuxtjs/eslint-config": "^1.1.2", 48 | "babel-eslint": "^10.0.3", 49 | "babel-jest": "^24.9.0", 50 | "bili": "^4.8.1", 51 | "codecov": "^3.6.1", 52 | "eslint": "^6.5.1", 53 | "eslint-config-standard": "^14.1.0", 54 | "eslint-plugin-import": "^2.18.2", 55 | "eslint-plugin-jest": "^22.17.0", 56 | "eslint-plugin-node": "^10.0.0", 57 | "eslint-plugin-promise": "^4.2.1", 58 | "eslint-plugin-standard": "^4.0.1", 59 | "eslint-plugin-vue": "^5.2.3", 60 | "jest": "^24.9.0", 61 | "nuxt": "^2.10.1", 62 | "standard-version": "^7.0.0", 63 | "vue-jest": "^3.0.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/blueprint.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import consola from 'consola' 4 | import defu from 'defu' 5 | import serveStatic from 'serve-static' 6 | import { Module } from '@nuxt/core' 7 | import { 8 | ucfirst, 9 | runOnceGuard, 10 | createFileFilter, 11 | walk, 12 | exists, 13 | readFile, 14 | copyFile, 15 | ensureDir 16 | } from './utils' 17 | 18 | const defaultOptions = { 19 | autodiscover: true, 20 | pluginsStrategy: 'unshift' 21 | } 22 | 23 | export default class Blueprint extends Module { 24 | static features = {} 25 | 26 | constructor (nuxt, options = {}) { 27 | // singleton blueprints dont support being loaded twice 28 | if (new.target.features.singleton && !runOnceGuard(new.target, 'constructed')) { 29 | throw new Error(`${new.target.name}: trying to load a singleton blueprint which is already loaded`) 30 | } 31 | 32 | super(nuxt) 33 | 34 | this.id = options.id || this.constructor.id || 'blueprint' 35 | 36 | this.blueprintOptions = defu(options, defaultOptions) 37 | 38 | this.templateOptions = this.blueprintOptions 39 | } 40 | 41 | setup () { 42 | if (!runOnceGuard(Blueprint, 'setup')) { 43 | return 44 | } 45 | 46 | const webpackAliases = this.blueprintOptions.webpackAliases 47 | if (webpackAliases) { 48 | this.extendBuild((config) => { 49 | const aliases = [] 50 | if (webpackAliases === true) { 51 | aliases.push(this.id) 52 | } else if (typeof webpackAliases === 'string') { 53 | aliases.push(webpackAliases) 54 | } else { 55 | aliases.push(...webpackAliases) 56 | } 57 | 58 | for (const alias of aliases) { 59 | if (Array.isArray(alias)) { 60 | const [_alias, _path] = alias 61 | config.resolve.alias[_alias] = _path 62 | } else { 63 | config.resolve.alias[alias] = path.join(this.nuxt.options.buildDir, alias) 64 | } 65 | } 66 | }) 67 | } 68 | } 69 | 70 | async init (files) { 71 | this.setup() 72 | 73 | // static files need to be added immediately 74 | // because otherwise the serveStatic middleware 75 | // is added after the server has already started listening 76 | if (files && files.static) { 77 | await this.resolveFiles({ static: files.static }) 78 | delete files.static 79 | } 80 | 81 | this.nuxt.hook('builder:prepared', async () => { 82 | if (this.blueprintOptions.autodiscover) { 83 | const autodiscoveredFiles = await this.autodiscover() 84 | await this.resolveFiles(autodiscoveredFiles) 85 | } 86 | 87 | if (files) { 88 | await this.resolveFiles(files) 89 | } 90 | }) 91 | } 92 | 93 | createTemplatePaths (filePath, rootDir, prefix) { 94 | if (typeof filePath !== 'string') { 95 | return filePath 96 | } 97 | 98 | let src = filePath 99 | if (!path.isAbsolute(filePath)) { 100 | rootDir = rootDir || this.blueprintOptions.dir 101 | src = path.join(rootDir, filePath) 102 | } 103 | 104 | return { 105 | src, 106 | dst: prefix ? path.join(prefix || '', filePath) : filePath, 107 | dstRelative: filePath 108 | } 109 | } 110 | 111 | static autodiscover (...args) { 112 | return new Blueprint({}).autodiscover(...args) 113 | } 114 | 115 | async autodiscover (rootDir, { validate, filter } = {}) { 116 | rootDir = rootDir || this.blueprintOptions.dir 117 | filter = filter || this.blueprintOptions.filter 118 | validate = validate || this.blueprintOptions.validate 119 | 120 | if (!rootDir || !await exists(rootDir)) { 121 | return {} 122 | } 123 | 124 | filter = createFileFilter(filter) 125 | const files = await walk(rootDir, { validate }) 126 | const filesByType = {} 127 | 128 | for (const file of files) { 129 | if (!file) { 130 | continue 131 | } 132 | 133 | const parsedFile = path.parse(file) 134 | 135 | // TODO: fix sub folders 136 | const { dir, ext } = parsedFile 137 | const [type] = dir.split(path.sep) 138 | 139 | // dont add anything without an extension -> not a proper file 140 | if (!type && !ext) { 141 | continue 142 | } 143 | // filter files 144 | if (filter && !filter(parsedFile)) { 145 | continue 146 | } 147 | 148 | filesByType[type] = filesByType[type] || [] 149 | filesByType[type].push(this.createTemplatePaths(file, rootDir)) 150 | } 151 | 152 | return filesByType 153 | } 154 | 155 | resolveAppPath ({ dstRelative }) { 156 | const nuxtOptions = this.nuxt.options 157 | return path.join(nuxtOptions.srcDir, nuxtOptions.dir.app, this.id, dstRelative) 158 | } 159 | 160 | async resolveAppOverrides (templates) { 161 | // Early return if the main app dir doesnt exists 162 | const appDir = this.resolveAppPath({ dstRelative: '' }) 163 | if (!await exists(appDir)) { 164 | return templates 165 | } 166 | 167 | return Promise.all(templates.map(async (paths) => { 168 | // Use ejected template from nuxt's app dir if it exists 169 | const appPath = this.resolveAppPath(paths) 170 | if (await exists(appPath)) { 171 | paths.src = appPath 172 | } 173 | 174 | return paths 175 | })) 176 | } 177 | 178 | getPathPrefix(pathPrefix) { 179 | return pathPrefix || this.id 180 | } 181 | 182 | async resolveFiles (files, pathPrefix) { 183 | pathPrefix = this.getPathPrefix(pathPrefix) 184 | 185 | // use an instance var to keep track 186 | // of final template src/dst mappings 187 | this.filesMapping = {} 188 | 189 | for (const type in files) { 190 | let typeFiles = files[type].map((file) => { 191 | if (typeof file === 'string') { 192 | return this.createTemplatePaths(file, undefined, pathPrefix) 193 | } 194 | 195 | return { 196 | ...file, 197 | dst: path.join(pathPrefix, file.dst), 198 | dstRelative: file.dst 199 | } 200 | }) 201 | 202 | typeFiles = await this.resolveAppOverrides(typeFiles) 203 | 204 | // Turns 'modules' into 'addModules' 205 | const methodName = `add${ucfirst(type)}` 206 | 207 | // If methodName function exists that means there are 208 | // files with a special meaning for Nuxt.js (ie modules, plugins) 209 | if (this[methodName]) { 210 | await this[methodName](typeFiles) 211 | continue 212 | } 213 | 214 | // The files are just some generic js/vue files, but can be templates 215 | await this.addFiles(typeFiles, type) 216 | } 217 | 218 | // convert absolute paths in fileMapping 219 | // to relative paths from the nuxt.buildDir 220 | // this also creates a copy of filesMapping in the process 221 | // so successive resolveFiles calls dont overwrite the 222 | // same object already returned to the user 223 | const relativeFilesMapping = {} 224 | for (const key in this.filesMapping) { 225 | const filePath = this.filesMapping[key] 226 | 227 | if (path.isAbsolute(filePath)) { 228 | relativeFilesMapping[key] = path.relative(this.nuxt.options.buildDir, filePath) 229 | continue 230 | } 231 | 232 | relativeFilesMapping[key] = filePath 233 | } 234 | 235 | return relativeFilesMapping 236 | } 237 | 238 | async copyFile ({ src, dst }) { 239 | if (!src) { 240 | return 241 | } 242 | 243 | if (!path.isAbsolute(dst)) { 244 | dst = path.join(this.nuxt.options.buildDir, dst) 245 | } 246 | 247 | try { 248 | consola.debug(`${this.constructor.name}: Copying '${path.relative(this.nuxt.options.srcDir, src)}' to '${path.relative(this.nuxt.options.buildDir, dst)}'`) 249 | 250 | await ensureDir(path.dirname(dst)) 251 | await copyFile(src, dst, fs.constants.COPYFILE_FICLONE) 252 | return dst 253 | } catch (err) { 254 | consola.error(`${this.constructor.name}: An error occured while copying '${path.relative(this.nuxt.options.srcDir, src)}' to '${path.relative(this.nuxt.options.buildDir, dst)}'\n`, err) 255 | return false 256 | } 257 | } 258 | 259 | addTemplateIfNeeded ({ src, dst, dstRelative } = {}) { 260 | if (!src) { 261 | return 262 | } 263 | 264 | const templateSuffices = ['tmpl', '$tmpl', 'template', '$template'] 265 | 266 | let templatePath 267 | for (const suffix of templateSuffices) { 268 | if (src.includes(`.${suffix}.`)) { 269 | // if user provided a custom dst, use that 270 | if (!src.endsWith(dstRelative)) { 271 | templatePath = dst 272 | break 273 | } 274 | 275 | const { name, ext } = path.parse(src) 276 | 277 | // if template suffix starts with $ 278 | // create a unique but predictable name by replacing 279 | // the template indicator by this.id 280 | // TODO: normalize id? 281 | const id = suffix[0] === '$' ? `.${this.id}` : '' 282 | templatePath = path.join(path.dirname(dst), `${path.basename(name, `.${suffix}`)}${id}${ext}`) 283 | break 284 | } 285 | } 286 | 287 | // its a template 288 | if (templatePath) { 289 | const { dst: templateDst } = this.addTemplate({ 290 | src, 291 | fileName: templatePath, 292 | options: this.templateOptions 293 | }) 294 | 295 | this.filesMapping[dstRelative] = templateDst 296 | 297 | return templateDst 298 | } 299 | 300 | this.filesMapping[dstRelative] = src 301 | return src 302 | } 303 | 304 | async addTemplateOrCopy ({ src, dst, dstRelative } = {}) { 305 | const dest = this.addTemplateIfNeeded({ src, dst, dstRelative }) 306 | 307 | if (dest === src) { 308 | await this.copyFile({ src, dst }) 309 | return dst 310 | } 311 | 312 | return dest 313 | } 314 | 315 | addFiles (files, type) { 316 | return Promise.all(files.map(file => this.addTemplateOrCopy(file))) 317 | } 318 | 319 | addAssets (assets) { 320 | // TODO: run addAssets more than once while adding just one plugin 321 | // or set unique webpack plugin name 322 | const emitAssets = (compilation) => { 323 | // Note: the order in which assets are emitted is not stable 324 | /* istanbul ignore next */ 325 | return Promise.all(assets.map(async ({ src, dst }) => { 326 | const assetBuffer = await readFile(src) 327 | 328 | compilation.assets[dst] = { 329 | source: () => assetBuffer, 330 | size: () => assetBuffer.length 331 | } 332 | })) 333 | } 334 | 335 | // add webpack plugin 336 | this.nuxt.options.build.plugins.push({ 337 | apply (compiler) { 338 | /* istanbul ignore next */ 339 | compiler.hooks.emit.tapPromise(`${this.id}BlueprintPlugin`, emitAssets) 340 | } 341 | }) 342 | } 343 | 344 | async addLayouts (layouts) { 345 | for (const layout of layouts) { 346 | const layoutPath = await this.addTemplateOrCopy(layout) 347 | 348 | const { name: layoutName } = path.parse(layoutPath) 349 | const existingLayout = this.nuxt.options.layouts[layoutName] 350 | 351 | if (existingLayout) { 352 | consola.warn(`Duplicate layout registration, "${layoutName}" has been registered as "${existingLayout}"`) 353 | continue 354 | } 355 | 356 | // Add to nuxt layouts 357 | this.nuxt.options.layouts[layoutName] = `./${layoutPath}` 358 | } 359 | } 360 | 361 | async addModules (modules) { 362 | for (const module of modules) { 363 | const modulePath = this.addTemplateIfNeeded(module) 364 | await this.addModule(modulePath) 365 | } 366 | } 367 | 368 | async addPlugins (plugins) { 369 | const newPlugins = [] 370 | // dont use addPlugin here due to its addTemplate use 371 | for (const plugin of plugins) { 372 | const pluginPath = await this.addTemplateOrCopy(plugin) 373 | 374 | // Add to nuxt plugins 375 | newPlugins.push({ 376 | src: path.join(this.nuxt.options.buildDir, pluginPath), 377 | // TODO: remove deprecated option in Nuxt 3 378 | ssr: plugin.ssr, 379 | mode: plugin.mode 380 | }) 381 | } 382 | 383 | // nuxt default behaviour is to put new plugins 384 | // at the front of the array, so thats what we 385 | // want do as well. But we want to maintain 386 | // order of the files 387 | // TODO: check if walk is stable in the order of resolving files 388 | const pluginsStrategy = this.blueprintOptions.pluginsStrategy 389 | if (typeof pluginsStrategy === 'function') { 390 | pluginsStrategy(this.nuxt.options.plugins, newPlugins) 391 | return 392 | } 393 | 394 | if (!this.nuxt.options.plugins[pluginsStrategy]) { 395 | throw new Error(`Unsupported plugin strategy ${pluginsStrategy}`) 396 | } 397 | 398 | this.nuxt.options.plugins[pluginsStrategy](...newPlugins) 399 | } 400 | 401 | async addStatic (staticFiles) { 402 | /* istanbul ignore next */ 403 | const files = await Promise.all(staticFiles.map((file) => { 404 | return this.addTemplateOrCopy(file) 405 | })) 406 | 407 | const staticMiddleware = serveStatic( 408 | path.resolve(this.nuxt.options.buildDir, path.dirname(files[0])), 409 | this.nuxt.options.render.static 410 | ) 411 | staticMiddleware.prefix = this.nuxt.options.render.static.prefix 412 | 413 | this.addServerMiddleware(staticMiddleware) 414 | } 415 | 416 | addStyles (stylesheets) { 417 | for (let stylesheet of stylesheets) { 418 | if (typeof stylesheet === 'object') { 419 | stylesheet = this.addTemplateIfNeeded(stylesheet) 420 | } 421 | 422 | if (stylesheet && !this.nuxt.options.css.includes(stylesheet)) { 423 | this.nuxt.options.css.push(stylesheet) 424 | } 425 | } 426 | } 427 | 428 | addApp (appFiles) { 429 | return Promise.all(appFiles.map(({ src, dst }) => { 430 | return this.addTemplate({ 431 | src, 432 | // dst has blueprint id and app dir name added, remove those 433 | // eg dst: blueprint/app/router.js -> router.js 434 | fileName: dst.substr(dst.indexOf('app') + 4) 435 | }) 436 | })) 437 | } 438 | 439 | addStore () { 440 | consola.warn(`${this.constructor.name}: adding store modules from blueprints is not (yet) implemented`) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/cli/commands/eject.js: -------------------------------------------------------------------------------- 1 | import { join, dirname, relative } from 'path' 2 | import consola from 'consola' 3 | import { ensureDir, appendFile, readFile, writeFile } from 'fs-extra' 4 | 5 | export async function ejectTemplates (nuxt, options, templates) { 6 | const { name, appDir } = options 7 | const resolvedAppDir = join(nuxt.options.srcDir, nuxt.options.dir.app, appDir || name) 8 | await ensureDir(resolvedAppDir) 9 | 10 | await Promise.all(templates.map(template => ejectTemplate(nuxt, options, template, resolvedAppDir))) 11 | } 12 | 13 | export async function ejectTemplate (nuxt, { name, appDir }, { src, dst }, resolvedAppDir) { 14 | if (!resolvedAppDir) { 15 | resolvedAppDir = join(nuxt.options.srcDir, nuxt.options.dir.app, appDir || name) 16 | } 17 | 18 | const dstFile = join(resolvedAppDir, dst) 19 | consola.debug(`Ejecting template '${src}' to '${dstFile}'`) 20 | 21 | const content = await readFile(src) 22 | if (!content) { 23 | consola.warn(`Reading source template file returned empty content, eject aborted for: ${relative(nuxt.options.srcDir, src)}`) 24 | return 25 | } 26 | 27 | await ensureDir(dirname(dstFile)) 28 | 29 | await writeFile(dstFile, content) 30 | consola.info(`Ejected ${relative(nuxt.options.srcDir, dstFile)}`) 31 | } 32 | 33 | export async function ejectTheme (nuxt, options, discoveryPath) { 34 | // TODO: prevent appending the same theme.css more than once 35 | const content = await readFile(join(discoveryPath, 'theme.css')) 36 | if (!content) { 37 | consola.warn(`Reading from theme.css returned empty content, eject aborted`) 38 | return 39 | } 40 | 41 | const dstFile = join(nuxt.options.rootDir, 'nuxt.press.css') 42 | await appendFile(dstFile, content) 43 | 44 | consola.info(`Ejected to ./nuxt.press.css`) 45 | } 46 | -------------------------------------------------------------------------------- /src/cli/commands/index.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import inquirer from 'inquirer' 3 | import Blueprint from '../../blueprint' 4 | import { exists } from '../../utils' 5 | import { ejectTheme, ejectTemplates } from './eject' 6 | 7 | export default class Commands { 8 | static async eject (args, nuxt, options) { 9 | const { 10 | dir, 11 | autodiscover: autodiscoverOptions, 12 | blueprints, 13 | normalizeInput = str => str.includes('/') || str.endsWith('s') ? str : `${str}s` 14 | } = options 15 | let templates = options.templates 16 | 17 | let discoveryPath = dir 18 | let typeKey = args[0] || '' 19 | let blueprint 20 | 21 | if (!typeKey) { 22 | consola.fatal(`A template key identifying the template you wish to eject is required`) 23 | return 24 | } 25 | 26 | if (blueprints) { 27 | [blueprint, typeKey] = args[0].split('/') 28 | 29 | discoveryPath = blueprints[blueprint] 30 | 31 | if (!discoveryPath) { 32 | consola.fatal(`Unrecognized blueprint '${blueprint}'`) 33 | return 34 | } 35 | } 36 | 37 | if (!discoveryPath || !await exists(discoveryPath)) { 38 | consola.fatal(`Blueprint path '${discoveryPath}' is empty or does not exists`) 39 | return 40 | } 41 | 42 | if (!templates) { 43 | templates = await Blueprint.autodiscover(discoveryPath, autodiscoverOptions) 44 | 45 | if (!templates) { 46 | consola.fatal(`Unrecognized blueprint path, autodiscovery failed for '${discoveryPath}'`) 47 | return 48 | } 49 | } 50 | 51 | // normalize key 52 | if (typeof normalizeInput === 'function') { 53 | typeKey = normalizeInput(typeKey) 54 | } 55 | 56 | if (typeKey === 'theme' || typeKey === 'themes') { 57 | await ejectTheme(blueprint, discoveryPath) 58 | return 59 | } 60 | 61 | const templatesToEject = [] 62 | 63 | if (templates[typeKey]) { 64 | templatesToEject.push(...[].concat(templates[typeKey])) 65 | } 66 | 67 | if (!templatesToEject.length) { 68 | for (const type in templates) { 69 | const templateToEject = templates[type].find(t => t.dst === typeKey) 70 | if (templateToEject) { 71 | templatesToEject.push(templateToEject) 72 | break 73 | } 74 | } 75 | } 76 | 77 | if (!templatesToEject.length) { 78 | // show a prompt so user can select for a list 79 | const choices = [] 80 | for (const type in templates) { 81 | const templateChoices = templates[type].map((t, i) => ({ 82 | name: t.dst, 83 | value: [type, i] 84 | })) 85 | 86 | choices.push(...templateChoices) 87 | } 88 | 89 | const answers = await inquirer.prompt([{ 90 | type: 'checkbox', 91 | name: 'templates', 92 | message: 'Unrecognized template key, please select the files you wish to eject:\n', 93 | choices, 94 | pageSize: 15 95 | }]) 96 | 97 | if (!answers.templates.length) { 98 | consola.fatal(`Unrecognized template key '${typeKey}'`) 99 | return 100 | } 101 | 102 | for (const [type, index] of answers.templates) { 103 | templatesToEject.push(templates[type][index]) 104 | } 105 | } 106 | 107 | await ejectTemplates(nuxt, options, templatesToEject) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { NuxtCommand, options } from '@nuxt/cli' 3 | import Commands from './commands' 4 | 5 | const { common } = options 6 | 7 | export default async function runCommand (options = {}) { 8 | const { 9 | name = 'blueprint', 10 | description 11 | } = options 12 | 13 | await NuxtCommand.run({ 14 | name, 15 | description: description || `CLI for ${name}`, 16 | usage: `${name} `, 17 | options: { 18 | ...common 19 | }, 20 | async run (cmd) { 21 | // remove argv's so nuxt doesnt pick them up as rootDir 22 | const [command = '', ...args] = cmd.argv._.splice(0, cmd.argv._.length) 23 | 24 | if (!command || !Commands[command]) { 25 | consola.fatal(`Unrecognized command '${command}'`) 26 | return 27 | } 28 | 29 | const config = await cmd.getNuxtConfig() 30 | const nuxt = await cmd.getNuxt(config) 31 | 32 | return Commands[command](args, nuxt, options) 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Blueprint } from './blueprint' 2 | export { default as run } from './cli' 3 | export * from './utils' 4 | -------------------------------------------------------------------------------- /src/utils/fs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import klaw from 'klaw' 3 | export { access, readFile, copyFile, ensureDir } from 'fs-extra' 4 | 5 | export function exists (p) { 6 | return new Promise((resolve, reject) => { 7 | fs.access(p, fs.constants.F_OK, (err) => { 8 | if (err) { 9 | resolve(false) 10 | return 11 | } 12 | 13 | resolve(true) 14 | }) 15 | }) 16 | } 17 | 18 | export function createFileFilter (filter) { 19 | if (!filter) { 20 | return 21 | } 22 | 23 | if (filter instanceof RegExp) { 24 | return path => filter.test(path) 25 | } 26 | 27 | if (typeof filter === 'string') { 28 | return path => path.includes(filter) 29 | } 30 | 31 | return filter 32 | } 33 | 34 | export function walk (dir, { validate, sliceRoot = true } = {}) { 35 | const matches = [] 36 | 37 | let sliceAt 38 | if (sliceRoot) { 39 | if (sliceRoot === true) { 40 | sliceRoot = dir 41 | } 42 | 43 | sliceAt = sliceRoot.length + (sliceRoot.endsWith('/') ? 0 : 1) 44 | } 45 | 46 | validate = createFileFilter(validate) 47 | 48 | return new Promise((resolve) => { 49 | klaw(dir) 50 | .on('data', (match) => { 51 | const path = sliceAt ? match.path.slice(sliceAt) : match.path 52 | 53 | if (!path.includes('node_modules') && (!validate || validate(path))) { 54 | matches.push(path) 55 | } 56 | }) 57 | .on('end', () => resolve(matches)) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/guards.js: -------------------------------------------------------------------------------- 1 | export function abstractGuard (target, className) { 2 | if (target === className) { 3 | throw new Error(`${className} is an abstract class, do not instantiate it directly`) 4 | } 5 | } 6 | 7 | export function runOnceGuard (instance, name) { 8 | if (!instance._runGuards) { 9 | instance._runGuards = {} 10 | } 11 | 12 | if (instance._runGuards[name]) { 13 | return false 14 | } 15 | 16 | instance._runGuards[name] = true 17 | return true 18 | } 19 | 20 | export function runOnceGuardBlocking (instance, name) { 21 | if (!instance._runGuards) { 22 | instance._runGuards = {} 23 | } 24 | 25 | if (instance._runGuards[name] === true) { 26 | return Promise.resolve(false) 27 | } 28 | 29 | if (instance._runGuards[name]) { 30 | return new Promise((resolve) => { 31 | instance._runGuards[name].push(() => resolve(false)) 32 | }) 33 | } 34 | 35 | instance._runGuards[name] = [] 36 | 37 | return Promise.resolve(() => { 38 | instance._runGuards[name].forEach(r => r()) 39 | instance._runGuards[name] = true 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './fs' 2 | export * from './guards' 3 | export * from './string' 4 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | export function ucfirst (str) { 2 | str = str || '' 3 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/blueprint.test.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import fsExtra from 'fs-extra' 3 | import Blueprint from 'src/blueprint' 4 | 5 | import * as utils from 'src/utils' 6 | import { resetUtilMocks as _resetUtilMocks } from 'test-utils' 7 | jest.mock('src/utils') 8 | const resetUtilMocks = utilNames => _resetUtilMocks(utils, utilNames) 9 | 10 | jest.mock('fs-extra') 11 | jest.mock('serve-static', () => () => jest.fn()) 12 | 13 | describe('blueprint', () => { 14 | beforeAll(() => resetUtilMocks()) 15 | 16 | afterEach(() => jest.clearAllMocks()) 17 | 18 | test('basic', () => { 19 | const nuxt = { 20 | options: { 21 | buildDir: '.nuxt' 22 | }, 23 | hook: jest.fn() 24 | } 25 | 26 | const blueprint = new Blueprint(nuxt, { 27 | webpackAliases: [ 28 | 'test-alias', 29 | ['test-alias2', 'my-custom-path'] 30 | ] 31 | }) 32 | 33 | let buildFn 34 | blueprint.extendBuild = jest.fn(_buildFn => (buildFn = _buildFn)) 35 | 36 | blueprint.setup() 37 | expect(blueprint.extendBuild).toHaveBeenCalledTimes(1) 38 | 39 | const config = { resolve: { alias: {} } } 40 | buildFn(config) 41 | expect(config.resolve.alias).toEqual({ 42 | 'test-alias': '.nuxt/test-alias', 43 | 'test-alias2': 'my-custom-path' 44 | }) 45 | 46 | blueprint.init() 47 | expect(blueprint.extendBuild).toHaveBeenCalledTimes(1) 48 | expect(nuxt.hook).toHaveBeenCalledTimes(1) 49 | expect(nuxt.hook).toHaveBeenCalledWith('builder:prepared', expect.any(Function)) 50 | }) 51 | 52 | test('construct singleton', () => { 53 | class SingletonBlueprint extends Blueprint { 54 | static features = { singleton: true } 55 | } 56 | 57 | expect(() => new SingletonBlueprint({})).not.toThrow() 58 | expect(() => new SingletonBlueprint({})).toThrowError('singleton blueprint') 59 | }) 60 | 61 | test('autodiscover returns empty object when rootDir doesnt exist', async () => { 62 | utils.exists.mockReturnValue(false) 63 | utils.walk.mockReturnValue([ 64 | '/path/type/a-file.js' 65 | ]) 66 | 67 | const files = await Blueprint.autodiscover('/my-test-dir') 68 | expect(files).toEqual({}) 69 | 70 | resetUtilMocks(['exists', 'walk']) 71 | }) 72 | 73 | test('autodiscover returns discovered files by type', async () => { 74 | utils.exists.mockReturnValue(true) 75 | utils.walk.mockReturnValue([ 76 | '', 77 | 'type/a-type-file.js', 78 | 'type/ignored-file.js', 79 | 'an-extensionless-file' 80 | ]) 81 | 82 | const filter = ({ name }) => !name.includes('ignored') 83 | const files = await Blueprint.autodiscover('/my-test-dir', { filter }) 84 | expect(files).toEqual({ 85 | type: [{ 86 | src: '/my-test-dir/type/a-type-file.js', 87 | dst: 'type/a-type-file.js', 88 | dstRelative: 'type/a-type-file.js' 89 | }] 90 | }) 91 | 92 | resetUtilMocks(['exists', 'walk']) 93 | }) 94 | 95 | test('resolveFiles calls appropiate methods and returns mapping', async () => { 96 | const nuxt = { 97 | hook: jest.fn(), 98 | options: { 99 | srcDir: '/var/nuxt/src', 100 | buildDir: '/var/nuxt/.nuxt', 101 | dir: { 102 | app: 'app' 103 | }, 104 | build: { 105 | plugins: [] 106 | }, 107 | layouts: {}, 108 | plugins: [], 109 | css: [], 110 | render: { 111 | static: {} 112 | } 113 | } 114 | } 115 | const options = { 116 | dir: '/var/nuxt/my-blueprint-dir' 117 | } 118 | 119 | const blueprint = new Blueprint(nuxt, options) 120 | blueprint.addModule = jest.fn() 121 | blueprint.addTemplate = jest.fn(({ src, fileName }) => ({ dst: fileName })) 122 | blueprint.addServerMiddleware = jest.fn() 123 | 124 | const files = { 125 | assets: [ 126 | 'assets/my-asset.zip' 127 | ], 128 | layouts: [ 129 | 'layouts/docs.tmpl.vue', 130 | { 131 | src: 'layouts/docs.tmpl.vue', 132 | dst: 'layouts/docs.tmpl.vue', 133 | dstRelative: 'layouts/docs.tmpl.vue' 134 | } 135 | ], 136 | modules: [ 137 | 'modules/my-module.js' 138 | ], 139 | plugins: [ 140 | { 141 | src: 'plugins/my-plugin.$tmpl.js', 142 | dst: 'plugins/my-plugin.$tmpl.js' 143 | } 144 | ], 145 | static: [ 146 | 'static/my-static.txt' 147 | ], 148 | styles: [ 149 | 'styles/my-test.css' 150 | ], 151 | app: [ 152 | { 153 | src: 'app/empty.js', 154 | dst: 'app/empty.js' 155 | } 156 | ], 157 | custom: [ 158 | 'custom-file.log', 159 | { 160 | src: 'custom-other-file.tmpl.js', 161 | dst: 'custom-build-path/file.js', 162 | dstRelative: 'custom-build-path/file.js' 163 | }, 164 | { 165 | dst: 'should-be-skipped', 166 | dstRelative: 'should-be-skipped' 167 | } 168 | ] 169 | } 170 | 171 | const mapping = await blueprint.resolveFiles(files) 172 | 173 | // for non-template paths below the paths are listed, 174 | /// relatively from the buildDir 175 | expect(mapping).toEqual({ 176 | 'custom-file.log': '../my-blueprint-dir/custom-file.log', 177 | 'custom-build-path/file.js': 'blueprint/custom-build-path/file.js', 178 | 'layouts/docs.tmpl.vue': 'blueprint/layouts/docs.vue', 179 | 'modules/my-module.js': '../my-blueprint-dir/modules/my-module.js', 180 | 'plugins/my-plugin.$tmpl.js': 'blueprint/plugins/my-plugin.blueprint.js', 181 | 'static/my-static.txt': '../my-blueprint-dir/static/my-static.txt', 182 | 'styles/my-test.css': '../my-blueprint-dir/styles/my-test.css' 183 | }) 184 | 185 | expect(nuxt.options.layouts).toEqual({ docs: './blueprint/layouts/docs.vue' }) 186 | expect(nuxt.options.plugins).toEqual([{ src: '/var/nuxt/.nuxt/blueprint/plugins/my-plugin.blueprint.js' }]) 187 | expect(nuxt.options.css).toEqual(['/var/nuxt/my-blueprint-dir/styles/my-test.css']) 188 | expect(nuxt.options.build.plugins).toEqual([{ apply: expect.any(Function) }]) 189 | expect(blueprint.addServerMiddleware).toHaveBeenCalledTimes(1) 190 | expect(blueprint.addServerMiddleware).toHaveBeenCalledWith(expect.any(Function)) 191 | expect(blueprint.addModule).toHaveBeenCalledTimes(1) 192 | expect(blueprint.addModule).toHaveBeenCalledWith('/var/nuxt/my-blueprint-dir/modules/my-module.js') 193 | expect(blueprint.addTemplate).toHaveBeenCalledTimes(5) 194 | expect(blueprint.addTemplate).toHaveBeenCalledWith(expect.objectContaining({ fileName: 'empty.js' })) 195 | 196 | expect(consola.warn).toHaveBeenCalledTimes(1) 197 | expect(consola.warn).toHaveBeenCalledWith(expect.stringContaining('Duplicate layout registration')) 198 | }) 199 | 200 | test('createTemplatePaths returns when filePath is already object', () => { 201 | const blueprint = new Blueprint({}, { id: 'test' }) 202 | 203 | const filePath = { src: 'test.js' } 204 | 205 | expect(blueprint.createTemplatePaths(filePath)).toBe(filePath) 206 | }) 207 | 208 | test('resolveAppPath', () => { 209 | const nuxt = { 210 | options: { 211 | srcDir: '/test-src', 212 | dir: { 213 | app: 'app' 214 | } 215 | } 216 | } 217 | 218 | const blueprint = new Blueprint(nuxt, { id: 'test' }) 219 | expect(blueprint.resolveAppPath({ dstRelative: 'rel' })).toEqual('/test-src/app/test/rel') 220 | }) 221 | 222 | test('resolveAppOverrides exists', async () => { 223 | utils.exists.mockReturnValue(true) 224 | 225 | const nuxt = { 226 | options: { 227 | srcDir: '/test-src', 228 | dir: { 229 | app: 'app' 230 | } 231 | } 232 | } 233 | const templates = [ 234 | { 235 | src: '/my-src/test.js', 236 | dstRelative: 'test.js' 237 | } 238 | ] 239 | 240 | const blueprint = new Blueprint(nuxt, { id: 'test' }) 241 | await expect(blueprint.resolveAppOverrides(templates)).resolves.toEqual([{ 242 | src: '/test-src/app/test/test.js', 243 | dstRelative: 'test.js' 244 | }]) 245 | }) 246 | 247 | test('resolveAppOverrides does not exists', async () => { 248 | let callCount = 0 249 | utils.exists.mockImplementation(() => { 250 | callCount++ 251 | return callCount > 2 252 | }) 253 | 254 | const nuxt = { 255 | options: { 256 | srcDir: '/test-src', 257 | dir: { 258 | app: 'app' 259 | } 260 | } 261 | } 262 | const templates = [ 263 | { 264 | src: '/my-src/test.js', 265 | dstRelative: 'test.js' 266 | } 267 | ] 268 | 269 | const blueprint = new Blueprint(nuxt, { id: 'test' }) 270 | await expect(blueprint.resolveAppOverrides(templates)).resolves.toEqual([{ 271 | src: '/my-src/test.js', 272 | dstRelative: 'test.js' 273 | }]) 274 | }) 275 | 276 | test('copyFile logs on error', () => { 277 | const blueprint = new Blueprint({ 278 | options: { srcDir: '/', buildDir: '/.nuxt' } 279 | }) 280 | 281 | jest.spyOn(fsExtra, 'ensureDir').mockImplementation(() => { 282 | throw new Error('copy error') 283 | }) 284 | 285 | expect(() => blueprint.copyFile({ 286 | src: '/the-source', 287 | dst: '/.nuxt/the-destination' 288 | })).not.toThrow() 289 | 290 | expect(consola.error).toHaveBeenCalledTimes(1) 291 | expect(consola.error).toHaveBeenCalledWith(`Blueprint: An error occured while copying 'the-source' to 'the-destination'\n`, expect.any(Error)) 292 | }) 293 | 294 | test('addPlugins calls pluginsStrategy function', async () => { 295 | const nuxt = { 296 | options: { 297 | buildDir: '.nuxt', 298 | plugins: [] 299 | } 300 | } 301 | 302 | const options = { 303 | pluginsStrategy: jest.fn(_ => _) 304 | } 305 | 306 | const blueprint = new Blueprint(nuxt, options) 307 | blueprint.addTemplateOrCopy = jest.fn(_ => _) 308 | 309 | const plugins = ['plugins/my-plugin.js'] 310 | await blueprint.addPlugins(plugins) 311 | 312 | expect(options.pluginsStrategy).toHaveBeenCalledTimes(1) 313 | expect(options.pluginsStrategy).toHaveBeenCalledWith([], [{ 314 | src: '.nuxt/plugins/my-plugin.js', 315 | ssr: undefined, 316 | mode: undefined 317 | }]) 318 | }) 319 | 320 | test('addPlugins throws error on unsupported pluginsStrategy', async () => { 321 | const nuxt = { 322 | options: { 323 | buildDir: '.nuxt', 324 | plugins: [] 325 | } 326 | } 327 | 328 | const options = { 329 | pluginsStrategy: 'does-not-exist' 330 | } 331 | 332 | const blueprint = new Blueprint(nuxt, options) 333 | blueprint.addTemplateOrCopy = jest.fn(_ => _) 334 | 335 | const plugins = ['plugins/my-plugin.js'] 336 | 337 | await expect(blueprint.addPlugins(plugins)).rejects.toThrowError('Unsupported') 338 | }) 339 | 340 | test('store module warns not implemented', () => { 341 | const blueprint = new Blueprint({}) 342 | blueprint.addStore() 343 | 344 | expect(consola.warn).toHaveBeenCalledTimes(1) 345 | expect(consola.warn).toHaveBeenCalledWith(expect.stringContaining('not (yet) implemented')) 346 | }) 347 | }) 348 | -------------------------------------------------------------------------------- /test/unit/cli.eject-helpers.test.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import fsExtra from 'fs-extra' 3 | import { ejectTheme, ejectTemplates, ejectTemplate } from 'src/cli/commands/eject' 4 | 5 | import * as utils from 'src/utils' 6 | import { resetUtilMocks as _resetUtilMocks } from 'test-utils' 7 | jest.mock('src/utils') 8 | const resetUtilMocks = utilNames => _resetUtilMocks(utils, utilNames) 9 | 10 | jest.mock('fs-extra') 11 | 12 | describe('eject helpers', () => { 13 | beforeAll(() => resetUtilMocks()) 14 | 15 | afterEach(() => jest.resetAllMocks()) 16 | 17 | test('ejectTheme logs warning when source is empty', async () => { 18 | fsExtra.readFile.mockReturnValue(false) 19 | 20 | const nuxt = {} 21 | const options = {} 22 | const discoveryPath = '/var/nuxt' 23 | 24 | await ejectTheme(nuxt, options, discoveryPath) 25 | 26 | expect(fsExtra.readFile).toHaveBeenCalledTimes(1) 27 | expect(fsExtra.readFile).toHaveBeenCalledWith('/var/nuxt/theme.css') 28 | 29 | expect(consola.warn).toHaveBeenCalledTimes(1) 30 | expect(consola.warn).toHaveBeenCalledWith('Reading from theme.css returned empty content, eject aborted') 31 | }) 32 | 33 | test('ejectTheme logs info when theme ejected', async () => { 34 | fsExtra.readFile.mockReturnValue(true) 35 | 36 | const nuxt = { options: { rootDir: '/var/nuxt' } } 37 | const options = {} 38 | const discoveryPath = '/var/nuxt' 39 | 40 | await ejectTheme(nuxt, options, discoveryPath) 41 | 42 | expect(fsExtra.appendFile).toHaveBeenCalledTimes(1) 43 | expect(fsExtra.appendFile).toHaveBeenCalledWith('/var/nuxt/nuxt.press.css', true) 44 | 45 | expect(consola.info).toHaveBeenCalledTimes(1) 46 | expect(consola.info).toHaveBeenCalledWith('Ejected to ./nuxt.press.css') 47 | }) 48 | 49 | test('ejectTemplates creates main template dir', async () => { 50 | fsExtra.readFile.mockReturnValue(true) 51 | 52 | const nuxt = { 53 | options: { 54 | rootDir: '/var/nuxt', 55 | srcDir: '/var/nuxt', 56 | dir: { 57 | app: 'app' 58 | } 59 | } 60 | } 61 | const options = { appDir: 'test-dir' } 62 | const templates = [] 63 | 64 | await ejectTemplates(nuxt, options, templates) 65 | 66 | expect(fsExtra.ensureDir).toHaveBeenCalledTimes(1) 67 | expect(fsExtra.ensureDir).toHaveBeenCalledWith('/var/nuxt/app/test-dir') 68 | }) 69 | 70 | test('ejectTemplate logs warning when source is empty', async () => { 71 | fsExtra.readFile.mockReturnValue(false) 72 | 73 | const nuxt = { 74 | options: { 75 | rootDir: '/var/nuxt', 76 | srcDir: '/var/nuxt', 77 | dir: { 78 | app: 'app' 79 | } 80 | } 81 | } 82 | const options = { appDir: 'test-dir' } 83 | const template = { src: '/var/the/source', dst: 'the/destination' } 84 | const resolvedAppDir = undefined 85 | 86 | await ejectTemplate(nuxt, options, template, resolvedAppDir) 87 | 88 | expect(fsExtra.readFile).toHaveBeenCalledTimes(1) 89 | expect(fsExtra.readFile).toHaveBeenCalledWith('/var/the/source') 90 | 91 | expect(consola.warn).toHaveBeenCalledTimes(1) 92 | expect(consola.warn).toHaveBeenCalledWith('Reading source template file returned empty content, eject aborted for: ../the/source') 93 | }) 94 | 95 | test('ejectTemplate logs info when template ejected', async () => { 96 | fsExtra.readFile.mockReturnValue(true) 97 | 98 | const nuxt = { 99 | options: { 100 | rootDir: '/var/nuxt', 101 | srcDir: '/var/nuxt', 102 | dir: { 103 | app: 'app' 104 | } 105 | } 106 | } 107 | const options = { appDir: 'test-dir' } 108 | const template = { src: 'the/source', dst: 'the/destination' } 109 | const resolvedAppDir = undefined 110 | 111 | await ejectTemplate(nuxt, options, template, resolvedAppDir) 112 | 113 | expect(consola.debug).toHaveBeenCalledTimes(1) 114 | expect(consola.debug).toHaveBeenCalledWith(`Ejecting template 'the/source' to '/var/nuxt/app/test-dir/the/destination'`) 115 | 116 | expect(fsExtra.readFile).toHaveBeenCalledTimes(1) 117 | expect(fsExtra.readFile).toHaveBeenCalledWith('the/source') 118 | 119 | expect(fsExtra.ensureDir).toHaveBeenCalledTimes(1) 120 | expect(fsExtra.ensureDir).toHaveBeenCalledWith('/var/nuxt/app/test-dir/the') 121 | 122 | expect(consola.info).toHaveBeenCalledTimes(1) 123 | expect(consola.info).toHaveBeenCalledWith('Ejected app/test-dir/the/destination') 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /test/unit/cli.eject.test.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import inquirer from 'inquirer' 3 | import Blueprint from 'src/blueprint' 4 | import * as ejectHelpers from 'src/cli/commands/eject' 5 | 6 | import * as utils from 'src/utils' 7 | import { resetUtilMocks as _resetUtilMocks } from 'test-utils' 8 | jest.mock('src/utils') 9 | const resetUtilMocks = utilNames => _resetUtilMocks(utils, utilNames) 10 | 11 | jest.mock('inquirer') 12 | jest.mock('src/blueprint') 13 | jest.mock('src/cli/commands') 14 | jest.mock('src/cli/commands/eject') 15 | 16 | describe('Commands.eject', () => { 17 | beforeAll(() => resetUtilMocks()) 18 | 19 | afterEach(() => jest.resetAllMocks()) 20 | 21 | test('Commands.eject logs fatal error without key', async () => { 22 | utils.exists.mockReturnValue(true) 23 | const { default: Commands } = jest.requireActual('src/cli/commands') 24 | 25 | const args = [] 26 | const nuxt = {} 27 | const options = {} 28 | 29 | await Commands.eject(args, nuxt, options) 30 | 31 | expect(consola.fatal).toHaveBeenCalledTimes(1) 32 | expect(consola.fatal).toHaveBeenCalledWith('A template key identifying the template you wish to eject is required') 33 | 34 | resetUtilMocks(['exists']) 35 | }) 36 | 37 | test('Commands.eject logs fatal error with empty dir and no blueprints', async () => { 38 | utils.exists.mockReturnValue(true) 39 | const { default: Commands } = jest.requireActual('src/cli/commands') 40 | 41 | const args = ['blueprint/key'] 42 | const nuxt = {} 43 | const options = { dir: '' } 44 | 45 | await Commands.eject(args, nuxt, options) 46 | 47 | expect(consola.fatal).toHaveBeenCalledTimes(1) 48 | expect(consola.fatal).toHaveBeenCalledWith(`Blueprint path '' is empty or does not exists`) 49 | 50 | resetUtilMocks(['exists']) 51 | }) 52 | 53 | test('Commands.eject logs fatal error with unknown blueprint', async () => { 54 | utils.exists.mockReturnValue(true) 55 | const { default: Commands } = jest.requireActual('src/cli/commands') 56 | 57 | const args = ['blueprint/key'] 58 | const nuxt = {} 59 | const options = { blueprints: {} } 60 | 61 | await Commands.eject(args, nuxt, options) 62 | 63 | expect(consola.fatal).toHaveBeenCalledTimes(1) 64 | expect(consola.fatal).toHaveBeenCalledWith(`Unrecognized blueprint 'blueprint'`) 65 | 66 | resetUtilMocks(['exists']) 67 | }) 68 | 69 | test('Commands.eject logs fatal error with autodiscover for blueprint path returns nothing', async () => { 70 | utils.exists.mockReturnValue(true) 71 | Blueprint.autodiscover.mockReturnValue(false) 72 | const { default: Commands } = jest.requireActual('src/cli/commands') 73 | 74 | const args = ['blueprint/key'] 75 | const nuxt = {} 76 | const options = { 77 | blueprints: { 78 | blueprint: '/var/nuxt' 79 | } 80 | } 81 | 82 | await Commands.eject(args, nuxt, options) 83 | 84 | expect(consola.fatal).toHaveBeenCalledTimes(1) 85 | expect(consola.fatal).toHaveBeenCalledWith(`Unrecognized blueprint path, autodiscovery failed for '/var/nuxt'`) 86 | 87 | resetUtilMocks(['exists']) 88 | }) 89 | 90 | test('Commands.eject calls normalizeInput function & logs fatal error when prompt returns zero templates', async () => { 91 | utils.exists.mockReturnValue(true) 92 | Blueprint.autodiscover.mockReturnValue(true) 93 | inquirer.prompt.mockReturnValue({ templates: [] }) 94 | const { default: Commands } = jest.requireActual('src/cli/commands') 95 | 96 | const args = ['template-key'] 97 | const nuxt = {} 98 | const options = { 99 | dir: '/var/nuxt', 100 | normalizeInput: jest.fn(_ => _) 101 | } 102 | 103 | await Commands.eject(args, nuxt, options) 104 | 105 | expect(options.normalizeInput).toHaveBeenCalledTimes(1) 106 | expect(options.normalizeInput).toHaveBeenCalledWith(`template-key`) 107 | 108 | expect(consola.fatal).toHaveBeenCalledTimes(1) 109 | expect(consola.fatal).toHaveBeenCalledWith(`Unrecognized template key 'template-key'`) 110 | 111 | resetUtilMocks(['exists']) 112 | }) 113 | 114 | test('Commands.eject calls ejectTheme helper', async () => { 115 | utils.exists.mockReturnValue(true) 116 | Blueprint.autodiscover.mockReturnValue(true) 117 | const { default: Commands } = jest.requireActual('src/cli/commands') 118 | 119 | const args = ['theme'] 120 | const nuxt = {} 121 | const options = { 122 | dir: '/var/nuxt' 123 | } 124 | 125 | await Commands.eject(args, nuxt, options) 126 | 127 | expect(ejectHelpers.ejectTheme).toHaveBeenCalledTimes(1) 128 | expect(ejectHelpers.ejectTheme).toHaveBeenCalledWith(undefined, '/var/nuxt') 129 | 130 | resetUtilMocks(['exists']) 131 | }) 132 | 133 | test('Commands.eject calls ejectTemplate helper (options.templates.type)', async () => { 134 | utils.exists.mockReturnValue(true) 135 | Blueprint.autodiscover.mockReturnValue(true) 136 | const { default: Commands } = jest.requireActual('src/cli/commands') 137 | 138 | const args = ['plugins'] 139 | const nuxt = {} 140 | const options = { 141 | dir: '/var/nuxt', 142 | templates: { 143 | plugins: ['test-plugin.js'] 144 | } 145 | } 146 | 147 | await Commands.eject(args, nuxt, options) 148 | 149 | expect(ejectHelpers.ejectTemplates).toHaveBeenCalledTimes(1) 150 | expect(ejectHelpers.ejectTemplates).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), ['test-plugin.js']) 151 | 152 | resetUtilMocks(['exists']) 153 | }) 154 | 155 | test('Commands.eject calls ejectTemplate helper (template\'s dst path)', async () => { 156 | utils.exists.mockReturnValue(true) 157 | Blueprint.autodiscover.mockReturnValue({ 158 | plugins: [{ dst: 'test-plugin.js' }] 159 | }) 160 | 161 | const { default: Commands } = jest.requireActual('src/cli/commands') 162 | 163 | const args = ['test-plugin.js'] 164 | const nuxt = {} 165 | const options = { 166 | dir: '/var/nuxt' 167 | } 168 | 169 | await Commands.eject(args, nuxt, options) 170 | 171 | expect(ejectHelpers.ejectTemplates).toHaveBeenCalledTimes(1) 172 | expect(ejectHelpers.ejectTemplates).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), [{ dst: 'test-plugin.js' }]) 173 | 174 | resetUtilMocks(['exists']) 175 | }) 176 | 177 | test('Commands.eject calls ejectTemplate helper (selected with prompt)', async () => { 178 | utils.exists.mockReturnValue(true) 179 | Blueprint.autodiscover.mockReturnValue({ 180 | plugins: [{ dst: 'test-plugin.js' }] 181 | }) 182 | inquirer.prompt.mockReturnValue({ templates: [['plugins', 0]] }) 183 | 184 | const { default: Commands } = jest.requireActual('src/cli/commands') 185 | 186 | const args = ['something-plugin.js'] 187 | const nuxt = {} 188 | const options = { 189 | dir: '/var/nuxt' 190 | } 191 | 192 | await Commands.eject(args, nuxt, options) 193 | 194 | expect(ejectHelpers.ejectTemplates).toHaveBeenCalledTimes(1) 195 | expect(ejectHelpers.ejectTemplates).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), [{ dst: 'test-plugin.js' }]) 196 | 197 | resetUtilMocks(['exists']) 198 | }) 199 | }) 200 | -------------------------------------------------------------------------------- /test/unit/cli.test.js: -------------------------------------------------------------------------------- 1 | import { NuxtCommand } from '@nuxt/cli' 2 | import consola from 'consola' 3 | import runCommand from 'src/cli' 4 | 5 | import * as utils from 'src/utils' 6 | import { resetUtilMocks as _resetUtilMocks } from 'test-utils' 7 | jest.mock('src/utils') 8 | const resetUtilMocks = utilNames => _resetUtilMocks(utils, utilNames) 9 | 10 | jest.mock('@nuxt/cli') 11 | jest.mock('src/cli/commands') 12 | 13 | describe('cli', () => { 14 | beforeAll(() => resetUtilMocks()) 15 | 16 | afterEach(() => jest.resetAllMocks()) 17 | 18 | test('runCommand applies options to NuxtCommand', () => { 19 | let command 20 | NuxtCommand.run.mockImplementation(cmd => (command = cmd)) 21 | 22 | runCommand({ 23 | name: 'test-name', 24 | description: 'test-description' 25 | }) 26 | 27 | expect(command.name).toEqual('test-name') 28 | expect(command.description).toEqual('test-description') 29 | expect(command.usage).toEqual(expect.stringContaining('test-name')) 30 | expect(command.run).toBeInstanceOf(Function) 31 | 32 | NuxtCommand.run.mockReset() 33 | }) 34 | 35 | test('runCommand.run logs fatal error without command', async () => { 36 | let command 37 | NuxtCommand.run.mockImplementation(cmd => (command = cmd)) 38 | 39 | runCommand() 40 | await command.run({ 41 | argv: { 42 | _: [] 43 | } 44 | }) 45 | 46 | expect(consola.fatal).toHaveBeenCalledTimes(1) 47 | expect(consola.fatal).toHaveBeenCalledWith(`Unrecognized command ''`) 48 | 49 | NuxtCommand.run.mockReset() 50 | }) 51 | 52 | test('runCommand.run logs fatal error on unknown command', async () => { 53 | let command 54 | NuxtCommand.run.mockImplementation(cmd => (command = cmd)) 55 | 56 | runCommand() 57 | await command.run({ 58 | argv: { 59 | _: ['does-not-exists'] 60 | } 61 | }) 62 | 63 | expect(consola.fatal).toHaveBeenCalledTimes(1) 64 | expect(consola.fatal).toHaveBeenCalledWith(`Unrecognized command 'does-not-exists'`) 65 | 66 | NuxtCommand.run.mockReset() 67 | }) 68 | 69 | test('runCommand.run does not log error on known command', async () => { 70 | let command 71 | NuxtCommand.run.mockImplementation(cmd => (command = cmd)) 72 | 73 | runCommand() 74 | await command.run({ 75 | getNuxtConfig: jest.fn(), 76 | getNuxt: jest.fn(), 77 | argv: { 78 | _: ['eject'] 79 | } 80 | }) 81 | 82 | expect(consola.fatal).not.toHaveBeenCalled() 83 | 84 | NuxtCommand.run.mockReset() 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/unit/utils.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import klaw from 'klaw' 3 | import { 4 | ucfirst, 5 | abstractGuard, 6 | runOnceGuard, 7 | runOnceGuardBlocking, 8 | exists, 9 | createFileFilter, 10 | walk 11 | } from 'src/utils' 12 | 13 | jest.mock('fs') 14 | jest.mock('klaw') 15 | 16 | describe('utils', () => { 17 | afterEach(() => jest.resetAllMocks()) 18 | 19 | test('ucfirst', () => { 20 | expect(ucfirst()).toEqual('') 21 | expect(ucfirst('hello')).toEqual('Hello') 22 | expect(ucfirst('Hello')).toEqual('Hello') 23 | expect(ucfirst('HELLO')).toEqual('Hello') 24 | expect(ucfirst('hello test')).toEqual('Hello test') 25 | }) 26 | 27 | test('abstractGuard', () => { 28 | expect(() => abstractGuard('AbstractClass', 'AbstractClass')).toThrowError(/abstract class/) 29 | expect(() => abstractGuard('DerivedClass', 'AbstractClass')).not.toThrow() 30 | }) 31 | 32 | test('runOnceGuard', () => { 33 | const instance = {} 34 | const name = 'test-guard' 35 | 36 | expect(runOnceGuard(instance, name)).toBe(true) 37 | expect(runOnceGuard(instance, name)).toBe(false) 38 | expect(runOnceGuard(instance, name)).toBe(false) 39 | }) 40 | 41 | test('runOnceGuardBlocking', async () => { 42 | const instance = {} 43 | const name = 'test-guard' 44 | 45 | const guard1 = runOnceGuardBlocking(instance, name) 46 | const guard2 = runOnceGuardBlocking(instance, name) 47 | const guard3 = runOnceGuardBlocking(instance, name) 48 | 49 | const releaseBlock = await guard1 50 | expect(releaseBlock).toBeInstanceOf(Function) 51 | releaseBlock() 52 | 53 | await expect(Promise.all([guard2, guard3])).resolves.toEqual([false, false]) 54 | await expect(runOnceGuardBlocking(instance, name)).resolves.toEqual(false) 55 | }) 56 | 57 | test('exists', () => { 58 | let accessCb 59 | fs.access.mockImplementation((_, __, cb) => (accessCb = cb)) 60 | 61 | const pe = exists('/some/existing/path') 62 | accessCb() 63 | expect(pe).resolves.toBe(true) 64 | 65 | const pn = exists('/some/non-existing/path') 66 | accessCb(true) 67 | expect(pn).resolves.toBe(false) 68 | }) 69 | 70 | test('createFileFilter', () => { 71 | expect(createFileFilter()).toBeUndefined() 72 | 73 | const filterRE = createFileFilter(/.js$/) 74 | expect(filterRE).toBeInstanceOf(Function) 75 | expect(filterRE('/folder/test.js')).toBe(true) 76 | expect(filterRE('/folder/test.js.bak')).toBe(false) 77 | 78 | const filterSTR = createFileFilter('.js') 79 | expect(filterSTR).toBeInstanceOf(Function) 80 | expect(filterSTR('/folder/test.js')).toBe(true) 81 | expect(filterSTR('/folder/test.js.bak')).toBe(true) 82 | expect(filterSTR('/folder/test.bak')).toBe(false) 83 | 84 | const filterFN = createFileFilter(() => true) 85 | expect(filterFN).toBeInstanceOf(Function) 86 | expect(filterFN('/folder/test.js')).toBe(true) 87 | expect(filterFN('/folder/test.js.bak')).toBe(true) 88 | expect(filterFN('/folder/test.bak')).toBe(true) 89 | }) 90 | 91 | test('walk (basic)', async () => { 92 | const eventFns = {} 93 | const klawMock = { 94 | on: jest.fn((event, fn) => { 95 | eventFns[event] = fn 96 | return klawMock 97 | }) 98 | } 99 | klaw.mockReturnValue(klawMock) 100 | 101 | const p = walk('/test/dir') 102 | 103 | expect(eventFns.data).toBeInstanceOf(Function) 104 | expect(eventFns.end).toBeInstanceOf(Function) 105 | 106 | eventFns.data({ path: '/test/dir/a-file.js' }) 107 | eventFns.end() 108 | 109 | const matches = await p 110 | expect(matches).toEqual(['a-file.js']) 111 | }) 112 | 113 | test('walk (validate)', async () => { 114 | const eventFns = {} 115 | const klawMock = { 116 | on: jest.fn((event, fn) => { 117 | eventFns[event] = fn 118 | return klawMock 119 | }) 120 | } 121 | klaw.mockReturnValue(klawMock) 122 | 123 | const validate = 'some-string-that-doesnt-exists-in-the-test-path' 124 | const p = walk('/test/dir', { validate }) 125 | 126 | expect(eventFns.data).toBeInstanceOf(Function) 127 | expect(eventFns.end).toBeInstanceOf(Function) 128 | 129 | eventFns.data({ path: '/test/dir/a-file.js' }) 130 | eventFns.end() 131 | 132 | const matches = await p 133 | expect(matches).toEqual([]) 134 | }) 135 | 136 | test('walk (sliceRoot: false)', async () => { 137 | const eventFns = {} 138 | const klawMock = { 139 | on: jest.fn((event, fn) => { 140 | eventFns[event] = fn 141 | return klawMock 142 | }) 143 | } 144 | klaw.mockReturnValue(klawMock) 145 | 146 | const p = walk('/test/dir', { sliceRoot: false }) 147 | 148 | expect(eventFns.data).toBeInstanceOf(Function) 149 | expect(eventFns.end).toBeInstanceOf(Function) 150 | 151 | eventFns.data({ path: '/test/dir/a-file.js' }) 152 | eventFns.end() 153 | 154 | const matches = await p 155 | expect(matches).toEqual(['/test/dir/a-file.js']) 156 | }) 157 | 158 | test('walk (sliceRoot: string)', async () => { 159 | const eventFns = {} 160 | const klawMock = { 161 | on: jest.fn((event, fn) => { 162 | eventFns[event] = fn 163 | return klawMock 164 | }) 165 | } 166 | klaw.mockReturnValue(klawMock) 167 | 168 | const p = walk('/test/dir', { sliceRoot: '/test/' }) 169 | 170 | expect(eventFns.data).toBeInstanceOf(Function) 171 | expect(eventFns.end).toBeInstanceOf(Function) 172 | 173 | eventFns.data({ path: '/test/dir/a-file.js' }) 174 | eventFns.end() 175 | 176 | const matches = await p 177 | expect(matches).toEqual(['dir/a-file.js']) 178 | }) 179 | }) 180 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | const actualUtils = jest.requireActual('src/utils') 2 | 3 | export function resetUtilMocks (mockedUtils, utilNames) { 4 | for (const utilName in actualUtils) { 5 | if (!utilNames || utilNames.includes(utilName)) { 6 | mockedUtils[utilName].mockReset() 7 | mockedUtils[utilName].mockImplementation(actualUtils[utilName]) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/utils/setup.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | 3 | consola.mockTypes(() => jest.fn()) 4 | --------------------------------------------------------------------------------