Skip to content
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

[cli] Allow bootstrapping plugins from templates #355

Merged
merged 5 commits into from
Nov 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@sanity/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"chalk": "^1.1.3",
"configstore": "^3.0.0",
"debug": "^2.6.3",
"decompress": "^4.2.0",
"deep-sort-object": "^1.0.1",
"execa": "^0.6.0",
"fs-extra": "^4.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const path = require('path')
const semver = require('semver')
const fse = require('fs-extra')
const simpleGet = require('simple-get')
const decompress = require('decompress')
const resolveFrom = require('resolve-from')
const validateNpmPackageName = require('validate-npm-package-name')
const {pathTools} = require('@sanity/util')
const pkg = require('../../../package.json')
const {absolutify, pathIsEmpty} = pathTools

module.exports = async (context, url) => {
const {prompt, workDir} = context
let inProjectContext = false
try {
const projectManifest = await fse.readJson(path.join(workDir, 'sanity.json'))
inProjectContext = Boolean(projectManifest.root)
} catch (err) {
// Intentional noop
}

let zip
try {
zip = await getZip(url)
} catch (err) {
err.message = `Failed to get template: ${err.message}`
throw err
}

const manifest = zip.find(file => path.basename(file.path) === 'package.json')
const baseDir = path.join(path.dirname(manifest.path), 'template')
const templateFiles = zip.filter(file => file.type === 'file' && file.path.indexOf(baseDir) === 0)
const manifestContent = manifest.data.toString()
const tplVars = parseJson(manifestContent).sanityTemplate || {}
const {minimumBaseVersion} = tplVars

if (minimumBaseVersion) {
const installed = getSanityVersion(workDir)
if (semver.lt(installed, minimumBaseVersion)) {
throw new Error(
`Template requires Sanity at version ${minimumBaseVersion}, installed is ${installed}`
)
}
}

const name = await prompt.single({
type: 'input',
message: 'Plugin name:',
default: tplVars.suggestedName || '',
validate: async pkgName => {
const {validForNewPackages, errors} = validateNpmPackageName(pkgName)
if (!validForNewPackages) {
return errors[0]
}

const outputPath = path.join(workDir, 'plugins', pkgName)
const isEmpty = await pathIsEmpty(outputPath)
if (inProjectContext && !isEmpty) {
return 'Plugin with given name already exists in project'
}

return true
}
})

let outputPath = path.join(workDir, 'plugins', name)
if (!inProjectContext) {
outputPath = await prompt.single({
type: 'input',
message: 'Output path:',
default: workDir,
validate: validateEmptyPath,
filter: absolutify
})
}

let createConfig = tplVars.requiresConfig
if (typeof createConfig === 'undefined') {
createConfig = await prompt.single({
type: 'confirm',
message: 'Does the plugin need a configuration file?',
default: false
})
}

await fse.ensureDir(outputPath)
await Promise.all(
templateFiles.map(file => {
const filename = file.path.slice(baseDir.length)
return fse.outputFile(path.join(outputPath, filename), file.data)
})
)

return {name, outputPath, inPluginsPath: inProjectContext}
}

async function validateEmptyPath(dir) {
const isEmpty = await pathIsEmpty(dir)
return isEmpty ? true : 'Path is not empty'
}

function getZip(url) {
return new Promise((resolve, reject) => {
simpleGet.concat(url, (err, res, data) => {
if (err) {
reject(err)
return
}

resolve(decompress(data))
})
})
}

function parseJson(json) {
try {
return JSON.parse(json)
} catch (err) {
return {}
}
}

