├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── bin └── create-strv-app.js ├── logo.svg ├── package.json ├── src ├── Generator.js ├── PluginAPI.js ├── create.js ├── plugins │ ├── commitlint │ │ └── index.js │ ├── cypress │ │ ├── index.js │ │ └── templates │ │ │ └── e2e │ │ │ ├── .eslintrc.js │ │ │ ├── plugins │ │ │ └── index.js │ │ │ ├── specs │ │ │ └── test.js │ │ │ └── support │ │ │ ├── commands.js │ │ │ └── index.js │ ├── dotfiles │ │ ├── index.js │ │ └── template │ │ │ ├── _editorconfig │ │ │ └── _gitignore │ ├── eslint │ │ └── index.js │ ├── firebase │ │ ├── index.js │ │ └── templates │ │ │ └── firebase.json │ ├── git-hooks │ │ └── index.js │ ├── graphql │ │ └── index.js │ ├── heroku │ │ └── index.js │ ├── prettier │ │ └── index.js │ ├── redux │ │ ├── index.js │ │ └── templates │ │ │ ├── modules │ │ │ └── index.js │ │ │ └── store.js │ ├── styled-components │ │ ├── index.js │ │ └── templates │ │ │ ├── _document.js │ │ │ └── global.js │ ├── stylelint │ │ └── index.js │ └── typescript │ │ ├── index.js │ │ └── template │ │ └── tsconfig.json ├── readmes │ ├── default.md │ ├── index.js │ └── styled-components.md └── utils │ ├── banner.js │ ├── merge.js │ └── sort-object.js ├── templates ├── .eslintrc.js ├── SPA │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── readme.md │ └── src │ │ ├── config.js │ │ ├── index.js │ │ ├── serviceWorker.js │ │ ├── styles │ │ ├── colors.js │ │ └── fonts.js │ │ └── views │ │ └── index.js └── SSR │ ├── components │ └── head.js │ ├── next.config.js │ ├── package.json │ ├── pages │ └── index.js │ ├── readme.md │ └── static │ └── favicon.ico ├── thumbnail.gif └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [COMMIT_EDITMSG] 12 | max_line_length = 70 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "@strv/javascript/environments/nodejs/v8", 5 | "@strv/javascript/environments/nodejs/optional", 6 | "prettier" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2018 10 | }, 11 | "rules": { 12 | "no-sync": 0, 13 | "no-console": 0, 14 | "no-process-exit": 0, 15 | "react/prop-types": 0, 16 | "no-process-env": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | dist 6 | build 7 | 8 | # testing 9 | .nyc_output 10 | coverage 11 | 12 | # logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # misc 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package.json 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pavel Prichodko 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 | ![](https://raw.githubusercontent.com/prichodko/create-strv-app/master/thumbnail.gif) 2 | 3 | By running one command this command line interface automatically generates a new project according to STRV's best practices. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | npx create-strv-app 9 | ``` 10 | 11 | alternatively 12 | 13 | ```bash 14 | yarn create strv-app 15 | ``` 16 | 17 | ## Author 18 | 19 | Pavel Prichodko ([@prchdk](https://twitter.com/prchdk)) 20 | 21 | ## License 22 | 23 | [MIT](https://github.com/strvcom/create-strv-app/blob/docs/readme/LICENSE) 24 | -------------------------------------------------------------------------------- /bin/create-strv-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const updateNotifier = require('update-notifier') 6 | const meow = require('meow') 7 | const chalk = require('chalk') 8 | 9 | const create = require('../src/create') 10 | const pkg = require('../package') 11 | 12 | updateNotifier({ pkg }).notify() 13 | require('please-upgrade-node')(pkg) 14 | 15 | const cli = meow(` 16 | ${chalk.gray('Usage')} 17 | 18 | ${chalk.gray('$')} create-strv-app ${chalk.yellow('[project-directory]')} 19 | `) 20 | 21 | create(cli.input[0] || '.') 22 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | create-strv-app 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-strv-app", 3 | "description": "Set up new app in line with STRV's best practices in one command", 4 | "version": "2.0.2", 5 | "author": "Pavel Prichodko ", 6 | "bugs": { 7 | "url": "https://github.com/prichodko/create-strv-app/issues" 8 | }, 9 | "keywords": [ 10 | "create-react-app", 11 | "react", 12 | "strv" 13 | ], 14 | "engines": { 15 | "node": ">=8.3.0" 16 | }, 17 | "files": [ 18 | "bin", 19 | "src", 20 | "templates" 21 | ], 22 | "bin": "./bin/create-strv-app.js", 23 | "repository": "prichodko/create-strv-app", 24 | "license": "MIT", 25 | "scripts": { 26 | "lint": "eslint bin src", 27 | "lint:templates": "eslint templates", 28 | "format": "prettier --write '*/**/*.{css,md,json,js}'" 29 | }, 30 | "prettier": { 31 | "semi": false, 32 | "singleQuote": true, 33 | "trailingComma": "es5" 34 | }, 35 | "lint-staged": { 36 | "*.js": [ 37 | "eslint --fix", 38 | "prettier --write", 39 | "git add" 40 | ], 41 | "*.{json,css,md}": [ 42 | "prettier --write", 43 | "git add" 44 | ] 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "dependencies": { 52 | "@babel/parser": "^7.2.2", 53 | "chalk": "^2.4.1", 54 | "execa": "^1.0.0", 55 | "fs-extra": "^7.0.1", 56 | "globby": "^8.0.1", 57 | "inquirer": "^6.2.1", 58 | "isbinaryfile": "^3.0.3", 59 | "lodash.isarray": "^4.0.0", 60 | "lodash.merge": "^4.6.1", 61 | "lodash.mergewith": "^4.6.1", 62 | "log-symbols": "^2.2.0", 63 | "meow": "^5.0.0", 64 | "mri": "^1.1.1", 65 | "ms": "^2.1.1", 66 | "ora": "^3.0.0", 67 | "please-upgrade-node": "^3.1.1", 68 | "recast": "^0.16.1", 69 | "sade": "^1.4.1", 70 | "sort-keys": "^2.0.0", 71 | "update-notifier": "^2.5.0", 72 | "validate-npm-package-name": "^3.0.0" 73 | }, 74 | "devDependencies": { 75 | "@commitlint/cli": "^7.2.1", 76 | "@commitlint/config-conventional": "^7.1.2", 77 | "@strv/eslint-config-javascript": "^9.1.1", 78 | "@types/jest": "^23.3.10", 79 | "@types/next": "^7.0.5", 80 | "@types/node": "^10.12.15", 81 | "@types/react": "^16.7.17", 82 | "@types/react-dom": "^16.0.11", 83 | "@types/react-redux": "^6.0.11", 84 | "@types/react-router-dom": "^4.3.1", 85 | "@types/styled-components": "^4.1.4", 86 | "@types/webpack-env": "^1.13.6", 87 | "@zeit/next-typescript": "^1.1.1", 88 | "babel-eslint": "^10.0.1", 89 | "babel-plugin-styled-components": "^1.10.0", 90 | "eslint": "^5.10.0", 91 | "eslint-config-prettier": "^3.3.0", 92 | "eslint-plugin-cypress": "^2.1.3", 93 | "husky": "^1.2.1", 94 | "lint-staged": "^8.1.0", 95 | "prettier": "^1.15.3", 96 | "react-redux": "^6.0.0", 97 | "redux": "^4.0.1", 98 | "redux-devtools-extension": "^2.13.7", 99 | "styled-components": "^4.1.2", 100 | "stylelint": "^9.9.0", 101 | "stylelint-config-standard": "^18.2.0", 102 | "stylelint-config-styled-components": "^0.1.1", 103 | "stylelint-processor-styled-components": "^1.5.1", 104 | "typescript": "^3.2.2" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const fs = require('fs-extra') 6 | 7 | const sortObject = require('./utils/sort-object') 8 | const readmes = require('./readmes') 9 | const PluginAPI = require('./PluginAPI') 10 | 11 | class Generator { 12 | constructor({ name, type, plugins, targetDir }) { 13 | this.name = name 14 | this.type = type 15 | this.plugins = plugins 16 | this.targetDir = targetDir 17 | 18 | this.pkg = { 19 | name, 20 | version: '0.1.0', 21 | private: true, 22 | author: '', 23 | scripts: {}, 24 | devDependencies: {}, 25 | dependencies: {}, 26 | } 27 | this.babel = null 28 | this.copyFiles = {} 29 | this.writeFiles = {} 30 | this.completeCbs = [] 31 | } 32 | 33 | sortPackageJson() { 34 | this.pkg.dependencies = sortObject(this.pkg.dependencies) 35 | this.pkg.devDependencies = sortObject(this.pkg.devDependencies) 36 | this.pkg.scripts = sortObject(this.pkg.scripts, [ 37 | 'start', 38 | 'dev', 39 | 'build', 40 | 'test', 41 | 'e2e', 42 | 'lint', 43 | 'deploy', 44 | ]) 45 | this.pkg = sortObject(this.pkg, [ 46 | 'name', 47 | 'version', 48 | 'private', 49 | 'description', 50 | 'author', 51 | 'scripts', 52 | 'dependencies', 53 | 'devDependencies', 54 | 'babel', 55 | 'lint-staged', 56 | 'husky', 57 | 'prettier', 58 | 'eslintConfig', 59 | 'stylelint', 60 | 'browserslist', 61 | 'jest', 62 | ]) 63 | } 64 | 65 | copyFileTree() { 66 | return Promise.all( 67 | Object.keys(this.copyFiles).map(async name => { 68 | const filePath = path.join(this.targetDir, name) 69 | await fs.ensureDir(path.dirname(filePath)) 70 | await fs.copy(this.copyFiles[name], filePath) 71 | }) 72 | ) 73 | } 74 | 75 | writeFileTree() { 76 | return Promise.all( 77 | Object.keys(this.writeFiles).map(async name => { 78 | const filePath = path.join(this.targetDir, name) 79 | await fs.ensureDir(path.dirname(filePath)) 80 | await fs.writeFile(filePath, this.writeFiles[name]) 81 | }) 82 | ) 83 | } 84 | 85 | renderReadme(templateDir) { 86 | const PLACEHOLDERS = [{ re: '', value: this.name }] 87 | 88 | const filtered = readmes.filter(readme => { 89 | if (!readme.plugin) { 90 | return true 91 | } 92 | 93 | return Object.keys(this.plugins).includes(readme.plugin) 94 | }) 95 | 96 | // add readme from template at the beginning 97 | filtered.unshift({ file: path.resolve(templateDir, 'readme.md') }) 98 | 99 | const result = filtered.reduce((acc, { file }) => { 100 | let content = fs.readFileSync(file, 'utf8') 101 | PLACEHOLDERS.forEach(p => { 102 | const re = new RegExp(p.re, 'ug') 103 | content = content.replace(re, p.value) 104 | }) 105 | acc = acc.concat(content, '\n') 106 | return acc 107 | }, '') 108 | 109 | const filePath = path.resolve(this.targetDir, 'readme.md') 110 | return fs.writeFile(filePath, result, 'utf8') 111 | } 112 | 113 | onComplete() { 114 | this.completeCbs.forEach(cb => cb()) 115 | } 116 | 117 | async generate() { 118 | const templateDir = path.resolve(__dirname, '../templates', this.type) 119 | 120 | const templatePkgJson = fs.readJsonSync( 121 | path.resolve(templateDir, 'package.json') 122 | ) 123 | 124 | this.pkg = { 125 | ...this.pkg, 126 | ...templatePkgJson, 127 | } 128 | 129 | const api = new PluginAPI(this) 130 | api.copy(templateDir) 131 | 132 | this.plugins.forEach(plugin => plugin.apply(api)) 133 | 134 | this.sortPackageJson() 135 | 136 | this.writeFiles['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n' 137 | if (this.babel) { 138 | this.writeFiles['.babelrc'] = JSON.stringify(this.babel, null, 2) + '\n' 139 | } 140 | 141 | // we provide our own 142 | delete this.copyFiles['package.json'] 143 | delete this.copyFiles['readme.md'] 144 | 145 | await this.copyFileTree() 146 | await this.writeFileTree() 147 | await this.renderReadme(templateDir) 148 | } 149 | } 150 | 151 | module.exports = Generator 152 | -------------------------------------------------------------------------------- /src/PluginAPI.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const globby = require('globby') 6 | const chalk = require('chalk') 7 | 8 | const merge = require('./utils/merge') 9 | const { devDependencies } = require('../package.json') 10 | 11 | const isString = val => typeof val === 'string' 12 | 13 | class PluginAPI { 14 | constructor(generator) { 15 | this.generator = generator 16 | } 17 | 18 | is(type) { 19 | return this.generator.type === type 20 | } 21 | 22 | hasPlugin(id) { 23 | return this.generator.plugins.some(plugin => plugin.name === id) 24 | } 25 | 26 | updatePackageJson(fields) { 27 | this.generator.pkg = merge(this.generator.pkg, fields) 28 | } 29 | 30 | updateBabel(fields) { 31 | if (this.generator.babel === null) { 32 | this.generator.babel = { 33 | presets: this.is('SPA') 34 | ? ['@strv/react-scripts/babel'] 35 | : ['next/babel'], 36 | } 37 | } 38 | 39 | this.generator.babel = merge(this.generator.babel, fields) 40 | } 41 | 42 | addDependency(name) { 43 | const dep = devDependencies[name] 44 | if (dep) { 45 | this.updatePackageJson({ dependencies: { [name]: dep } }) 46 | } else { 47 | console.log() 48 | console.log( 49 | chalk.red( 50 | `Please add ${dep} to create-strv-app package.json dependencies.` 51 | ) 52 | ) 53 | } 54 | } 55 | 56 | addDevDependency(name) { 57 | const dep = devDependencies[name] 58 | if (dep) { 59 | this.updatePackageJson({ devDependencies: { [name]: dep } }) 60 | } else { 61 | console.log() 62 | console.log( 63 | chalk.red( 64 | `Please add ${dep} to create-strv-app package.json devDependencies.` 65 | ) 66 | ) 67 | } 68 | } 69 | 70 | onCreateComplete(cb) { 71 | this.generator.completeCbs.push(cb) 72 | } 73 | 74 | copy(source) { 75 | if (isString(source)) { 76 | const sourceFiles = globby.sync(['**/*'], { cwd: source }) 77 | 78 | for (const file of sourceFiles) { 79 | let filename = path.basename(file) 80 | 81 | // dotfiles are ignored on npm, they should be prefixed with underscore 82 | // we convert them back to dotfiles during generating 83 | if (filename.charAt(0) === '_') { 84 | filename = `.${filename.slice(1)}` 85 | } 86 | 87 | const targetPath = path.join(path.dirname(file), filename) 88 | const sourcePath = path.resolve(source, file) 89 | 90 | this.generator.copyFiles[targetPath] = sourcePath 91 | } 92 | } else { 93 | source(this.generator.copyFiles) 94 | } 95 | } 96 | 97 | render(source) { 98 | source(this.generator.writeFiles) 99 | } 100 | } 101 | 102 | module.exports = PluginAPI 103 | -------------------------------------------------------------------------------- /src/create.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const ms = require('ms') 6 | const execa = require('execa') 7 | const fs = require('fs-extra') 8 | const { prompt } = require('inquirer') 9 | const validateName = require('validate-npm-package-name') 10 | const chalk = require('chalk') 11 | const ora = require('ora') 12 | 13 | const Generator = require('./Generator') 14 | const banner = require('./utils/banner') 15 | 16 | const dotfiles = require('./plugins/dotfiles') 17 | const eslint = require('./plugins/eslint') 18 | const prettier = require('./plugins/prettier') 19 | const stylelint = require('./plugins/stylelint') 20 | const gitHooks = require('./plugins/git-hooks') 21 | const commitlint = require('./plugins/commitlint') 22 | const styledComponents = require('./plugins/styled-components') 23 | const redux = require('./plugins/redux') 24 | const typescript = require('./plugins/typescript') 25 | // const firebase = require('./plugins/firebase') 26 | // const heroku = require('./plugins/heroku') 27 | // const cypress = require('./plugins/cypress') 28 | 29 | // const isTest = process.env.TEST 30 | 31 | const DEFAULT_PLUGINS = [ 32 | { 33 | name: 'dotfiles', 34 | apply: dotfiles, 35 | }, 36 | { 37 | name: 'ESLint', 38 | apply: eslint, 39 | }, 40 | { 41 | name: 'Prettier', 42 | apply: prettier, 43 | }, 44 | { 45 | name: 'git-hooks', 46 | apply: gitHooks, 47 | }, 48 | { 49 | name: 'stylelint', 50 | apply: stylelint, 51 | }, 52 | ] 53 | 54 | let spinner = ora({ 55 | color: 'red', 56 | }) 57 | 58 | async function create() { 59 | const start = Date.now() 60 | 61 | banner() 62 | 63 | const { name } = await prompt({ 64 | name: 'name', 65 | message: 'What is the name of the project?', 66 | validate: value => { 67 | const { errors } = validateName(value) 68 | if (errors) { 69 | errors.unshift(`Invalid package name: ${value}`) 70 | return errors 71 | .map(e => e.charAt(0).toUpperCase() + e.substring(1)) 72 | .join(`\n${chalk.red('>>')} `) 73 | } 74 | 75 | const targetDir = path.resolve(value) 76 | if (fs.existsSync(targetDir)) { 77 | return `Target directory ${chalk.cyan(targetDir)} already exists.` 78 | } 79 | 80 | return true 81 | }, 82 | }) 83 | 84 | const { type } = await prompt({ 85 | name: 'type', 86 | type: 'list', 87 | message: 'What type of application do you need?', 88 | choices: [ 89 | { name: 'Single-page', value: 'SPA' }, 90 | { name: 'Server-rendered', value: 'SSR' }, 91 | // { name: 'Static', value: 'Static' }, 92 | ], 93 | }) 94 | 95 | const { packageManager } = await prompt({ 96 | name: 'packageManager', 97 | message: 'What is your preferred package manager?', 98 | type: 'list', 99 | choices: [{ name: 'Yarn', value: 'yarn' }, 'npm'], 100 | }) 101 | 102 | const { useTypeScript } = await prompt({ 103 | name: 'useTypeScript', 104 | type: 'confirm', 105 | message: 'Do you want to use TypeScript?', 106 | }) 107 | 108 | const { optionalPlugins } = await prompt({ 109 | name: 'optionalPlugins', 110 | message: 'Add some optional plugins?', 111 | type: 'checkbox', 112 | choices: [ 113 | { 114 | name: 'styled-components', 115 | value: { name: 'styled-components', apply: styledComponents }, 116 | }, 117 | { 118 | name: 'redux', 119 | value: { name: 'redux', apply: redux }, 120 | }, 121 | { 122 | name: 'commitlint', 123 | value: { name: 'commitlint', apply: commitlint }, 124 | }, 125 | ], 126 | }) 127 | 128 | const targetDir = path.resolve(name) 129 | const plugins = optionalPlugins.concat(DEFAULT_PLUGINS) 130 | if (useTypeScript) { 131 | plugins.push({ name: 'TypeScript', apply: typescript }) 132 | } 133 | 134 | // run the generator 135 | spinner.start('Generating project') 136 | const generator = new Generator({ name, type, plugins, targetDir }) 137 | await generator.generate() 138 | spinner.succeed(`Project generated at ${chalk.blue(targetDir)}`) 139 | 140 | // install dependencies 141 | spinner.start(`Installing dependencies with ${chalk.blue(packageManager)}`) 142 | await execa(packageManager, ['install'], { cwd: targetDir }) 143 | spinner.succeed(`Dependencies installed with ${chalk.blue(packageManager)}`) 144 | 145 | // intialize git 146 | spinner.start('Initializing git') 147 | await execa('git', ['init'], { cwd: targetDir }) 148 | await execa('git', ['add', '-A'], { cwd: targetDir }) 149 | await execa('git', ['commit', '-m', 'Initial commit'], { 150 | cwd: targetDir, 151 | }) 152 | await execa('node', ['node_modules/husky/husky.js', 'install'], { 153 | cwd: targetDir, 154 | }) 155 | spinner.succeed(`${chalk.blue('git')} initialized`) 156 | 157 | spinner.succeed(`Project created in ${chalk.blue(ms(Date.now() - start))} 🎉`) 158 | 159 | generator.onComplete() 160 | } 161 | 162 | // eslint-disable-next-line 163 | module.exports = (...args) => { 164 | create().catch(error => { 165 | spinner.fail(`Error: ${error.message}`) 166 | // if (process.env.DEBUG) { 167 | console.log(error) 168 | // } 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /src/plugins/commitlint/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDevDependency('@commitlint/cli') 5 | api.addDevDependency('@commitlint/config-conventional') 6 | api.updatePackageJson({ 7 | commitlint: { 8 | extends: ['@commitlint/config-conventional'], 9 | }, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/cypress/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDevDependency('cypress') 5 | api.addDevDependency('eslint-plugin-cypress') 6 | api.updatePackageJson({ 7 | scripts: { 8 | 'test:e2e': 'cypress run', 9 | 'cypress:open': 'cypress open', 10 | }, 11 | }) 12 | api.copy(require.resolve('./templates')) 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins/cypress/templates/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cypress'], 3 | env: { 4 | mocha: true, 5 | 'cypress/globals': true, 6 | }, 7 | rules: { 8 | strict: 'off', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/cypress/templates/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/guides/guides/plugins-guide.html 2 | 3 | module.exports = (on, config) => { 4 | return Object.assign({}, config, { 5 | fixturesFolder: 'tests/e2e/fixtures', 6 | integrationFolder: 'tests/e2e/specs', 7 | screenshotsFolder: 'tests/e2e/screenshots', 8 | videosFolder: 'tests/e2e/videos', 9 | supportFile: 'tests/e2e/support/index.js', 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/cypress/templates/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'Welcome to Your Vue.js App') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/plugins/cypress/templates/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /src/plugins/cypress/templates/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /src/plugins/dotfiles/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | module.exports = api => { 6 | api.copy(path.resolve(__dirname, './template')) 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/dotfiles/template/_editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [COMMIT_EDITMSG] 12 | max_line_length = 70 -------------------------------------------------------------------------------- /src/plugins/dotfiles/template/_gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | dist 6 | build 7 | .next 8 | 9 | # testing 10 | .nyc_output 11 | coverage 12 | 13 | # logs 14 | *.log 15 | 16 | # misc 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /src/plugins/eslint/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | if (!api.is('SPA')) { 5 | api.addDevDependency('eslint') 6 | api.addDevDependency('babel-eslint') 7 | } 8 | api.addDevDependency('@strv/eslint-config-javascript') 9 | api.addDevDependency('eslint-config-prettier') 10 | 11 | api.updatePackageJson({ 12 | scripts: { 13 | lint: 'eslint .', 14 | }, 15 | eslintConfig: { 16 | extends: [ 17 | '@strv/javascript/environments/react/v16', 18 | 'prettier', 19 | 'prettier/react', 20 | ], 21 | root: true, 22 | env: { 23 | browser: true, 24 | commonjs: true, 25 | es6: true, 26 | jest: true, 27 | node: true, 28 | }, 29 | parser: 'babel-eslint', 30 | parserOptions: { 31 | ecmaVersion: 2018, 32 | sourceType: 'module', 33 | ecmaFeatures: { 34 | jsx: true, 35 | }, 36 | }, 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/plugins/firebase/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.copy(require.resolve('./templates')) 5 | } 6 | -------------------------------------------------------------------------------- /src/plugins/firebase/templates/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ], 11 | "headers": [ 12 | { 13 | "source": "/service-worker.js", 14 | "headers": [{ "key": "Cache-Control", "value": "no-cache" }] 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/plugins/git-hooks/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDevDependency('husky') 5 | api.addDevDependency('lint-staged') 6 | 7 | const hasTS = api.hasPlugin('TypeScript') 8 | const hasSC = api.hasPlugin('styled-components') 9 | 10 | api.updatePackageJson({ 11 | 'lint-staged': { 12 | '*.js': [ 13 | 'eslint --fix', 14 | ...(hasSC ? ['stylelint'] : []), 15 | 'prettier --write', 16 | 'git add', 17 | ], 18 | ...(hasTS && { 19 | '*.{ts,tsx}': [ 20 | ...(hasSC ? ['stylelint'] : []), 21 | 'prettier --write', 22 | 'git add', 23 | ], 24 | }), 25 | '*.{json,md}': ['prettier --write', 'git add'], 26 | '*.css': ['stylelint', 'prettier --write', 'git add'], 27 | }, 28 | husky: { 29 | hooks: { 30 | 'pre-commit': 'lint-staged', 31 | }, 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/graphql/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line 4 | module.exports = api => {} 5 | -------------------------------------------------------------------------------- /src/plugins/heroku/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line 4 | module.exports = api => {} 5 | -------------------------------------------------------------------------------- /src/plugins/prettier/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDevDependency('prettier') 5 | 6 | const ext = api.hasPlugin('TypeScript') ? 'ts,tsx' : 'js' 7 | 8 | api.updatePackageJson({ 9 | scripts: { 10 | format: `prettier --write '*/**/*.{${ext},css,md,json}'`, 11 | }, 12 | prettier: { 13 | semi: false, 14 | singleQuote: true, 15 | trailingComma: 'es5', 16 | }, 17 | }) 18 | 19 | api.render(files => { 20 | files['.prettierignore'] = 'package.json' 21 | if (!api.is('SPA')) { 22 | files['.prettierignore'] += '\n.next' 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/plugins/redux/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDependency('redux') 5 | api.addDependency('react-redux') 6 | } 7 | 8 | // api.addDependency('redux-saga') 9 | // api.addDependency('redux-observable') 10 | // api.addDependency('rxjs') 11 | 12 | // rematch 13 | // import { init } from '@rematch/core' 14 | // import models from './models' 15 | 16 | // export default initialState => { 17 | // const store = init({ 18 | // models, 19 | // redux: { 20 | // initialState 21 | // rootReducers: { 22 | // 'RESET': (state, action) => undefined, 23 | // } 24 | // } 25 | // }) 26 | 27 | // export const { dispatch } = store 28 | // export default store 29 | 30 | // redux-observable 31 | // import { createEpicMiddleware } from 'redux-observable'; 32 | // import { rootEpic } from './modules' 33 | // export const rootEpic = combineEpics( 34 | // pingEpic, 35 | // fetchUserEpic 36 | // ); 37 | // const epicMiddleware = createEpicMiddleware() 38 | // epicMiddleware.run(rootEpic); 39 | 40 | // redux-saga 41 | // const sagas = [] 42 | // export default function* rootSaga() { 43 | // yield sagas.map(saga => fork(saga)) 44 | // } 45 | // import { rootSaga } from './modules' 46 | // import createSagaMiddleware from 'redux-saga' 47 | // const sagaMiddleware = createSagaMiddleware() 48 | // sagaMiddleware.run(rootSaga) 49 | 50 | // import { Provider, connect } from 'react-redux' 51 | // import createStore from './store' 52 | // const store = createStore() 53 | // 54 | // 55 | -------------------------------------------------------------------------------- /src/plugins/redux/templates/modules/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | export const rootReducer = combineReducers() 4 | -------------------------------------------------------------------------------- /src/plugins/redux/templates/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { composeWithDevTools } from 'redux-devtools-extension' 3 | 4 | import { rootReducer } from '../modules' 5 | 6 | const middlewares = [] 7 | 8 | export default initialState => { 9 | const store = createStore( 10 | rootReducer, 11 | initialState, 12 | composeWithDevTools(applyMiddleware(...middlewares)) 13 | ) 14 | 15 | if (module.hot) { 16 | module.hot.accept('./modules/index.js', () => { 17 | store.replaceReducer(rootReducer) 18 | }) 19 | } 20 | 21 | return store 22 | } 23 | -------------------------------------------------------------------------------- /src/plugins/styled-components/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDependency('styled-components') 5 | api.addDevDependency('babel-plugin-styled-components') 6 | 7 | api.updateBabel({ 8 | plugins: api.is('SPA') 9 | ? ['babel-plugin-styled-components'] 10 | : [['babel-plugin-styled-components', { ssr: true }]], 11 | }) 12 | 13 | api.copy(files => { 14 | const dest = api.is('SPA') ? 'src/styles/global.js' : 'styles/global.js' 15 | files[dest] = require.resolve('./templates/global.js') 16 | }) 17 | 18 | if (!api.is('SPA')) { 19 | api.copy(files => { 20 | files['pages/_document.js'] = require.resolve('./templates/_document.js') 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/plugins/styled-components/templates/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document' 2 | import { ServerStyleSheet } from 'styled-components' 3 | 4 | export default class MyDocument extends Document { 5 | static getInitialProps({ renderPage }) { 6 | const sheet = new ServerStyleSheet() 7 | const page = renderPage(App => props => 8 | sheet.collectStyles() 9 | ) 10 | const styleTags = sheet.getStyleElement() 11 | return { ...page, styleTags } 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | {this.props.styleTags} 18 | 19 |
20 | 21 | 22 | 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugins/styled-components/templates/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | const GlobalStyle = createGlobalStyle`` 4 | 5 | export default GlobalStyle 6 | -------------------------------------------------------------------------------- /src/plugins/stylelint/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = api => { 4 | api.addDevDependency('stylelint') 5 | api.addDevDependency('stylelint-config-standard') 6 | 7 | if (api.hasPlugin('styled-components')) { 8 | api.addDevDependency('stylelint-processor-styled-components') 9 | api.addDevDependency('stylelint-config-styled-components') 10 | api.updatePackageJson({ 11 | stylelint: { 12 | processors: ['stylelint-processor-styled-components'], 13 | extends: [ 14 | 'stylelint-config-standard', 15 | 'stylelint-config-styled-components', 16 | ], 17 | rules: {}, 18 | }, 19 | }) 20 | } else { 21 | api.updatePackageJson({ 22 | stylelint: { 23 | extends: 'stylelint-config-standard', 24 | rules: {}, 25 | }, 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/plugins/typescript/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const chalk = require('chalk') 5 | 6 | module.exports = api => { 7 | api.addDevDependency('typescript') 8 | api.addDevDependency('@types/node') 9 | api.addDevDependency('@types/react-dom') 10 | 11 | if (api.is('SPA')) { 12 | api.addDevDependency('@types/jest') 13 | api.addDevDependency('@types/react-router-dom') 14 | api.addDevDependency('@types/webpack-env') 15 | } else { 16 | api.addDevDependency('@types/next') 17 | api.addDependency('@zeit/next-typescript') 18 | api.onCreateComplete(() => { 19 | console.log() 20 | console.log( 21 | `To Finish ${chalk.blue( 22 | 'TypeScript' 23 | )} setup you will need to update next.config.js 24 | https://github.com/zeit/next-plugins/tree/master/packages/next-typescript 25 | ` 26 | ) 27 | }) 28 | } 29 | 30 | api.updatePackageJson({ 31 | scripts: { 32 | 'type-check': 'tsc', 33 | }, 34 | }) 35 | 36 | if (api.hasPlugin('styled-components')) { 37 | api.addDevDependency('@types/styled-components') 38 | } 39 | 40 | if (api.hasPlugin('redux')) { 41 | api.addDevDependency('@types/react-redux') 42 | } 43 | 44 | api.copy(path.resolve('./template')) 45 | } 46 | -------------------------------------------------------------------------------- /src/plugins/typescript/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "jsx": "preserve", 7 | "lib": ["dom", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "preserveConstEnums": true, 15 | "removeComments": false, 16 | "skipLibCheck": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "target": "esnext" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/readmes/default.md: -------------------------------------------------------------------------------- 1 | ## Code quality 2 | 3 | ### Prettier 4 | 5 | ### ESLint 6 | 7 | ### Git hooks 8 | -------------------------------------------------------------------------------- /src/readmes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const getPath = name => path.resolve(__dirname, name) 6 | 7 | // order matters here 8 | module.exports = [ 9 | { file: getPath('./default.md') }, 10 | { 11 | plugin: 'styled-components', 12 | file: getPath('./styled-components.md'), 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /src/readmes/styled-components.md: -------------------------------------------------------------------------------- 1 | ### styled-components 2 | -------------------------------------------------------------------------------- /src/utils/banner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = () => 4 | console.log(` 5 | ███████╗████████╗██████╗ ██╗ ██╗ 6 | ██╔════╝╚══██╔══╝██╔══██╗██║ ██║ 7 | ███████╗ ██║ ██████╔╝██║ ██║ 8 | ╚════██║ ██║ ██╔══██╗╚██╗ ██╔╝ 9 | ███████║ ██║ ██║ ██║ ╚████╔╝ 10 | ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ 11 | `) 12 | -------------------------------------------------------------------------------- /src/utils/merge.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mergeWith = require('lodash.mergewith') 4 | const isArray = require('lodash.isarray') 5 | 6 | module.exports = (objA, objB) => 7 | mergeWith(objA, objB, (objValue, srcValue) => { 8 | if (isArray(objValue)) { 9 | return objValue.concat(srcValue) 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/utils/sort-object.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (obj, keyOrder) => { 4 | const res = {} 5 | 6 | if (keyOrder) { 7 | keyOrder.forEach(key => { 8 | res[key] = obj[key] 9 | delete obj[key] 10 | }) 11 | } 12 | 13 | const keys = Object.keys(obj) 14 | 15 | keys.sort() 16 | keys.forEach(key => { 17 | res[key] = obj[key] 18 | }) 19 | 20 | return res 21 | } 22 | -------------------------------------------------------------------------------- /templates/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@strv/javascript/environments/react/v16', 4 | 'prettier', 5 | 'prettier/react', 6 | ], 7 | env: { 8 | browser: true, 9 | commonjs: true, 10 | es6: true, 11 | jest: true, 12 | node: true, 13 | }, 14 | parser: 'babel-eslint', 15 | parserOptions: { 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | }, 22 | rules: { 23 | 'import/no-unresolved': 0, 24 | 'react/prop-types': 0, 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /templates/SPA/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "/", 3 | "scripts": { 4 | "start": "react-scripts start", 5 | "build": "react-scripts build", 6 | "analyze": "react-scripts analyze", 7 | "test": "react-scripts test --env=jsdom" 8 | }, 9 | "dependencies": { 10 | "@strv/react-scripts": "^2.0.0", 11 | "react": "^16.6.3", 12 | "react-dom": "^16.6.3", 13 | "react-router-dom": "^4.3.1", 14 | "sanitize.css": "^8.0.0" 15 | }, 16 | "browserslist": [ 17 | ">0.2%", 18 | "not dead", 19 | "not ie <= 11", 20 | "not op_mini all" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /templates/SPA/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/create-strv-app/1dd3b2aac37c12294946756e6b575d789d249b6a/templates/SPA/public/favicon.ico -------------------------------------------------------------------------------- /templates/SPA/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /templates/SPA/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /templates/SPA/readme.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ## Getting started 4 | 5 | ## Development 6 | 7 | ```bash 8 | npm start 9 | ``` 10 | 11 | or 12 | 13 | ```bash 14 | yarn start 15 | ``` 16 | 17 | ## Production 18 | 19 | ```bash 20 | npm build 21 | ``` 22 | 23 | or 24 | 25 | ```bash 26 | yarn build 27 | ``` 28 | 29 | > See `scripts` field inside package.json for available commands 30 | -------------------------------------------------------------------------------- /templates/SPA/src/config.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /templates/SPA/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { BrowserRouter as Router } from 'react-router-dom' 4 | import 'sanitize.css' 5 | 6 | import App from './views' 7 | import * as serviceWorker from './serviceWorker' 8 | 9 | const render = () => { 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ) 16 | } 17 | 18 | if (module.hot) { 19 | module.hot.accept('./views', render) 20 | } 21 | 22 | render() 23 | 24 | // If you want your app to work offline and load faster, you can change 25 | // unregister() to register() below. Note this comes with some pitfalls. 26 | // Learn more about service workers: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 27 | serviceWorker.unregister() 28 | -------------------------------------------------------------------------------- /templates/SPA/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ) 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config) 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ) 48 | }) 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing 63 | if (installingWorker == null) { 64 | return 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ) 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration) 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.') 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error) 98 | }) 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type') 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload() 115 | }) 116 | }) 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config) 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ) 126 | }) 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister() 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /templates/SPA/src/styles/colors.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /templates/SPA/src/styles/fonts.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /templates/SPA/src/views/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class App extends Component { 4 | render() { 5 | return ( 6 |
7 |

Hello, World!

8 |
9 | ) 10 | } 11 | } 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /templates/SSR/components/head.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NextHead from 'next/head' 3 | 4 | const Head = ({ 5 | title = 'STRV - Next.js template', 6 | description = '', 7 | ogUrl = '', 8 | ogImage = '', 9 | }) => ( 10 | 11 | 12 | {title} 13 | 14 | 15 | 16 | {/* */} 17 | {/* */} 18 | {/* */} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | 31 | export default Head 32 | -------------------------------------------------------------------------------- /templates/SSR/next.config.js: -------------------------------------------------------------------------------- 1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 2 | 3 | const { ANALYZE } = process.env 4 | 5 | module.exports = { 6 | webpack: (config, { isServer }) => { 7 | if (ANALYZE) { 8 | config.plugins.push( 9 | new BundleAnalyzerPlugin({ 10 | analyzerMode: 'server', 11 | analyzerPort: isServer ? 8888 : 8889, 12 | openAnalyzer: true, 13 | }) 14 | ) 15 | } 16 | 17 | return config 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /templates/SSR/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "next", 4 | "build": "next build", 5 | "start": "next start", 6 | "analyze": "ANALYZE=1 next build" 7 | }, 8 | "dependencies": { 9 | "next": "^7.0.2", 10 | "react": "^16.6.3", 11 | "react-dom": "^16.6.3" 12 | }, 13 | "devDependencies": { 14 | "webpack-bundle-analyzer": "^3.0.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/SSR/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from '../components/head' 3 | 4 | export default () => ( 5 |
6 | 7 | Hello, world! 8 |
9 | ) 10 | -------------------------------------------------------------------------------- /templates/SSR/readme.md: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /templates/SSR/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/create-strv-app/1dd3b2aac37c12294946756e6b575d789d249b6a/templates/SSR/static/favicon.ico -------------------------------------------------------------------------------- /thumbnail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/create-strv-app/1dd3b2aac37c12294946756e6b575d789d249b6a/thumbnail.gif --------------------------------------------------------------------------------