Skip to content

Commit

Permalink
[cli] Allow bootstrapping plugins from templates (#355)
Browse files Browse the repository at this point in the history
* [cli] Remove sanity-style plugin init code

* [cli] Allow bootstrapping plugins from templates

* [cli] Add minimum version check to plugin template bootstrap

* [cli] Add tool templates

* [cli] Don't allow bootstrapping to existing plugin folder
  • Loading branch information
rexxars authored and bjoerge committed Nov 13, 2017
1 parent 3e7351b commit e9a62be
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 202 deletions.
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
126 changes: 126 additions & 0 deletions packages/@sanity/cli/src/actions/init-plugin/bootstrapFromTemplate.js
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
}
})
}

0 comments on commit e9a62be

Please sign in to comment.