function getSanityVersion(workDir) {
const basePkg = resolveFrom.silent(workDir, '@sanity/base/package.json')
return basePkg ? require(basePkg).version : pkg.version
}
57 changes: 33 additions & 24 deletions packages/@sanity/cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function runCli() {

const devMode = hasDevMode()
const args = parseArguments()
const isInit = args.groupOrCommand === 'init'
const isInit = args.groupOrCommand === 'init' && args.argsWithoutOptions[0] !== 'plugin'
const cwd = checkCwdPresence()
const workDir = isInit ? process.cwd() : resolveRootDir(cwd)
const options = {
Expand All @@ -29,11 +29,13 @@ module.exports = function runCli() {
}

if (sanityEnv !== 'production') {
console.warn(chalk.yellow(
knownEnvs.includes(sanityEnv)
? `[WARN] Running in ${sanityEnv} environment mode\n`
: `[WARN] Running in ${chalk.red('UNKNOWN')} "${sanityEnv}" environment mode\n`
))
console.warn(
chalk.yellow(
knownEnvs.includes(sanityEnv)
? `[WARN] Running in ${sanityEnv} environment mode\n`
: `[WARN] Running in ${chalk.red('UNKNOWN')} "${sanityEnv}" environment mode\n`
)
)
}

if (!isInit && workDir !== cwd) {
Expand Down Expand Up @@ -65,15 +67,14 @@ module.exports = function runCli() {
args.groupOrCommand = 'help'
}

Promise.resolve(getCliRunner(commands))
.then(cliRunner => cliRunner.runCommand(args.groupOrCommand, args, options))
.catch(err => {
const debug = core.d || core.debug
const error = (debug && err.details) || err
const errMessage = debug ? (error.stack || error) : (error.message || error)
console.error(chalk.red(errMessage)) // eslint-disable-line no-console
process.exit(1) // eslint-disable-line no-process-exit
})
const cliRunner = getCliRunner(commands)
cliRunner.runCommand(args.groupOrCommand, args, options).catch(err => {
const debug = core.d || core.debug
const error = (debug && err.details) || err
const errMessage = debug ? error.stack || error : error.message || error
console.error(chalk.red(errMessage)) // eslint-disable-line no-console
process.exit(1) // eslint-disable-line no-process-exit
})
}

// Weird edge case where the folder the terminal is currently in has been removed
Expand All @@ -100,12 +101,16 @@ function hasDevMode() {
function resolveRootDir(cwd) {
// Resolve project root directory
try {
return resolveProjectRoot({
basePath: cwd,
sync: true
}) || cwd
return (
resolveProjectRoot({
basePath: cwd,
sync: true
}) || cwd
)
} catch (err) {
console.warn(chalk.red(['Error occured trying to resolve project root:', err.message].join('\n')))
console.warn(
chalk.red(['Error occured trying to resolve project root:', err.message].join('\n'))
)
process.exit(1)
}

Expand All @@ -120,10 +125,14 @@ function getCoreModulePath(workDir) {

const hasManifest = fse.existsSync(path.join(workDir, 'sanity.json'))
if (hasManifest && process.argv.indexOf('install') === -1) {
console.warn(chalk.yellow([
'@sanity/core not installed in current project',
'Project-specific commands not available until you run `sanity install`'
].join('\n')))
console.warn(
chalk.yellow(
[
'@sanity/core not installed in current project',
'Project-specific commands not available until you run `sanity install`'
].join('\n')
)
)
}

return undefined
Expand Down
54 changes: 11 additions & 43 deletions packages/@sanity/cli/src/commands/init/bootstrapPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import path from 'path'
import fse from 'fs-extra'
import {partialRight} from 'lodash'
import promiseProps from 'promise-props-recursive'
import {
createSanityManifest,
createPluginManifest
} from './createManifest'
import {createSanityManifest, createPluginManifest} from './createManifest'

export default function bootstrapPlugin(data, opts = {}) {
const writeIfNotExists = partialRight(writeFileIfNotExists, opts.output)
Expand All @@ -16,21 +13,15 @@ export default function bootstrapPlugin(data, opts = {}) {
readme: `# ${data.name}\n\n${data.description}\n`,
sanity: createSanityManifest(data, {
isPlugin: true,
isSanityStyle: opts.sanityStyle,
serialize: true
})
}

const targetPath = data.outputPath
const styleMetaFiles = ['babelrc', 'editorconfig', 'eslintignore', 'eslintrc', 'npmignore', 'gitignore']

if (opts.sanityStyle) {
styleMetaFiles.forEach(file => {
collect[file] = readTemplate(path.join('sanity-style', file))
})
}

return fse.ensureDir(targetPath).then(() => promiseProps(collect))
return fse
.ensureDir(targetPath)
.then(() => promiseProps(collect))
.then(templates => {
if (!data.createConfig) {
return templates
Expand All @@ -47,43 +38,20 @@ export default function bootstrapPlugin(data, opts = {}) {
writeIfNotExists(path.join(targetPath, 'README.md'), templates.readme)
]

if (opts.sanityStyle) {
styleMetaFiles.forEach(file =>
writeOps.push(writeIfNotExists(
path.join(targetPath, `.${file}`),
templates[file]
)))
}

return Promise.all(writeOps)
})
.then(() => {
if (!opts.sanityStyle) {
return
}

fse.ensureDir(path.join(targetPath, 'src')).then(() =>
writeIfNotExists(
path.join(targetPath, 'src', 'MyComponent.js'),
"import React from 'react'\n\n"
+ 'export default function MyComponent() {\n'
+ ' return <div />\n'
+ '}\n'
))
})
}

function readTemplate(file) {
return fse.readFile(path.join(__dirname, 'templates', file))
}

function writeFileIfNotExists(filePath, content, output) {
return fse.writeFile(filePath, content, {flag: 'wx'})
.catch(err => {
if (err.code === 'EEXIST') {
output.print(`[WARN] File "${filePath}" already exists, skipping`)
} else {
throw err
}
})
return fse.writeFile(filePath, content, {flag: 'wx'}).catch(err => {
if (err.code === 'EEXIST') {
output.print(`[WARN] File "${filePath}" already exists, skipping`)
} else {
throw err
}
})
}
Loading