-
-
Notifications
You must be signed in to change notification settings - Fork 372
Remote Templates #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remote Templates #329
Changes from 47 commits
71a6b6c
d4ff638
6c3c3aa
b7f4cdc
427b22e
8ab7688
6b878d4
86a4f3b
1ee0dfc
5e4fdb0
a23765e
172ff54
f04532f
4dc79c0
8daf606
eccca4f
15f1b6b
45b28c2
ce8d316
60e4830
34379f5
8dc0583
da46a8b
1e18c1a
c443554
39091a7
8af1c30
dfec01b
c3430c7
54b93ae
2f71fd3
a18f968
2fd9522
6e1509f
e9463b8
92fec7a
6d0493b
32acd00
1fb1133
771f43c
1a994b7
d7f748a
ca3b235
85dfe85
fb8f273
b3380e0
f8f7ec4
1880a38
bc7af44
0ed79d1
faa8cad
741774e
7c1ad41
e7ee7f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,68 +1,40 @@ | ||
| import asyncCommand from '../lib/async-command'; | ||
| import fs from 'fs.promised'; | ||
| import copy from 'recursive-copy'; | ||
| import glob from 'glob'; | ||
| import mkdirp from 'mkdirp'; | ||
| import { resolve } from 'path'; | ||
| import ora from 'ora'; | ||
| import chalk from 'chalk'; | ||
| import inquirer from 'inquirer'; | ||
| import path from 'path'; | ||
| import { install, initialize, pkgScripts, initGit, trimLeft } from './../lib/setup'; | ||
| import glob from 'glob'; | ||
| import gittar from 'gittar'; | ||
| import { green } from 'chalk'; | ||
| import { prompt } from 'inquirer'; | ||
| import asyncCommand from '../lib/async-command'; | ||
| import { install, initGit, addScripts } from './../lib/setup'; | ||
| import { isDir, hasCommand, error, trim, warn } from '../util'; | ||
|
|
||
| const TEMPLATES = { | ||
| full: 'examples/full', | ||
| empty: 'examples/empty', | ||
| root: 'examples/root', | ||
| simple: 'examples/simple' | ||
| full: 'preactjs-templates/default', | ||
| default: 'preactjs-templates/default', | ||
| // simple: 'examples/simple', | ||
| // empty: 'examples/empty', | ||
| // root: 'examples/root', | ||
| }; | ||
|
|
||
| export default asyncCommand({ | ||
| command: 'create <name> [dest]', | ||
| command: 'create <template> <dest>', | ||
|
|
||
| desc: 'Create a new application.', | ||
|
|
||
| builder: { | ||
| name: { | ||
| description: 'directory and package name for the new app' | ||
| }, | ||
| dest: { | ||
| description: 'Directory to create the app within', | ||
| defaultDescription: '<name>' | ||
| description: 'The application\'s name' | ||
| }, | ||
| force: { | ||
| description: 'Force option to create the directory for the new app', | ||
| default: false | ||
| }, | ||
| type: { | ||
| description: 'A project template to start from', | ||
| choices: [ | ||
| 'full', | ||
| 'root', | ||
| 'simple', | ||
| 'empty' | ||
| ], | ||
| default: 'full' | ||
| }, | ||
| yarn: { | ||
| description: "Use 'yarn' instead of 'npm'", | ||
| type: 'boolean', | ||
| default: false | ||
| }, | ||
| less: { | ||
| description: 'Pre-install LESS support', | ||
| type: 'boolean', | ||
| default: false | ||
| }, | ||
| sass: { | ||
| description: 'Pre-install SASS/SCSS support', | ||
| type: 'boolean', | ||
| default: false | ||
| }, | ||
| stylus: { | ||
| description: 'Pre-install STYLUS support', | ||
| type: 'boolean', | ||
| default: false | ||
| }, | ||
| git: { | ||
| description: 'Initialize version control using git', | ||
| type: 'boolean', | ||
|
|
@@ -76,153 +48,121 @@ export default asyncCommand({ | |
| }, | ||
|
|
||
| async handler(argv) { | ||
| let template = TEMPLATES[argv.type]; | ||
|
|
||
| if (!template) { | ||
| throw Error(`Unknown app template "${argv.type}".`); | ||
| } | ||
|
|
||
| let target = path.resolve(process.cwd(), argv.dest || argv.name); | ||
| let isYarn = argv.yarn && hasCommand('yarn'); | ||
| let cwd = argv.cwd ? resolve(argv.cwd) : process.cwd(); | ||
| let target = argv.dest && resolve(cwd, argv.dest); | ||
| let exists = target && isDir(target); | ||
|
|
||
| if (target) { | ||
| if (exists && !argv.force) { | ||
| return error('Refusing to overwrite current directory! Please specify a different destination or use the `--force` flag', 1); | ||
| } | ||
|
|
||
| let exists = false; | ||
| try { | ||
| exists = (await fs.stat(target)).isDirectory(); | ||
| } | ||
| catch (err) {} | ||
|
|
||
| if (exists && argv.force) { | ||
| const question = { | ||
| type: 'confirm', | ||
| name: 'enableForce', | ||
| message: `You are using '--force'. Do you wish to continue?`, | ||
| default: false, | ||
| }; | ||
|
|
||
| let { enableForce } = await inquirer.prompt(question); | ||
|
|
||
| if (enableForce) { | ||
| process.stdout.write('Initializing project in the current directory...\n'); | ||
| } else { | ||
| process.stderr.write(chalk.red('Error: Cannot initialize in the current directory\n')); | ||
| process.exit(1); | ||
| if (exists && argv.force) { | ||
| let { enableForce } = await prompt({ | ||
| type: 'confirm', | ||
| name: 'enableForce', | ||
| message: `You are using '--force'. Do you wish to continue?`, | ||
| default: false | ||
| }); | ||
|
|
||
| if (enableForce) { | ||
| process.stdout.write('Initializing project in the current directory...\n'); | ||
| } else { | ||
| return error('Refusing to overwrite current directory!', 1); | ||
| } | ||
| } | ||
| } else { | ||
| // TODO: interactive | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @developit Should we really do this? This make it more complex for us to handle.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's save this discussion for a future PR. I want to break it up instead of making this (even more) massive. |
||
| } | ||
|
|
||
| if (exists && !argv.force) { | ||
| process.stderr.write(chalk.red('Error: Cannot initialize in the current directory, please specify a different destination or use --force\n')); | ||
| process.exit(1); | ||
| } | ||
| let repo = TEMPLATES[argv.template] || argv.template; | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be reasonable here to do something like: "if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. Or just include new official templates (should not be often!) as patch releases. |
||
| // Attempt to fetch the `template` | ||
| let archive = await gittar.fetch(repo).catch(err => { | ||
| err = err || { message:'An error occured while fetching template.' }; | ||
| return error(err.code === 404 ? `Could not find repostory: ${repo}` : err.message, 1); | ||
| }); | ||
|
|
||
| let spinner = ora({ | ||
| text: 'Creating project', | ||
| color: 'magenta' | ||
| }).start(); | ||
|
|
||
| if (!exists) { | ||
| await Promise.promisify(mkdirp)(target); | ||
| } | ||
|
|
||
| await copy( | ||
| path.resolve(__dirname, '../..', template), | ||
| target, | ||
| { filter: ['**/*', '!build'] } | ||
| ); | ||
|
|
||
| spinner.text = 'Initializing project'; | ||
|
|
||
| await initialize(argv.yarn, target); | ||
| // Extract files from `archive` to `target` | ||
| await gittar.extract(archive, target, { | ||
| strip: 2, | ||
| filter(path) { | ||
| // TODO: remove `/build/` ?? | ||
| // TODO: read & respond to meta/hooks | ||
| return path.includes('/template/') && !path.includes('/build/'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't find
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Let's do that in another PR. return error(`No \`template\` directory found within ${ repo }!`);
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could even detect
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to force a template structure. When things become too flexible they become less clear and harder to debug. |
||
| } | ||
| }); | ||
|
|
||
| let pkg = JSON.parse(await fs.readFile(path.resolve(target, 'package.json'))); | ||
| spinner.text = 'Parsing `package.json` file'; | ||
|
|
||
| pkg.scripts = await pkgScripts(argv.yarn, pkg); | ||
| // Validate user's `package.json` file | ||
| let pkgData; | ||
| let pkgFile = resolve(target, 'package.json'); | ||
|
|
||
| try { | ||
| await fs.stat(path.resolve(target, 'src')); | ||
| } | ||
| catch (err) { | ||
| pkg.scripts.test = pkg.scripts.test.replace('src', '.'); | ||
| if (pkgFile) { | ||
| pkgData = JSON.parse(await fs.readFile(pkgFile)); | ||
| // Write default "scripts" if none found | ||
| pkgData.scripts = pkgData.scripts || (await addScripts(pkgData, target, isYarn)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe instead of making pkgData.scripts = {
...(pkgData.scripts || {}),
...(await addScripts(pkgData, target, isYarn))
};
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think there is a necessary for that since templates will have scripts defined in the package.json file (includes our scripts too)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just sucks if the cli wants to change script down the line 😋 |
||
| } else { | ||
| warn('Could not locate `package.json` file!'); | ||
| } | ||
|
|
||
| pkg.eslintConfig = { | ||
| extends: 'eslint-config-synacor' | ||
| }; | ||
| if (argv.name) { | ||
| spinner.text = 'Updating `name` within `package.json` file'; | ||
| // Update `package.json` key | ||
| pkgData && (pkgData.name = argv.name); | ||
| // Find a `manifest.json`; use the first match, if any | ||
| let files = await Promise.promisify(glob)(target + '/**/manifest.json'); | ||
| let manifest = files[0] && JSON.parse(await fs.readFile(files[0])); | ||
| if (manifest) { | ||
| spinner.text = 'Updating `name` within `manifest.json` file'; | ||
| manifest.name = manifest.short_name = argv.name; | ||
| // Write changes to `manifest.json` | ||
| await fs.writeFile(files[0], JSON.stringify(manifest, null, 2)); | ||
| if (argv.name.length > 12) { | ||
| // @see https://developer.chrome.com/extensions/manifest/name#short_name | ||
| process.stdout.write('\n'); | ||
| warn('Your `short_name` should be fewer than 12 characters.'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| await fs.writeFile(path.resolve(target, 'package.json'), JSON.stringify(pkg, null, 2)); | ||
| if (pkgData) { | ||
| // Assume changes were made ¯\_(ツ)_/¯ | ||
| await fs.writeFile(pkgFile, JSON.stringify(pkgData, null, 2)); | ||
| } | ||
|
|
||
| if (argv.install) { | ||
| spinner.text = 'Installing dev dependencies'; | ||
|
|
||
| await install(argv.yarn, target, [ | ||
| 'preact-cli', | ||
| 'if-env', | ||
| 'eslint', | ||
| 'eslint-config-synacor', | ||
|
|
||
| // install sass setup if --sass | ||
| ...(argv.sass ? [ | ||
| 'node-sass', | ||
| 'sass-loader' | ||
| ] : []), | ||
|
|
||
| // install less setup if --less | ||
| ...(argv.less ? [ | ||
| 'less', | ||
| 'less-loader' | ||
| ] : []), | ||
|
|
||
| // install stylus if --stylus | ||
| ...(argv.stylus ? [ | ||
| 'stylus', | ||
| 'stylus-loader' | ||
| ] : []) | ||
| ], 'dev'); | ||
|
|
||
| spinner.text = 'Installing dependencies'; | ||
|
|
||
| await install(argv.yarn, target, [ | ||
| 'preact', | ||
| 'preact-compat', | ||
| 'preact-router' | ||
| ]); | ||
| await install(target, isYarn); | ||
| } | ||
|
|
||
| spinner.succeed('Done!\n'); | ||
|
|
||
| if (argv.less || argv.sass || argv.stylus) { | ||
| let extension; | ||
|
|
||
| if (argv.less) extension = '.less'; | ||
| if (argv.sass) extension = '.scss'; | ||
| if (argv.stylus) extension = '.styl'; | ||
|
|
||
| const cssFiles = await Promise.promisify(glob)(`${target}/**/*.css`, { | ||
| ignore: [ | ||
| `${target}/build/**`, | ||
| `${target}/node_modules/**` | ||
| ] | ||
| }); | ||
|
|
||
| const changeExtension = fileName => fs.rename(fileName, fileName.replace(/.css$/, extension)); | ||
|
|
||
| await Promise.all(cssFiles.map(changeExtension)); | ||
| } | ||
|
|
||
| if (argv.git) { | ||
| await initGit(target); | ||
| } | ||
|
|
||
| return trimLeft(` | ||
| let pfx = isYarn ? 'yarn' : 'npm run'; | ||
|
|
||
| return trim(` | ||
| To get started, cd into the new directory: | ||
| \u001b[32mcd ${path.relative(process.cwd(), target)}\u001b[39m | ||
| ${ green('cd ' + argv.dest) } | ||
|
|
||
| To start a development live-reload server: | ||
| \u001b[32m${argv.yarn === true ? 'yarn start' : 'npm start'}\u001b[39m | ||
| ${ green(pfx + ' start') } | ||
|
|
||
| To create a production build (in ./build): | ||
| \u001b[32m${argv.yarn === true ? 'yarn build' : 'npm run build'}\u001b[39m | ||
| ${ green(pfx + ' build') } | ||
|
|
||
| To start a production HTTP/2 server: | ||
| \u001b[32m${argv.yarn === true ? 'yarn serve' : 'npm run serve'}\u001b[39m | ||
| ${ green(pfx + ' serve') } | ||
| `) + '\n'; | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why both
full&default? Since they do the same thing.Instead we can just add
default&simplefrom templates.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add
simple. @developit requested in Slack that we make default an alias offullThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was just thinking "principle of least surprise" here, this makes it work quite similarly to how it does in 1.0.