diff --git a/.gitignore b/.gitignore index 01ce33f..7b796bd 100644 --- a/.gitignore +++ b/.gitignore @@ -152,4 +152,6 @@ tmp/ temp/ # End of https://www.gitignore.io/api/node,macos,linux -n \ No newline at end of file + +# Editor +.vscode diff --git a/lib/cli.js b/lib/cli.js index be3ec53..428889a 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,4 +1,4 @@ -import { add, clone, newBluprint, remove, start, token } from 'bluprint'; +import { add, clone, newBluprint, remove, start, token, test } from 'bluprint'; import { name, version } from '../package.json'; import chalk from 'chalk'; @@ -66,4 +66,13 @@ yargs // eslint-disable-line no-unused-expressions }, async function({ accessToken }) { await token(accessToken); }) + .command('test [directory]', 'Test the bluprint in the given directory', (yargs) => { + yargs + .positional('directory', { + describe: 'Local path to a bluprint', + type: 'string', + }); + }, async function({ directory }) { + await test(directory); + }) .argv; diff --git a/lib/commands/start/fetchBluprint/parser.js b/lib/commands/start/fetchBluprint/parser.js index 291486d..2bae65d 100644 --- a/lib/commands/start/fetchBluprint/parser.js +++ b/lib/commands/start/fetchBluprint/parser.js @@ -40,9 +40,11 @@ export default (resolve, reject, filterGlobs, mergeJson) => { if (entry.type === 'Directory') { fs.mkdirSync(entryPath, { recursive: true }); entry.resume(); - } else if (entry.type === 'File') { + } else if (['File', 'SymbolicLink'].includes(entry.type)) { archive[entryPath] = []; entry.on('data', c => archive[entryPath].push(c)); + } else { + throw new Error(`unable to handle entry of type ${entry.type}`); } }, }) diff --git a/lib/commands/test/index.js b/lib/commands/test/index.js new file mode 100644 index 0000000..63a3014 --- /dev/null +++ b/lib/commands/test/index.js @@ -0,0 +1,93 @@ +import fs from 'fs'; +import path from 'path'; +import tar from 'tar'; +import os from 'os'; + +import handleActions from '../../actions'; +import getLogger from '../../utils/getLogger'; +import choosePart from '../start/choosePart'; +import getParser from '../start/fetchBluprint/parser'; +import minimatch from 'minimatch'; + +const logger = getLogger(); + +// best effort attempt at reproducing .gitignore behaviour +const createIgnoreFilter = config => { + const globs = fs.readFileSync(config, 'utf-8').split('\n').filter(line => !line.trim().startsWith('#')); + return file => !globs.some(glob => file.startsWith(glob) || minimatch(file, glob)); +}; + +const getFiles = dir => { + const basename = path.basename(dir); + const files = fs.readdirSync(dir); + const result = []; + + let filterFn = () => true; + + for (const file of files) { + const fullPath = path.join(dir, file); + + if (file === '.gitignore') { + filterFn = createIgnoreFilter(fullPath); + } + + if (fs.statSync(fullPath).isDirectory()) { + getFiles(fullPath).forEach(f => result.push(f)); + } else { + result.push(file); + } + } + + return result.filter(e => filterFn(e) && !e.match(/^\.git(\/|$)/)).map(e => [basename, path.sep, e].join('')); +}; + +const buildTarball = async(directory) => { + const tarball = path.join(os.tmpdir(), 'tmp-bluprint.tar'); + const files = getFiles(directory); + + await tar.create({ + gzip: false, + file: tarball, + cwd: path.dirname(directory), + strict: true, + }, files); + + return tarball; +}; + +const fetchBluprint = async(tarballPath, filterGlobs, mergeJson) => { + return new Promise((resolve, reject) => { + const rs = fs.createReadStream(tarballPath); + const parser = getParser(resolve, reject, filterGlobs, mergeJson); + + rs.pipe(parser) + .on('error', (e) => { + logger.error(`Tarball parsing error.`); + reject(e); + }); + }); +}; + +const defaultInject = { + method: null, + category: null, + bluprint: null, + partConfirm: null, + partChoice: null, +}; + +export default async(directory, inject = defaultInject) => { + const bluprintrc = JSON.parse(fs.readFileSync(path.join(directory, '.bluprintrc'))); + + const { parts, mergeJson } = bluprintrc; + const { part, globs: filterGlobs } = await choosePart(parts, inject); + + const tarball = await buildTarball(directory); + + try { + await fetchBluprint(tarball, filterGlobs, mergeJson); + await handleActions(bluprintrc.actions, part); + } finally { + fs.rmSync(tarball); + } +}; diff --git a/lib/index.js b/lib/index.js index 01192cd..5dc3b64 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,7 @@ export { default as remove } from './commands/remove'; export { default as start } from './commands/start'; export { default as clone } from './commands/clone'; export { default as token } from './commands/token'; +export { default as test } from './commands/test'; export { default as handleActions } from './actions'; diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..2c25936 --- /dev/null +++ b/test/test.js @@ -0,0 +1,73 @@ +const expect = require('expect.js'); +const path = require('path'); +const { test } = require('../dist'); +const os = require('os'); +const fs = require('fs'); + +const resolvePath = (filePath) => path.join(process.cwd(), filePath); + +const createTestBluprint = function() { + const tempdir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-bluprint')); + + fs.writeFileSync(path.join(tempdir, '.bluprintrc'), JSON.stringify({ + bluprint: '^0.0.1', + name: 'My test bluprint', + category: 'testing', + actions: [ + { + action: 'render', + engine: 'mustache', + context: { foo: 'bar' }, + files: [ + 'test-command-1.txt', + 'subdir/test-command-2.txt', + ], + }, + ], + })); + + fs.mkdirSync(path.join(tempdir, 'subdir')); + fs.writeFileSync(path.join(tempdir, 'test-command-1.txt'), 'foo = {{ foo }}'); + fs.writeFileSync(path.join(tempdir, 'subdir', 'test-command-2.txt'), 'foo = {{ foo }}'); + fs.writeFileSync(path.join(tempdir, 'subdir', 'ignored.txt'), 'ignore me'); + fs.writeFileSync(path.join(tempdir, 'subdir', '.gitignore'), '# this should be ignored\nignored.*'); + + return tempdir; +}; + +describe('Test command: test', function() { + this.timeout(10000); + + let testBluprint; + let cleanupFiles; + + beforeEach(async function() { + testBluprint = createTestBluprint(); + + cleanupFiles = [ + testBluprint, + resolvePath('test-command-1.txt'), + resolvePath('subdir/test-command-2.txt'), + resolvePath('subdir/ignored.txt'), + resolvePath('subdir/.gitignore'), + resolvePath('subdir'), + ]; + }); + + afterEach(function() { + cleanupFiles.forEach(f => fs.rmSync(f, { recursive: true, force: true })); + }); + + it('Creates a new project from a bluprint directory', async function() { + await test(testBluprint); + + expect(fs.readFileSync('test-command-1.txt', 'utf8')).to.be('foo = bar'); + expect(fs.readFileSync('subdir/test-command-2.txt', 'utf8')).to.be('foo = bar'); + }); + + it('Ignores files from .gitignore', async function() { + await test(testBluprint); + + expect(fs.existsSync('ignored.txt')).to.be(false); + }); +});