diff --git a/lib/cli.js b/lib/cli.js index 2b4be67..7969295 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -60,6 +60,15 @@ module.exports = function(argv, console, done) { .then(() => quit(null, 'OK'), quit) }) + program + .command('eject [prefix] [filename]') + .description("Extract part of thought's default templates into the local configuration directory") + .action(function(prefix, filename) { + changeDir() + .then(() => require('../lib/eject.js')(prefix, filename)) + .then(() => quit(null, 'OK'), quit) + }) + program .command('up-to-date') .description( diff --git a/lib/eject.js b/lib/eject.js new file mode 100644 index 0000000..6c72861 --- /dev/null +++ b/lib/eject.js @@ -0,0 +1,82 @@ +/* eslint-disable no-console */ + +const customize = require('customize') +const debug = require('debug')('though:eject') +const fs = require('fs-extra') +const path = require('path') + +module.exports = async function eject(optionalPrefix, filename) { + if (filename == null) { + return logEjectableFiles(optionalPrefix) + } + + const config = await customize() + // Load `customize`-spec + .load(require('../customize')('.')) + .buildConfig() + + switch (optionalPrefix) { + case 'template': + return ejectFile({ + source: config.handlebars.templates[filename], + targetFile: path.join('.thought', 'templates', filename), + notFoundMessage: `There is no template "${filename}"!` + }) + case 'partial': + return ejectFile({ + source: config.handlebars.partials[filename], + targetFile: path.join('.thought', 'partials', filename), + notFoundMessage: `There is no partial "${filename}"!` + }) + } +} + +async function logEjectableFiles(optionalPrefix) { + const config = await customize() + // Load `customize`-spec + .load(require('../customize')('.')) + .buildConfig() + + debug(config) + + console.log('I can eject the following files for you: ') + switch (optionalPrefix) { + case undefined: + case null: + logDefaultFiles(config.handlebars.templates, { prefix: 'template' }) + logDefaultFiles(config.handlebars.partials, { prefix: 'partial' }) + break + case 'template': + logDefaultFiles(config.handlebars.templates, { prefix: 'template' }) + break + case 'partial': + logDefaultFiles(config.handlebars.partials, { prefix: 'partial' }) + break + default: + throw new Error(`Unknown prefix "${optionalPrefix}", try without prefix!"`) + } +} + +function logDefaultFiles(fileObject, { prefix }) { + Object.entries(fileObject).forEach(([fileName, file]) => { + if (!isOverridden(file)) { + console.log(` ${prefix} ${fileName}`) + } + }) +} + +async function ejectFile({ source, targetFile, notFoundMessage }) { + if (source == null) { + throw new Error(notFoundMessage) + } + if (isOverridden(source)) { + throw new Error(`File "${source.path}" already exists in this project!`) + } + + console.log(`Ejecting "${targetFile}"`) + await fs.copy(source.path, targetFile) +} + +function isOverridden(file) { + return file.path.match(/^\.thought/) +} diff --git a/test/cli-spec.js b/test/cli-spec.js index aba839a..1243001 100644 --- a/test/cli-spec.js +++ b/test/cli-spec.js @@ -6,6 +6,9 @@ const expect = chai.expect const exec = require('../lib/utils/exeq') const cp = require('child_process') +const fs = require('fs-extra') +const path = require('path') + describe('The cli script', function() { this.timeout(30000) const scenario = new Scenario('simple-project') @@ -84,6 +87,110 @@ describe('The cli script', function() { }) ) }) + + describe('the "eject" command', () => { + const ejectScenario = new Scenario('with-partial-and-template') + + it('should show a list of ejectable files if no parameter is provided', async function() { + await ejectScenario.prepareAndRun(() => + runMockThought('eject').then(result => { + expect(result.code).to.equal(0) + expect(result.stdout).to.match(/partial howitworks\.md\.hbs/) + expect(result.stdout).to.match(/template README\.md\.hbs/) + expect(result.stdout).not.to.match(/usage\.md\.hbs/) + + expect(result.stderr).to.equal('') + }) + ) + }) + + it('should show only ejectable templates, if the prefix is "template"', async function() { + await ejectScenario.prepareAndRun(() => + runMockThought('eject', 'template').then(result => { + expect(result.code).to.equal(0) + expect(result.stdout).not.to.match(/partial howitworks\.md\.hbs/) + expect(result.stdout).to.match(/template README\.md\.hbs/) + expect(result.stderr).to.equal('') + }) + ) + }) + + it('should show only ejectable partials, if the prefix is "partials"', async function() { + await ejectScenario.prepareAndRun(() => + runMockThought('eject', 'partial').then(result => { + expect(result.code).to.equal(0) + expect(result.stdout).to.match(/partial howitworks\.md\.hbs/) + expect(result.stdout).not.to.match(/template README\.md\.hbs/) + expect(result.stderr).to.equal('') + }) + ) + }) + + it('should eject a partial if a filename is specified and the prefix is "partial"', async function() { + await ejectScenario.prepareAndRun(async () => { + const result = await runMockThought('eject', 'partial', 'howitworks.md.hbs') + + expect(result.code).to.equal(0) + expect(result.stdout).to.match(/Ejecting "\.thought.partials.howitworks\.md\.hbs"/) + + const ejectedPath = path.join('.thought', 'partials', 'howitworks.md.hbs') + const ejectedFileContents = await ejectScenario.readActual(ejectedPath) + + const defaultPath = require.resolve('../handlebars/partials/howitworks.md.hbs') + const defaultFileContents = await fs.readFile(defaultPath, 'utf-8') + + expect(ejectedFileContents).to.equal(defaultFileContents) + }) + }) + + it('should eject a template if a filename is specified and the prefix is "template"', async function() { + await ejectScenario.prepareAndRun(async () => { + const result = await runMockThought('eject', 'template', 'README.md.hbs') + + expect(result.code).to.equal(0) + expect(result.stdout).to.match(/Ejecting "\.thought.templates.README\.md\.hbs"/) + expect(result.stderr).to.equal('') + + const ejectedPath = path.join('.thought', 'templates', 'README.md.hbs') + const ejectedFileContents = await ejectScenario.readActual(ejectedPath) + + const defaultPath = require.resolve('../handlebars/templates/README.md.hbs') + const defaultFileContents = await fs.readFile(defaultPath, 'utf-8') + + expect(ejectedFileContents).to.equal(defaultFileContents) + }) + }) + + it('should give sensible error messages', async function() { + await ejectScenario.prepareAndRun(async () => { + await expectError({ args: ['eject', 'temp'], errorMessage: 'Unknown prefix "temp", try without prefix!' }) + + await expectError({ + args: ['eject', 'template', 'RED.md.hbs'], + errorMessage: 'There is no template "RED.md.hbs"!' + }) + await expectError({ + args: ['eject', 'template', 'anotherFile.md.hbs'], + errorMessage: `File ".thought/templates/anotherFile.md.hbs" already exists in this project!` + }) + + await expectError({ + args: ['eject', 'partial', 'RED.md.hbs'], + errorMessage: 'There is no partial "RED.md.hbs"!' + }) + await expectError({ + args: ['eject', 'partial', 'usage.md.hbs'], + errorMessage: `File ".thought/partials/usage.md.hbs" already exists in this project!` + }) + }) + }) + + async function expectError({ args, errorMessage }) { + const result = await runMockThought(...args) + expect(result.stderr).to.contain(errorMessage) + expect(result.code).to.equal(1) + } + }) }) /** diff --git a/test/fixtures/scenarios/with-partial-and-template/expected/.thought/partials/usage.md.hbs b/test/fixtures/scenarios/with-partial-and-template/expected/.thought/partials/usage.md.hbs new file mode 100644 index 0000000..f78e99a --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/expected/.thought/partials/usage.md.hbs @@ -0,0 +1 @@ +Overridden usage file \ No newline at end of file diff --git a/test/fixtures/scenarios/with-partial-and-template/expected/.thought/templates/anotherFile.md.hbs b/test/fixtures/scenarios/with-partial-and-template/expected/.thought/templates/anotherFile.md.hbs new file mode 100644 index 0000000..34be88a --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/expected/.thought/templates/anotherFile.md.hbs @@ -0,0 +1 @@ +this is another file \ No newline at end of file diff --git a/test/fixtures/scenarios/with-partial-and-template/expected/README.md b/test/fixtures/scenarios/with-partial-and-template/expected/README.md new file mode 100644 index 0000000..2167ea5 --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/expected/README.md @@ -0,0 +1,27 @@ +# simple-project + +[![NPM version](https://img.shields.io/npm/v/simple-project.svg)](https://npmjs.com/package/simple-project) + +> A simple description + + +# Installation + +``` +npm install simple-project +``` + +Overridden usage file + + +# License + +`simple-project` is published under the ISC-license. + +No file "LICENSE*" found + + + +# Contributing guidelines + +See [CONTRIBUTING.md](CONTRIBUTING.md). \ No newline at end of file diff --git a/test/fixtures/scenarios/with-partial-and-template/expected/index.js b/test/fixtures/scenarios/with-partial-and-template/expected/index.js new file mode 100644 index 0000000..7d3c40d --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/expected/index.js @@ -0,0 +1,7 @@ +/** + * Beschreibung + * @param {string=} param + */ +function abc(param) { + +} diff --git a/test/fixtures/scenarios/with-partial-and-template/expected/package.json b/test/fixtures/scenarios/with-partial-and-template/expected/package.json new file mode 100644 index 0000000..9bd48bb --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/expected/package.json @@ -0,0 +1,18 @@ +{ + "name": "eject-project", + "version": "1.0.0", + "description": "A simple description", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/unit-test/eject-project.git" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "fs-walker": "^1.0.0" + } +} diff --git a/test/fixtures/scenarios/with-partial-and-template/input/.thought/partials/usage.md.hbs b/test/fixtures/scenarios/with-partial-and-template/input/.thought/partials/usage.md.hbs new file mode 100644 index 0000000..f78e99a --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/input/.thought/partials/usage.md.hbs @@ -0,0 +1 @@ +Overridden usage file \ No newline at end of file diff --git a/test/fixtures/scenarios/with-partial-and-template/input/.thought/templates/anotherFile.md.hbs b/test/fixtures/scenarios/with-partial-and-template/input/.thought/templates/anotherFile.md.hbs new file mode 100644 index 0000000..34be88a --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/input/.thought/templates/anotherFile.md.hbs @@ -0,0 +1 @@ +this is another file \ No newline at end of file diff --git a/test/fixtures/scenarios/with-partial-and-template/input/index.js b/test/fixtures/scenarios/with-partial-and-template/input/index.js new file mode 100644 index 0000000..7d3c40d --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/input/index.js @@ -0,0 +1,7 @@ +/** + * Beschreibung + * @param {string=} param + */ +function abc(param) { + +} diff --git a/test/fixtures/scenarios/with-partial-and-template/input/package.json b/test/fixtures/scenarios/with-partial-and-template/input/package.json new file mode 100644 index 0000000..9bd48bb --- /dev/null +++ b/test/fixtures/scenarios/with-partial-and-template/input/package.json @@ -0,0 +1,18 @@ +{ + "name": "eject-project", + "version": "1.0.0", + "description": "A simple description", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/unit-test/eject-project.git" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "fs-walker": "^1.0.0" + } +}