diff --git a/.editorconfig b/.editorconfig index 192641a..818e072 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,17 +1,13 @@ -# http://editorconfig.org root = true [*] indent_style = space -indent_size = 2 end_of_line = lf charset = utf-8 +indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true -[*.md] -trim_trailing_whitespace = false - -[test/fixtures/*] -insert_final_newline = false +[{**/{actual,fixtures,expected,templates}/**,*.md}] trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..948dbdb --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,122 @@ +{ + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true + }, + + "env": { + "browser": false, + "es6": true, + "node": true, + "mocha": true + }, + + "globals": { + "document": false, + "navigator": false, + "window": false + }, + + "rules": { + "accessor-pairs": 2, + "arrow-spacing": [2, { "before": true, "after": true }], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "comma-dangle": [2, "never"], + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "constructor-super": 2, + "curly": [2, "multi-line"], + "dot-location": [2, "property"], + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + "generator-star-spacing": [2, { "before": true, "after": true }], + "handle-callback-err": [2, "^(err|error)$" ], + "indent": [2, 2, { "SwitchCase": 1 }], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + "keyword-spacing": [2, { "before": true, "after": true }], + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-class-assign": 2, + "no-cond-assign": 2, + "no-const-assign": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], + "no-native-reassign": 0, + "no-negated-in-lhs": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-proto": 0, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-return-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 0, + "no-undef": 2, + "no-undef-init": 2, + "no-unexpected-multiline": 2, + "no-unneeded-ternary": [2, { "defaultAssignment": false }], + "no-unreachable": 2, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-useless-call": 0, + "no-with": 2, + "one-var": [0, { "initialized": "never" }], + "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], + "padded-blocks": [0, "never"], + "quotes": [2, "single", "avoid-escape"], + "radix": 2, + "semi": [2, "always"], + "semi-spacing": [2, { "before": false, "after": true }], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], + "use-isnan": 2, + "valid-typeof": 2, + "wrap-iife": [2, "any"], + "yoda": [2, "never"] + } +} diff --git a/.gitattributes b/.gitattributes index e9dd6bc..660957e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,10 @@ # Enforce Unix newlines -*.* text eol=lf +* text eol=lf # binaries +*.ai binary +*.psd binary *.jpg binary *.gif binary *.png binary -*.jpeg binary \ No newline at end of file +*.jpeg binary diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 0000000..3957f52 --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,45 @@ +# Contributing to Generate + +First and foremost, thank you! We appreciate that you want to contribute to Generate, your time is valuable, and your contributions mean a lot to us. + +**What does "contributing" mean?** + +Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following: + +- Updating or correcting documentation +- Feature requests +- Bug reports + +The [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) also has good advice. + +## Issues + +**Before creating an issue** + +Please make sure you're creating one in the right place: + +- do you have a template syntax question? Like how to accomplish something with handlebars? The best place to get answers for this is [stackoverflow.com](https://github.com/stackoverflow.com), the [handlebars docs](handlebarsjs.com), or the documentation for the template engine you're using. +- Are you having an issue with an Generate feature that is powered by an underlying lib? This is sometimes difficult to know, but sometimes it can be pretty easy to find out. For example, if you use a glob pattern somewhere and you found what you believe to be a matching bug, that would probably be an issue for [node-glob][] or [micromatch][] + +**Creating an issue** + +Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue: + +- what version of assemble are you using? +- is the issue helper-related? If so, this issue should probably be opened on the repo related to the helper being used. +- do you have any custom helpers defined? Is the issue related to the helper itself, data (context) being passed to the helper, or actually registering the helper in the first place? +- are you using middleware? +- any plugins? + + +## Above and beyond + +Here are some tips for creating idiomatic issues. Taking just a little bit extra time will make your issue easier to read, easier to resolve, more likely to be found by others who have the same or similar issue in the future. + +- take some time to learn basic markdown. This [markdown cheatsheet](https://gist.github.com/jonschlinkert/5854601) is super helpful, as is the GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/). +- Learn about [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). And if you want to really go above and beyond, read [mastering markdown](https://guides.github.com/features/mastering-markdown/). +- use backticks to wrap code. This ensures that code will retain its format, making it much more readable to others +- use syntax highlighting by adding the correct language name after the first "code fence" + +[node-glob]: https://github.com/isaacs/node-glob +[micromatch]: https://github.com/jonschlinkert/micromatch diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..1409b5d --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,13 @@ +Before opening an issue, please: + +- read the [contributing guidelines](https://github.com/generate/generate/blob/master/.github/contributing.md) +- [search for existing duplicate or closed issues](https://github.com/generate/generate/issues?utf8=%E2%9C%93&q=is%3Aissue) +- Do not open issues to ask for implementation help or to ask "how to" questions here. Instead ask on [StackOverflow](stackoverflow.com) or one of the services listed on the [readme](https://github.com/generate/generate/blob/master/README.md#community) + +For bug reports, please provide the following details: + +- **version**: what version of generate you were using when you experienced the bug? +- **[reduced test case](https://css-tricks.com/reduced-test-cases/)**: the minimum amount of detail and code to reproduce the bug +- **error messages**: please paste any error reports into the issue or a gist + +Please wrap all code and error messages in [markdown code fences](https://help.github.com/articles/creating-and-highlighting-code-blocks/). \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1082f89..7988154 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,21 @@ +# always ignore files +*.DS_Store +*.sublime-* + +# test related, or directories generated by tests +test/actual +actual +coverage + +# npm node_modules npm-debug.log -tmp +# misc +_gh_pages +benchmark +bower_components +vendor temp +tmp TODO.md -vendor - -*.sublime-* -*.DS_Store \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 6e5a84a..0000000 --- a/.jshintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "asi": false, - "boss": true, - "curly": true, - "eqeqeq": true, - "eqnull": true, - "esnext": true, - "immed": true, - "latedef": false, - "laxcomma": false, - "mocha": true, - "newcap": true, - "noarg": true, - "node": true, - "sub": true, - "undef": true, - "unused": true -} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3932eaa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +sudo: false +language: node_js +node_js: + - '6' + - '5' + - '4' + - '0.12' + - '0.10' +matrix: + fast_finish: true + allow_failures: + - node_js: '4' + - node_js: '0.10' + - node_js: '0.12' diff --git a/.verb.md b/.verb.md index 4ba2246..062714a 100644 --- a/.verb.md +++ b/.verb.md @@ -1,39 +1,253 @@ -# {%= name %} {%= badge("fury") %} +You might also be interested in: -> {%= description %} +- [generate](https://github.com/generate/generate) +- [assemble](https://github.com/assemble/assemble) +- [verb](https://github.com/verbose/verb) -Currently this only updates the year in a project, but the goal is to add other features for keeping a project up to date. +## Quickstart -Also, the regex used for updating the year in copyright statements is pretty opinionated at the moment, but I'm open to making this more flexible if someone wants to do a PR. +Install Update's CLI and an example [updater](#updaters) globally: + +```sh +$ npm install --global update && updater-example +``` + +Initialize `update`: + +```sh +$ update init +``` + +Run update: + +```sh +$ update +``` + +## Overview + +### What does Update do? + +All updating is accomplished using plugins called _updaters_, which are run by command line or API, and can be installed globally, locally, or created in a local `updatefile.js`. + +You can create your own [updaters](docs/updaters.md) using Update's API, or install updaters using [npm](https://www.npmjs.com/), to do things like: + +* create continuity and consistency across projects +* enforce conventions across all of your projects +* instantly update an old or inherited project to your latest personal preferences (convert tabs to spaces, convert from `jshint` to `eslint` or the other way around, or any other detail) +* reformat code to meet your standards +* convert a config file to a different format (json to yaml, yaml to json, etc) +* update files that are typically excluded from build cycles, and are often forgotten about after they're created. For example: + - fix dates in copyrights, [licenses](https://github.com/update/updater-license) and banners + - remove deprecated fields from [project manifests](https://github.com/update/updater-package) + - update settings in [runtime config](https://github.com/update/updater-eslint) files, preferences in [dotfiles](https://github.com/update/updater-editorconfig) +* after initializing a new project with a project generator, like [generate][] or Google's Yeoman, you can "normalize" all of the generated files to use your own preferences + + +### Why should I use Update? + +- **be more productive**: Update eliminates time spent on things that _can be automated_, but typically aren't since they either don't need to be done often, don't fit into the build cycle or a project's deliverables, or because they're usually updated manually. As code projects mature, time spent on these things tend to stay linear or increase as the size of a community grows. If you maintain more than a handful of projects, time spent on these things compounds with each new project under your stewardship. +- **your way, instantly**: updaters can be published to and installed from npm, but you can also easily create your own [personal updaters](docs/symlinking-updaters.md). Once your updaters are setup, just run `update init`, then projects under your maintenance will convert to the the conventions you prefer within milliseconds after running `update`. +- **plugin ecosystem**: any plugins that work with [Base applications](#discovering-plugins) will work also with Update. Which means you can use plugins (or generators) from [assemble][], [verb][], and [generate][], to name a few. +- **well tested**: with more than 1,250 unit tests + +### Examples + +Here are some random example commits after running `$ update`. + +**Project**/**Commit** | **Updaters used** +--- | --- +[generate-scaffold][generate-scaffold-commit] | `editorconfig`, `travis` +[updater-editorconfig][updater-editorconfig-commit] | `editorconfig`, `eslint`, `travis`, `license` +[expand-target][et] | `editorconfig`, `eslint`, `travis`, `package` + +### Features + +* **unparalleled flow control**: through the use of [updaters](docs/updaters.md), [sub-updaters][getting-started] and [tasks](docs/tasks.md) +* **generators**: support for [generate][] generators. If your updater needs to create new files, there might be a [generator for that](https://www.npmjs.com/browse/keyword/generate-generator). Just use the generator the same way you would use an [updater](docs/updaters.md). +* **render templates**: use templates to create new files, or replace existing files +* **prompts**: It's easy to create custom prompts. Answers to prompts can be used as context for rendering templates, for settings options, determining file names, directory structure, and anything else that requires user feedback. +* **any engine**: use any template engine to render templates, including [handlebars][], [lodash][], [swig][] and [pug][], or anything supported by [consolidate][]. +* **data**: gather data from the user's environment to populate "hints" in user prompts or for rendering templates +* **fs**: in the spirit of [gulp][], use `.src` and `.dest` to read and write globs of files. +* **vinyl**: files and templates are [vinyl][] files +* **streams**: full support for [gulp][] and [assemble][] plugins +* **smart plugins**: Update is built on [base][], so any "smart" plugin from the Base ecosystem can be used +* **stores**: persist configuration settings, global defaults, project-specific defaults, answers to prompts, and so on. +* much more! -{%= include("install-global") %} ## CLI -From the command line, run: +### Installing update + +**Install update** + +To use Update's CLI, `update` must first be installed globally with [npm](https://www.npmjs.com/): + +```sh +$ npm install --global update +``` + +This adds the `update` command to your system path, allowing it to be run from anywhere. + + +### Installing updaters + +Updaters can be [found on npm](https://www.npmjs.com/browse/keyword/updateupdater), but if you're not familiar with how Update works, we recommend installing `updater-example`: + +```sh +$ npm install --global updater-example +``` + +**Create "example.txt"** + +In the current working directory, create an empty file named `example.txt`. + +**Run** + +As a habit, when using `update` make sure your work is committed, then run: -```bash -update +```sh +$ update example ``` -## Run tests +This appends the string `foo` to the contents of `example.txt`. Visit the [updater-example][] project for additional steps and guidance. + +## Tasks + +Update ships with the following built-in [tasks](docs/tasks.md). These will be externalized to an updater or [generate][] generator at some point. + +{%= apidocs("lib/updatefile.js") %} + +## Help menu + +```console +$ update help + + Usage: update [options] + + Command: updater or tasks to run + + Options: + + --config, -c Save a configuration value to the `update` object in package.json + --cwd Set or display the current working directory + --help, -h Display this help menu + --init, -i Prompts you to choose the updaters to automatically run (your "queue") + --add Add updaters to your queue + --remove Remove updaters from your queue + --run Force tasks to run regardless of command line flags used + --silent, -S Silence all tasks and updaters in the terminal + --show Display the value of + --version, -V Display the current version of update + --verbose, -v Display all verbose logging messages + + Examples: + + # run updater "foo" + $ update foo + + # run task "bar" from updater "foo" + $ update foo:bar + + # run multiple tasks from updater "foo" + $ update foo:bar,baz,qux + + # run a sub-generator from updater "foo" + $ update foo.abc + + # run task "xyz" from sub-generator "foo.abc" + $ update foo.abc:xyz + + Update attempts to automatically determine if "foo" is a task or updater. + If there is a conflict, you can force update to run updater "foo" + by specifying its default task. Example: `$ update foo:default` +``` + +## API + +### Updaters + +#### Discovering updaters + +* Find updaters to install by [searching npm](https://www.npmjs.com/browse/keyword/updateupdater) for packages with the keyword `updateupdater` +* Visit [Update's GitHub org](https://github.com/update) to see the updaters maintained by the core team + +#### Discovering plugins + +Plugins from any applications built on [base][] should work with Update (and can be used in your updater): + +* [base][base-plugin]: find base plugins on npm using the `baseplugin` keyword +* [assemble][assemble-plugin]: find assemble plugins on npm using the `assembleplugin` keyword +* [generate][generate-plugin]: find generate plugins on npm using the `generateplugin` keyword +* [templates][templates-plugin]: find templates plugins on npm using the `templatesplugin` keyword +* [update][update-plugin]: find update plugins on npm using the `updateplugin` keyword +* [verb][verb-plugin]: find verb plugins on npm using the `verbplugin` keyword + +#### Authoring updaters + +Visit the [updater documentation](docs/updaters.md) guide to learn how to use, author and publish updaters. + +## Configuration + +Customize settings and default behavior using the `update` property in package.json. These values will override global defaults. + +```js +{ + "update": { + "updaters": ["package", "license", "keywords"] + } +} +``` + +### Options + +The following options may be defined in package.json. + +#### updaters + +The updaters to run on the current project. + +**Example** -Install dev dependencies: +Run `updater-license` and `updater-package` on the current project: -```bash -node i -d && mocha +```js +{ + "update": { + "updaters": ["package", "license"] + } +} ``` -## Contributing -Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue]({%= bugs.url %}) +## More information -## Author -{%= include("author") %} +- See the [updaters maintained by the core team](https://github.com/update) +- Browse the [documentation](docs) +- Browse the [API documentation](docs/api) +- Learn about [updaters](docs/updaters.md) +- Learn about the [built-in updaters](docs/cli/built-in-updaters.md) +- Learn more about [base][] +- Get [Sublime Text Snippets][st] for creating tasks and updaters -## License -{%= copyright({year: 2014}) %} -{%= license() %} +## Release History +{%= increaseHeadings(changelog('CHANGELOG.md', { + changelogFooter: true, + stripHeading: true, + repo: repo +})) %} -*** +[Update]: https://github.com/update/update +[getting-started]: https://github.com/update/getting-started +[base-plugin]: https://www.npmjs.com/browse/keyword/baseplugin +[assemble-plugin]: https://www.npmjs.com/browse/keyword/assembleplugin +[generate-plugin]: https://www.npmjs.com/browse/keyword/generateplugin +[templates-plugin]: https://www.npmjs.com/browse/keyword/templatesplugin +[update-plugin]: https://www.npmjs.com/browse/keyword/updateplugin +[verb-plugin]: https://www.npmjs.com/browse/keyword/verbplugin +[st]: https://github.com/node-base/sublime-text-base-snippets -{%= include("footer") %} \ No newline at end of file +[generate-scaffold-commit]: https://github.com/generate/generate-scaffold/commit/440d71f7293cb1f79445c0161440afbb266a2fbe +[updater-editorconfig-commit]: https://github.com/update/updater-editorconfig/commit/b7bd0aa616519440fa4a0d29d3aefac26787cbaf +[et]: https://github.com/jonschlinkert/expand-target/commit/48d70a0bc95d8eb3f7def615b7e231e8f93816e8 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..170cc9c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Release History + +## key + +Changelog entries are classified using the following labels from [keep-a-changelog][]: + +* `added`: for new features +* `changed`: for changes in existing functionality +* `deprecated`: for once-stable features removed in upcoming releases +* `removed`: for deprecated features removed in this release +* `fixed`: for any bug fixes + +Custom labels used in this changelog: + +* `dependencies`: bumps dependencies +* `housekeeping`: code re-organization, minor edits, or other changes that don't fit in one of the other categories. + +**Heads up!** + +Please [let us know](../../issues) if any of the following heading links are broken. Thanks! + +## [0.7.3] - 2016-07-21 + +**fixed** + +- ensure `app.cwd` in the current instance is the cwd defined by the user on the options or argv. + +## [0.7.0] - 2016-07-21 + +**added** + +- as of v0.7.0, we will begin using the [keep-a-changelog][] format for release history +- adds support user-defined templates +- adds support for `app.home()`, which resolves to `~/` or the user-defined `options.homedir`. This directory is used to determine the base directory for user-defined templates. +- adds support for [common-config](https://github.com/jonschlinkert/common-config). Exposed on the `app.common` object (e.g. `app.common.set()` etc) +- adds experimental support for a `home` updater. If an `updatefile.js` exists in the `~/update` directory (this will be customizable, but it's not yet), this file will be loaded and `.use()`d as a plugin before other updaters are loaded. You can use this to set options, add defaults, etc. But you can also run it explictly via commandline with the `update home` command. + +**fixed** + +- fixes `app.cwd` so that it's updated when `app.options.dest` (`--dest`) is set +- ensure args are parsed consistently + +## [0.6.0] + +- Swap out [base][] for [assemble-core][] (which uses Base via [templates][]). This allows updaters to seamlessly run generators from [generate][], [assemble][], or [verb][] (when a file needs to be created, or re-created for example) +- Adds [assemble-loader][] to support glob patterns in collection methods + +## [0.5.0] + +First stable release! + +[keep-a-changelog]: https://github.com/olivierlacan/keep-a-changelog diff --git a/LICENSE b/LICENSE index 6d53705..1e49edf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2015, Jon Schlinkert. +Copyright (c) 2015-2016, Jon Schlinkert. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 559e5f0..e129f2d 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,400 @@ -# update [![NPM version](https://badge.fury.io/js/update.svg)](http://badge.fury.io/js/update) +

-> Update the year in all files in a project using glob patterns. + + + +

-Currently this only updates the year in a project, but the goal is to add other features for keeping a project up to date. +Be scalable! Update is a new, open source developer framework and CLI for automating updates of any kind in code projects. -Also, the regex used for updating the year in copyright statements is pretty opinionated at the moment, but I'm open to making this more flexible if someone wants to do a PR. +# update -## Install globally with [npm](npmjs.org): +[![NPM version](https://img.shields.io/npm/v/update.svg?style=flat)](https://www.npmjs.com/package/update) [![NPM monthly downloads](https://img.shields.io/npm/dm/update.svg?style=flat)](https://npmjs.org/package/update) [![Build Status](https://img.shields.io/travis/update/update.svg?style=flat)](https://travis-ci.org/update/update) -```bash -npm i -g update +You might also be interested in: + +* [generate](https://github.com/generate/generate) +* [assemble](https://github.com/assemble/assemble) +* [verb](https://github.com/verbose/verb) + +## Quickstart + +Install Update's CLI and an example [updater](#updaters) globally: + +```sh +$ npm install --global update && updater-example ``` +Initialize `update`: + +```sh +$ update init +``` + +Run update: + +```sh +$ update +``` + +## Overview + +### What does Update do? + +All updating is accomplished using plugins called _updaters_, which are run by command line or API, and can be installed globally, locally, or created in a local `updatefile.js`. + +You can create your own [updaters](docs/updaters.md) using Update's API, or install updaters using [npm](https://www.npmjs.com/), to do things like: + +* create continuity and consistency across projects +* enforce conventions across all of your projects +* instantly update an old or inherited project to your latest personal preferences (convert tabs to spaces, convert from `jshint` to `eslint` or the other way around, or any other detail) +* reformat code to meet your standards +* convert a config file to a different format (json to yaml, yaml to json, etc) +* update files that are typically excluded from build cycles, and are often forgotten about after they're created. For example: + - fix dates in copyrights, [licenses](https://github.com/update/updater-license) and banners + - remove deprecated fields from [project manifests](https://github.com/update/updater-package) + - update settings in [runtime config](https://github.com/update/updater-eslint) files, preferences in [dotfiles](https://github.com/update/updater-editorconfig) +* after initializing a new project with a project generator, like [generate](https://github.com/generate/generate) or Google's Yeoman, you can "normalize" all of the generated files to use your own preferences + +### Why should I use Update? + +* **be more productive**: Update eliminates time spent on things that _can be automated_, but typically aren't since they either don't need to be done often, don't fit into the build cycle or a project's deliverables, or because they're usually updated manually. As code projects mature, time spent on these things tend to stay linear or increase as the size of a community grows. If you maintain more than a handful of projects, time spent on these things compounds with each new project under your stewardship. +* **your way, instantly**: updaters can be published to and installed from npm, but you can also easily create your own [personal updaters](docs/symlinking-updaters.md). Once your updaters are setup, just run `update init`, then projects under your maintenance will convert to the the conventions you prefer within milliseconds after running `update`. +* **plugin ecosystem**: any plugins that work with [Base applications](#discovering-plugins) will work also with Update. Which means you can use plugins (or generators) from [assemble](https://github.com/assemble/assemble), [verb](https://github.com/verbose/verb), and [generate](https://github.com/generate/generate), to name a few. +* **well tested**: with more than 1,250 unit tests + +### Examples + +Here are some random example commits after running `$ update`. + +**Project**/**Commit** | **Updaters used** +--- | --- +[generate-scaffold](https://github.com/generate/generate-scaffold/commit/440d71f7293cb1f79445c0161440afbb266a2fbe) | `editorconfig`, `travis` +[updater-editorconfig](https://github.com/update/updater-editorconfig/commit/b7bd0aa616519440fa4a0d29d3aefac26787cbaf) | `editorconfig`, `eslint`, `travis`, `license` +[expand-target](https://github.com/jonschlinkert/expand-target/commit/48d70a0bc95d8eb3f7def615b7e231e8f93816e8) | `editorconfig`, `eslint`, `travis`, `package` + +### Features + +* **unparalleled flow control**: through the use of [updaters](docs/updaters.md), [sub-updaters](https://github.com/update/getting-started) and [tasks](docs/tasks.md) +* **generators**: support for [generate](https://github.com/generate/generate) generators. If your updater needs to create new files, there might be a [generator for that](https://www.npmjs.com/browse/keyword/generate-generator). Just use the generator the same way you would use an [updater](docs/updaters.md). +* **render templates**: use templates to create new files, or replace existing files +* **prompts**: It's easy to create custom prompts. Answers to prompts can be used as context for rendering templates, for settings options, determining file names, directory structure, and anything else that requires user feedback. +* **any engine**: use any template engine to render templates, including [handlebars](http://www.handlebarsjs.com/), [lodash](https://lodash.com/), [swig](https://github.com/paularmstrong/swig) and [pug](https://pugjs.org), or anything supported by [consolidate](https://github.com/visionmedia/consolidate.js). +* **data**: gather data from the user's environment to populate "hints" in user prompts or for rendering templates +* **fs**: in the spirit of [gulp](http://gulpjs.com), use `.src` and `.dest` to read and write globs of files. +* **vinyl**: files and templates are [vinyl](https://github.com/gulpjs/vinyl) files +* **streams**: full support for [gulp](http://gulpjs.com) and [assemble](https://github.com/assemble/assemble) plugins +* **smart plugins**: Update is built on [base](https://github.com/node-base/base), so any "smart" plugin from the Base ecosystem can be used +* **stores**: persist configuration settings, global defaults, project-specific defaults, answers to prompts, and so on. +* much more! + ## CLI -From the command line, run: +### Installing update + +**Install update** + +To use Update's CLI, `update` must first be installed globally with [npm](https://www.npmjs.com/): + +```sh +$ npm install --global update +``` + +This adds the `update` command to your system path, allowing it to be run from anywhere. + +### Installing updaters + +Updaters can be [found on npm](https://www.npmjs.com/browse/keyword/updateupdater), but if you're not familiar with how Update works, we recommend installing `updater-example`: + +```sh +$ npm install --global updater-example +``` + +**Create "example.txt"** + +In the current working directory, create an empty file named `example.txt`. + +**Run** + +As a habit, when using `update` make sure your work is committed, then run: + +```sh +$ update example +``` + +This appends the string `foo` to the contents of `example.txt`. Visit the [updater-example](https://github.com/update/updater-example) project for additional steps and guidance. + +## Tasks + +Update ships with the following built-in [tasks](docs/tasks.md). These will be externalized to an updater or [generate](https://github.com/generate/generate) generator at some point. + +### [init](lib/updatefile.js#L29) + +Select the updaters to run every time `update` is run. Use `--add` to add additional updaters, and `--remove` to remove them. You can run this command whenever you want to update your preferences, like after installing new updaters. + +**Example** + +```sh +$ update init +``` + +### [list](lib/updatefile.js#L64) + +Display a list of currently installed updaters. + +**Example** + +```sh +$ update defaults:list +# aliased as +$ update list +``` + +### [help](lib/updatefile.js#L85) + +Display a help [menu](#help-menu) of available commands and flags. + +**Example** + +```sh +$ update defaults:help +# aliased as +$ update help +``` + +### [show](lib/updatefile.js#L101) + +Show the list of updaters that are registered to run on the current project. + +**Example** + +```sh +$ update defaults:show +# aliased as +$ update show +``` + +### [help](lib/updatefile.js#L120) + +Default task for the built-in `defaults` generator. + +**Example** + +```sh +$ update help +``` + +## Help menu -```bash -update +```console +$ update help + + Usage: update [options] + + Command: updater or tasks to run + + Options: + + --config, -c Save a configuration value to the `update` object in package.json + --cwd Set or display the current working directory + --help, -h Display this help menu + --init, -i Prompts you to choose the updaters to automatically run (your "queue") + --add Add updaters to your queue + --remove Remove updaters from your queue + --run Force tasks to run regardless of command line flags used + --silent, -S Silence all tasks and updaters in the terminal + --show Display the value of + --version, -V Display the current version of update + --verbose, -v Display all verbose logging messages + + Examples: + + # run updater "foo" + $ update foo + + # run task "bar" from updater "foo" + $ update foo:bar + + # run multiple tasks from updater "foo" + $ update foo:bar,baz,qux + + # run a sub-generator from updater "foo" + $ update foo.abc + + # run task "xyz" from sub-generator "foo.abc" + $ update foo.abc:xyz + + Update attempts to automatically determine if "foo" is a task or updater. + If there is a conflict, you can force update to run updater "foo" + by specifying its default task. Example: `$ update foo:default` ``` -## Run tests +## API + +### Updaters + +#### Discovering updaters + +* Find updaters to install by [searching npm](https://www.npmjs.com/browse/keyword/updateupdater) for packages with the keyword `updateupdater` +* Visit [Update's GitHub org](https://github.com/update) to see the updaters maintained by the core team + +#### Discovering plugins + +Plugins from any applications built on [base](https://github.com/node-base/base) should work with Update (and can be used in your updater): + +* [base](https://www.npmjs.com/browse/keyword/baseplugin): find base plugins on npm using the `baseplugin` keyword +* [assemble](https://www.npmjs.com/browse/keyword/assembleplugin): find assemble plugins on npm using the `assembleplugin` keyword +* [generate](https://www.npmjs.com/browse/keyword/generateplugin): find generate plugins on npm using the `generateplugin` keyword +* [templates](https://www.npmjs.com/browse/keyword/templatesplugin): find templates plugins on npm using the `templatesplugin` keyword +* [update](https://www.npmjs.com/browse/keyword/updateplugin): find update plugins on npm using the `updateplugin` keyword +* [verb](https://www.npmjs.com/browse/keyword/verbplugin): find verb plugins on npm using the `verbplugin` keyword -Install dev dependencies: +#### Authoring updaters -```bash -node i -d && mocha +Visit the [updater documentation](docs/updaters.md) guide to learn how to use, author and publish updaters. + +## Configuration + +Customize settings and default behavior using the `update` property in package.json. These values will override global defaults. + +```js +{ + "update": { + "updaters": ["package", "license", "keywords"] + } +} +``` + +### Options + +The following options may be defined in package.json. + +#### updaters + +The updaters to run on the current project. + +**Example** + +Run `updater-license` and `updater-package` on the current project: + +```js +{ + "update": { + "updaters": ["package", "license"] + } +} ``` -## Contributing -Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](https://github.com/jonschlinkert/update/issues) +## More information + +* See the [updaters maintained by the core team](https://github.com/update) +* Browse the [documentation](docs) +* Browse the [API documentation](docs/api) +* Learn about [updaters](docs/updaters.md) +* Learn about the [built-in updaters](docs/cli/built-in-updaters.md) +* Learn more about [base](https://github.com/node-base/base) +* Get [Sublime Text Snippets](https://github.com/node-base/sublime-text-base-snippets) for creating tasks and updaters + +## Release History + +### key + +Changelog entries are classified using the following labels from [keep-a-changelog](https://github.com/olivierlacan/keep-a-changelog): + +* `added`: for new features +* `changed`: for changes in existing functionality +* `deprecated`: for once-stable features removed in upcoming releases +* `removed`: for deprecated features removed in this release +* `fixed`: for any bug fixes -## Author +Custom labels used in this changelog: + +* `dependencies`: bumps dependencies +* `housekeeping`: code re-organization, minor edits, or other changes that don't fit in one of the other categories. + +**Heads up!** + +Please [let us know](../../issues) if any of the following heading links are broken. Thanks! + +### [0.7.3](https://github.com/update/update/compare/0.7.0...0.7.3) - 2016-07-21 + +**fixed** + +* ensure `app.cwd` in the current instance is the cwd defined by the user on the options or argv. + +### [0.7.0](https://github.com/update/update/compare/0.6.0...0.7.0) - 2016-07-21 + +**added** + +* as of v0.7.0, we will begin using the [keep-a-changelog](https://github.com/olivierlacan/keep-a-changelog) format for release history +* adds support user-defined templates +* adds support for `app.home()`, which resolves to `~/` or the user-defined `options.homedir`. This directory is used to determine the base directory for user-defined templates. +* adds support for [common-config](https://github.com/jonschlinkert/common-config). Exposed on the `app.common` object (e.g. `app.common.set()` etc) +* adds experimental support for a `home` updater. If an `updatefile.js` exists in the `~/update` directory (this will be customizable, but it's not yet), this file will be loaded and `.use()`d as a plugin before other updaters are loaded. You can use this to set options, add defaults, etc. But you can also run it explictly via commandline with the `update home` command. + +**fixed** + +* fixes `app.cwd` so that it's updated when `app.options.dest` (`--dest`) is set +* ensure args are parsed consistently + +### [0.6.0](https://github.com/update/update/compare/0.5.0...0.6.0) + +* Swap out [base](https://github.com/node-base/base) for [assemble-core](https://github.com/assemble/assemble-core) (which uses Base via [templates](https://github.com/jonschlinkert/templates)). This allows updaters to seamlessly run generators from [generate](https://github.com/generate/generate), [assemble](https://github.com/assemble/assemble), or [verb](https://github.com/verbose/verb) (when a file needs to be created, or re-created for example) +* Adds [assemble-loader](https://github.com/assemble/assemble-loader) to support glob patterns in collection methods + +### [0.5.0] + +First stable release! + +_(Changelog generated by [helper-changelog](https://github.com/helpers/helper-changelog))_ + +## About + +### Related projects + +* [assemble](https://www.npmjs.com/package/assemble): Get the rocks out of your socks! Assemble makes you fast at creating web projects… [more](https://github.com/assemble/assemble) | [homepage](https://github.com/assemble/assemble "Get the rocks out of your socks! Assemble makes you fast at creating web projects. Assemble is used by thousands of projects for rapid prototyping, creating themes, scaffolds, boilerplates, e-books, UI components, API documentation, blogs, building websit") +* [base](https://www.npmjs.com/package/base): Framework for rapidly creating high quality, server-side node.js applications, using plugins like building blocks | [homepage](https://github.com/node-base/base "Framework for rapidly creating high quality, server-side node.js applications, using plugins like building blocks") +* [generate](https://www.npmjs.com/package/generate): Command line tool and developer framework for scaffolding out new GitHub projects. Generate offers the… [more](https://github.com/generate/generate) | [homepage](https://github.com/generate/generate "Command line tool and developer framework for scaffolding out new GitHub projects. Generate offers the robustness and configurability of Yeoman, the expressiveness and simplicity of Slush, and more powerful flow control and composability than either.") +* [verb](https://www.npmjs.com/package/verb): Documentation generator for GitHub projects. Verb is extremely powerful, easy to use, and is used… [more](https://github.com/verbose/verb) | [homepage](https://github.com/verbose/verb "Documentation generator for GitHub projects. Verb is extremely powerful, easy to use, and is used on hundreds of projects of all sizes to generate everything from API docs to readmes.") + +### Community + +Are you using [Update](https://github.com/update/update) in your project? Have you published an [updater](https://github.com/update/update/blob/master/docs/updaters.md) and want to share your Update project with the world? + +Here are some suggestions! + +* If you get like Update and want to tweet about it, please use the hashtag `#updatejs` (not `@`) +* Show your love by starring [Update](https://github.com/update/update) and `update` +* Get implementation help on [StackOverflow](http://stackoverflow.com/questions/tagged/update) (please use the `updatejs` tag in questions) +* **Gitter** Discuss Update with us on [Gitter](https://gitter.im/update/update) +* If you publish an updater, thank you! To make your project as discoverable as possible, please add the keyword `updateupdater` to package.json. + +### Contributing + +Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). + +Please read the [contributing guide](.github/contributing.md) for advice on opening issues, pull requests, and coding standards. + +### Running tests + +Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: + +```sh +$ npm install && npm test +``` + +### Author **Jon Schlinkert** - -+ [github/jonschlinkert](https://github.com/jonschlinkert) -+ [twitter/jonschlinkert](http://twitter.com/jonschlinkert) -## License -Copyright (c) 2014-2015 Jon Schlinkert -Released under the MIT license +* [github/jonschlinkert](https://github.com/jonschlinkert) +* [twitter/jonschlinkert](https://twitter.com/jonschlinkert) + +### License + +Copyright © 2018, [Jon Schlinkert](https://github.com/jonschlinkert). +Released under the [MIT License](LICENSE). *** -_This file was generated by [verb](https://github.com/assemble/verb) on February 19, 2015._ \ No newline at end of file +_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.6.0, on January 23, 2018._ \ No newline at end of file diff --git a/bin/update.js b/bin/update.js new file mode 100755 index 0000000..e9fdc13 --- /dev/null +++ b/bin/update.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +process.on('exit', function() { + require('set-blocking')(true); +}); + +var os = require('os'); +var Update = require('..'); +var commands = require('../lib/commands'); +var utils = require('../lib/utils'); +var argv = utils.parseArgs(process.argv.slice(2)); + +/** + * Listen for errors + */ + +Update.on('update.preInit', function(app) { + app.on('error', function(err) { + console.log(err.stack); + process.exit(1); + }); +}); + +Update.on('update.postInit', function(app) { + commands(app); +}); + +/** + * Init CLI + */ + +Update.cli(Update, argv, function(err, app) { + if (err) return console.log(err); + + app.cli.process(argv, function(err) { + if (err) app.emit('error', err); + + var tasks = argv._.length ? argv._ : ['default']; + if (app.updatefile !== true || argv.run) { + tasks = Update.resolveTasks(app, argv); + + } else if (app.updatefile === true && app.pkg.get('update.run')) { + tasks = Update.resolveTasks(app, argv).concat(tasks); + } + + app.once('task', function() { + if (!app.base.enabled('silent')) { + app.log.success('running:', logRunning(app, tasks.join(', '))); + } + }); + + app.update(tasks, function(err) { + if (err) return console.log(err); + app.emit('done'); + process.exit(); + }); + }); +}); + +function logRunning(app, str) { + if (os.platform() === 'win32') { + return app.log.bold(app.log.cyan(str)); + } + return app.log.bold(app.log.blue(str)); +} diff --git a/cli.js b/cli.js deleted file mode 100755 index 83a317a..0000000 --- a/cli.js +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -require('./verbfile'); \ No newline at end of file diff --git a/docs/api/plugins.md b/docs/api/plugins.md new file mode 100644 index 0000000..8834950 --- /dev/null +++ b/docs/api/plugins.md @@ -0,0 +1,65 @@ +# Plugins + +A plugin is function that takes an instance of `Update` and is registered with the `.use` method. See the [base-plugins](https://github.com/node-base/base-plugins) documentation for additional details. + +### .use + +The `.use` method is used for registering plugins that should be immediately invoked. + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +function plugin(app) { + // "app" and "this" both expose the instance of update we created above +} + +app.use(plugin); +``` + +Once a plugin is invoked, it will not be called again. + +### .run + +If a plugin returns a function after it's invoked by `.use`, the function will be pushed onto an array allowing it to be called again by the `.run` method. + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +function plugin(app) { + // "app" and "this" both expose the instance of update we created above + return plugin; +} + +app.use(plugin); +``` + +We can now run all plugins that were pushed onto the `.fns` array on any arbitrary object: + +```js +var obj = {}; +app.run(obj); +``` + +Additionally: + +* If `obj` has a `.use` method, it will be used on each plugin (e.g. `obj.use(fn)`). Otherwise `fn(obj)`. +* If the plugin returns a function again and `obj` has a `.run` method, the plugin will be pushed onto the `obj.fns` array. + +This can continue indefinitely as long as the plugin returns a function and the receiving object has `.use`/`.run` functions. + +## Updaters + +When plugins are [registered by name](docs/updaters.md), they are referred to as "updaters". See the [updater documentation](docs/updaters.md) for more details. + +## Related + +**API** + +* [updater](updater.md) +* [register](register.md) diff --git a/docs/api/register.md b/docs/api/register.md new file mode 100644 index 0000000..1c507ba --- /dev/null +++ b/docs/api/register.md @@ -0,0 +1,35 @@ +# Register + +Register an updater function by name. Similar to [.updater](updater.md) but does not invoke the updater function. + +```js +app.register(name, fn); +``` + +**Params** + +* `name` **{String}**: name of the updater to register +* `fn` **{Function}**: updater function + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +// not invoked until called by `.update` +app.register('foo', function(app) { + // do updater stuff +}); + +app.update('foo', function(err) { + if (err) return console.log(err); +}); +``` + +## Related + +**API** + +* [updater](updater.md) +* [plugins](plugins.md) diff --git a/docs/api/updater.md b/docs/api/updater.md new file mode 100644 index 0000000..2ffed48 --- /dev/null +++ b/docs/api/updater.md @@ -0,0 +1,43 @@ +# Updater + +Register an updater function by name. Similar to [.register](register.md) but immediately invokes the updater function upon registering it. + +```js +app.updater(name, fn); +``` + +**Params** + +* `name` **{String}**: name of the updater to register +* `updater` **{Function}**: updater function + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +// immediately invoked +app.updater('bar', function(app) { + // do updater stuff +}); + +app.update('bar', function(err) { + if (err) return console.log(err); +}); +``` + +## Related + +**CLI** + +* [commands](../cli/commands.md) + +**API** + +* [register](register.md) +* [plugins](plugins.md) + +**Docs** + +* [faq](../faq.md) diff --git a/docs/cli/built-in-updaters.md b/docs/cli/built-in-updaters.md new file mode 100644 index 0000000..6b40573 --- /dev/null +++ b/docs/cli/built-in-updaters.md @@ -0,0 +1,51 @@ +# Built in updaters + +Update only has a few built-in [updaters](docs/updaters.md) (these might be externalized at some point): + +* [init](#init): Choose the updaters to run by default each time `update` is run from the command line +* [list](#list): List all globally and locally installed updaters +* [show](#show): show the list of updaters that will run on the current project when the `update` command is given +* [new](#new): create a new `updatefile.js` in the current working directory +* [help](#help): show a help menu with all available commands + +## Usage + +### init + +Prompts you to choose one or more updaters to run by default each time `update` is run from the command line: + +```sh +$ update init +``` + +### list + +List all globally and locally installed updaters: + +```sh +$ update list +``` + +### show + +Show the list of updaters that will run on the current project when the `update` command is given: + +```sh +$ update show +``` + +### new + +Create a new `updatefile.js` in the current working directory: + +```sh +$ update new +``` + +### help + +Display a help menu with all available commands: + +```sh +$ update help +``` diff --git a/docs/cli/commands.md b/docs/cli/commands.md new file mode 100644 index 0000000..41df6d6 --- /dev/null +++ b/docs/cli/commands.md @@ -0,0 +1,17 @@ +# Command line flags + +Supported command line flags. + +## --run + +By default, when an `updatefile.js` exists in the current working directory, the `update` command will only run explicitly specified tasks or, if no tasks are explicitly defined, the `default` task in `updatefile.js`. + +The `--run` flag forces `update` to run stored tasks and the `default` task or explicitly specified tasks in `updatefile.js`. Stored tasks are executed first, in the order defined, then the `default` task or explicitly defined tasks. + +**Default**: `undefined` + +**Example** + +```sh +$ update --run +``` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..edb8ea8 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,23 @@ +# Faq + +## How is the name written? + +Update, with a capital "U", the rest lowercase. `updatefile.js` is all lowercase, no capital letters. + + + +## Aliases + +**What's an updater's alias, and what do they do?** + +Update tries to find globally installed updaters using an "alias" first, falling back on the updater's full name if not found by its alias. + +An updater's alias is created by stripping the substring `updater-` from the _full name_ of updater. Thus, when publishing an updater the naming convention `updater-foo` should be used (where `foo` is the alias, and `updater-foo` is the full name). + +Note that **no dots may be used in published updater names**. Aside from that, any characters considered valid by npm are fine. + +## Related + +**Docs** + +* [features](features.md) diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 0000000..60e9e14 --- /dev/null +++ b/docs/features.md @@ -0,0 +1,20 @@ +# Features + +Update offers an elegant and robust suite of methods, carefully organized to help you accomplish common activities in less time, including: + +* **unparalleled flow control**: through the use of [updaters](https://github.com/update/getting-started), [sub-updaters](https://github.com/update/getting-started) and [tasks](https://github.com/update/getting-started) +* **templates, scaffolds and boilerplates**: update a single file, initialize an entire project, or provide ad-hoc "components" throughout the duration of a project using any combination of [templates, scaffolds and boilerplates](#templates-scaffolds-and-boilerplates) +* **any engine**: use any template engine to render templates, including [handlebars](http://www.handlebarsjs.com/), [lodash](https://lodash.com/), [swig](https://github.com/paularmstrong/swig) and [pug](http://jade-lang.com) +* **prompts**: asks you for data when it can't find what it needs, and it's easy to customize prompts for any data you want +* **data**: gathers data from the user's environment to populate "hints" in user prompts and to use when rendering templates +* **streams**: interact with the file system, with full support for [gulp](http://gulpjs.com) and [assemble](https://github.com/assemble/assemble) plugins +* **smart plugins**: Update is built on [base](https://github.com/node-base/base), so any "smart" plugin can be used +* **stores**: persist configuration settings, global defaults, project-specific defaults, answers to prompts, and so on + +Visit the [getting started guide](https://github.com/update/getting-started) to learn more. + +## Related + +**Docs** + +* [faq](faq.md) diff --git a/docs/installing-the-cli.md b/docs/installing-the-cli.md new file mode 100644 index 0000000..7f384c8 --- /dev/null +++ b/docs/installing-the-cli.md @@ -0,0 +1,50 @@ +# Installing the cli + +To run update from the command line, you'll need to install Update's CLI globally first. You can do that now with the following command: + +```sh +$ npm install --global update +``` + +This adds the `update` command to your system path, allowing it to be run from any directory. + +You should now be able to use the `update` command to execute code in a local `updatefile.js` file, or to run any locally or globally installed updaters by their [aliases](tasks.md#alias-tasks) or full names. + +**Init** + +If it's your first time using update, run `update init` to set your global defaults. + +**CLI help** + +``` +Usage: update [options] + +Command: Updater or tasks to run + +Examples: + + # run the "foo" updater + $ update foo + + # run the "bar" task on updater "foo" + $ update foo:bar + + # run multiple tasks on updater "foo" + $ update foo:bar,baz,qux + + # run a sub-updater on updater "foo" + $ update foo.abc + + # run task "xyz" on sub-updater "foo.abc" + $ update foo.abc:xyz + + Update attempts to automatically determine if "foo" is a task or updater. + If there is a conflict, you can force update to run updater "foo" + by specifying a task on the updater. Example: `update foo:default` +``` + +## Related + +**Docs** + +* [installing-updaters](installing-updaters.md) diff --git a/docs/installing-updaters.md b/docs/installing-updaters.md new file mode 100644 index 0000000..450bb6a --- /dev/null +++ b/docs/installing-updaters.md @@ -0,0 +1,11 @@ +# Installing updaters + +Updaters are responsible for all of the "updating" that happens in update. You can find updaters to install by [searching npm](https://www.npmjs.com/browse/keyword/update-updater) for packages that have the keyword `update-updater`. + +TODO + +## Related + +**Docs** + +* [installing-the-cli](installing-the-cli.md) diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..3009a36 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/nested-updaters.md b/docs/nested-updaters.md new file mode 100644 index 0000000..ff87484 --- /dev/null +++ b/docs/nested-updaters.md @@ -0,0 +1,49 @@ +# Nested updaters + +Updaters provide a convenient way of wrapping code that should be executed on-demand, whilst also "namespacing" the code being wrapped, and making it available to be executed using a consistent and intuitive syntax by either CLI or API. + +TBC... + +## TODO + +* [ ] explain how nested updaters work +* [ ] command line syntax +* [ ] API syntax + +## Pre-requisites + +* [plugins](api/plugins.md) +* [updaters](updaters.md) + +## Sub-updaters + +As with [plugins](api/plugins.md), updaters may be nested: _any updater can register other updaters, and any updater can be registered by other updaters._ We refer to nested updaters as **sub-updaters**. + +**Example** + +```js +app.register('foo', function(foo) { + // do updater stuff + this.register('bar', function(bar) { + // do updater stuff + this.register('baz', function(baz) { + // do updater stuff + this.task('default', function(cb) { + console.log(baz.namespace); + cb(); + }); + }); + }); +}); +``` + +## Run nested updaters + +Use dot-notation to get the updater you wish to run: + +```js +app.update('foo.bar.baz', function(err) { + if (err) return console.log(err); + +}); +``` diff --git a/docs/symlinking-updaters.md b/docs/symlinking-updaters.md new file mode 100644 index 0000000..3e4347c --- /dev/null +++ b/docs/symlinking-updaters.md @@ -0,0 +1,75 @@ +# Symlinking updaters + +While developing [updaters](updaters.md), you might find it useful to symlink them to global `node_modules` so that Update's CLI will find them and run them, as if they had been installed from npm using `npm install --global`. + +The following example shows you to do this. + +## Example + +**1. Create an updater project** + +Create a new project named `updater-aaa`. You can expedite this using [generate](https://github.com/generate/generate) or Google's Yeoman or however you prefer. + +**2. Add `index.js`** + +In `index.js`, add the following code: + +```js +// -- index.js -- +module.exports = function(app) { + app.task('default', function(cb) { + console.log('updater', app.name, '> task', this.name); + cb(); + }); +}; +``` + +* `app.name` will display the name of the updater being run +* `this.name` will display the name of the task being run + +_(Also make sure the `index.js` is listed in the `main` property in package.json, so that node's `require()` system finds the file)_ + +**3. Symlink** + +Next, we need to symlink the module to global `node_modules`, so that `updater-aaa` is discoverable by Update's CLI. + +From the root of the `updater-aaa` project, run the following command: + +```sh +$ npm link +``` + +**4. Run** + +To test that `updater-aaa` was symlinked properly, run the following command: + +```sh +$ update aaa +``` + +You should see something like the following in the terminal + +```sh +updater updater-aaa > task default +``` + +If not, review the steps and make sure you did everything described. If you still can't get it working please [create an issue](../../../issues) so we can look into it. + +**Next steps** + +If you'd like to see how multiple updaters can work together, repeat the same steps described above to create and symlink `updater-bbb` and `updater-ccc`. + +Then run: + +```sh +update aaa bbb ccc +``` + +## Related + +**Docs** + +* [tasks](tasks.md) +* [updatefile](updatefile.md) +* [installing-updaters](installing-updaters.md) +* [updaters](updaters.md) diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 0000000..9ca80ba --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,180 @@ +# Tasks + +Tasks are used for wrapping code that should be executed at a later point, either when specified by command line or explicitly run when using the API. + +- [Creating tasks](#creating-tasks) +- [Running tasks](#running-tasks) + * [Command line](#command-line) + * [Task API](#task-api) + + [.task](#task) + + [.build](#build) + + [.update](#update) + * [Task composition](#task-composition) + + [Task dependencies](#task-dependencies) + + [Alias tasks](#alias-tasks) + * [default task](#default-task) + +## Creating tasks + +Tasks are asynchronous functions that are registered by name using the `.task` method, and can be run using the `.build` method. + +```js +app.task('foo', function(cb) { + // since tasks are asynchronous, you must call the callback when the task is complete + cb(); +}); +``` + +## Running tasks + +Tasks can be run by command line or API. + +### Command line + +Pass the names of the tasks to run after the `update` command. + +**Examples** + +Run task `foo`: + +```sh +update foo +``` + +Run tasks `foo`, `bar` and `baz`: + +```sh +update foo bar baz +``` + +**Conflict resolution** + +You might notice that [updaters](updaters.md) can also be run from the command line using the same syntax. Update can usually determine whether you meant to call tasks or updaters. Visit the [running updaters](updaters.md#running-updaters) documentation for more information. + +### Task API + +#### .task + +Create a task: + +```js +app.task(name, fn); +``` + +**Params** + +* `name` **{String}**: name of the task to register +* `fn` **{Function}**: asynchronous callback function, or es6 generator function + +**Example** + +```js +app.task('default', function(cb) { + // do task stuff (be sure to call the callback) + cb(); +}); +``` + +**Stream or callback** + +When using update's file system API (`.src`/`.dest` etc), you can optionally return a stream instead of calling a callback. Either a callback must be called, or a stream must be returned, otherwise update has no way of knowing when a task is complete. + +#### .build + +Run one or more tasks. + +**Params** + +* `names` **{String|Array|Function}**: names of one or more tasks to run, or callback function if you only want to run the [default task](#default-task) +* `callback`: callback function, invoked after all tasks have finished executing. The callback function exposes `err` as the only argument, with any errors that occurred during the execution of any tasks. + +**Example** + +```js +app.task('foo', function(cb) { + // do task stuff + cb(); +}); + +app.task('bar', function(cb) { + // do task stuff + cb(); +}); + +app.build(['foo', 'bar'], function(err) { + if (err) return console.log(err); + console.log('done'); +}); +``` + +#### .update + +The `.update` method may also be used to run tasks. However, `.update` can be used to run _tasks and updaters_, thus it will also look for updaters to run when a task is not found. + +_To ensure that only tasks are run, use the `.build` method._ + +See the [updaters documentation](updaters.md) for more details. + +### Task composition + +#### Task dependencies + +When a task has "dependencies", this means that one or more other tasks need to finish before the task is executed. + +Dependencies can be passed as the second argument to the `.task` method. + +**Example** + +In the following example, task `foo` has dependencies `bar` and `baz`: + +```js +app.task('foo', ['bar', 'baz'], function(cb) { + // do task stuff + cb(); +}); +``` + +Task `foo` will not execute until tasks `bar` and `baz` have completed. + +#### Alias tasks + +An "alias" task is a task with one or more dependencies and _no callback_. + +**Example** + +In this example, task `foo` is an alias for tasks `bar` and `baz`: + +```js +app.task('foo', ['bar', 'baz']); +``` + +In this example, task `foo` is an alias for task `baz` + +```js +app.task('foo', ['baz']); +``` + +### default task + +The `default` task is run automatically when a callback is passed as the only argument: + +```js +app.task('default', function(cb) { + // do task stuff + cb(); +}); + +// no need to specify "default", but you can if you want +app.build(function(err) { + if (err) return console.log(err); + console.log('done'); +}); +``` + +## Related + +**Docs** + +* [Running updaters](updaters.md#running-updaters) +* [updaters](updaters.md) +* [updatefile](updatefile.md) diff --git a/docs/updatefile.md b/docs/updatefile.md new file mode 100644 index 0000000..6e878ee --- /dev/null +++ b/docs/updatefile.md @@ -0,0 +1,62 @@ +# Updatefile + +Each time `update` is run, Update's CLI looks for an `updatefile.js` in the current working directory. + +**If `updatefile.js` exists** + +Update's CLI attempts to: + +* Load a local installation of the Update library using node's `require()` system, falling back to global installation if not found. +* Load the configuration from `updatefile.js` using node.js `require()` system +* Register it as the ["default" updater](updaters.md#default-updater) +* Execute any tasks or updaters you've specified for it to run. +* If multiple task or updater names are specified on the command line, Update's CLI will attempt to run all of the specified tasks and updaters. + +**If `updatefile.js` does not exist** + +Update's CLI attempts to: + +* Find any updaters you've specified for it to run by using node's `require()` system to search for locally and globally installed modules with the name `updater-*`. + +## Creating an updatefile.js + +An `updatefile.js` may contain any custom JavaScript code, but must export a function that takes an instance of Update (`app`): + +**Example** + +```js +// -- updatefile.js -- +module.exports = function(app) { + // custom code here +}; +``` + +Inside this function, you can define [tasks](tasks.md), additional [updaters](updaters.md), or any other custom JavaScript code necessary for your updater: + +```js +module.exports = function(app) { + // register a task + app.task('default', function(cb) { + // do task stuff + cb(); + }); + + // register an updater + app.register('foo', function() { + + }); + + // register another updater + app.register('bar', function() { + + }); +}; +``` + +## Related + +**Docs** + +* [installing-updaters](installing-updaters.md) +* [updaters](updaters.md) +* [tasks](tasks.md) diff --git a/docs/updaters.md b/docs/updaters.md new file mode 100644 index 0000000..46c8de2 --- /dev/null +++ b/docs/updaters.md @@ -0,0 +1,246 @@ +# Updaters + +This document describes how to create, register and run updaters. + +- [TODO](#todo) +- [What is an updater?](#what-is-an-updater) +- [Creating updaters](#creating-updaters) +- [Registering updaters](#registering-updaters) + * [.register](#register) + * [.updater](#updater) +- [Running updaters](#running-updaters) +- [Resolving updaters](#resolving-updaters) + * [Tasks and updaters](#tasks-and-updaters) + * [Naming tips](#naming-tips) + * [Order of precendence](#order-of-precendence) +- [Discovering updaters](#discovering-updaters) +- [Default updater](#default-updater) + +## TODO + +* [ ] document updater args +* [ ] explain how the `base` instance works +* [ ] document `env` + +## What is an updater? + +Updaters are [plugins](api/plugins.md) that are registered by name. If you're not familiar with plugins yet, it might be a good idea to review the [plugins docs](api/plugins.md) first. + +The primary difference between "updaters" and "plugins" is how they're registered, but there are a few other minor differences: + +| | **Plugin** | **Updater** | +| --- | --- | --- | +| Registered with | [.use](api/plugins.md#use) method | [.register](#register) method or [.updater](#updater) method | +| Instance | Loaded onto "current" `Update` instance | A `new Update()` instance is created for every updater registered | +| Invoked | Immediately | `.register` deferred (lazy), `.updater` immediately | +| Run using | [.run](api/plugins.md#run): all plugins are run at once | `.update`: only specified plugin(s) are run | + +## Creating updaters + +An updater function takes an instance of `Update` as the first argument. + +**Example** + +```js +function updater(app) { + // do updater stuff +} +``` + +## Registering updaters + +Updaters may be registered using either of the following methods: + +* `.register`: if the plugin should not be invoked until it's called by `.update` (stays lazy while it's cached, this is preferred) +* `.updater`: if the plugin needs to be invoked immediately when registered + +### .register + +Register an updater function with the given `name` using the `.register` method. + +**Example** + +```js +var update = require('update'); +var app = update(); + +function updater(app) { + // do updater stuff when the updater is run with the `.update` method. + console.log('foo is being run'); +} + +// register as an updater with the `.register` method +app.register('foo', updater); + +// run the `foo` updater with the `.update` method +app.update('foo', function(err) { + if (err) return console.log(err); +}); +//=> "foo is being run" +``` + +### .updater + +Register an updater function with the given `name` using the `.updater` method. + +**Example** + +```js +var update = require('update'); +var app = update(); + +function updater(app) { + // do updater stuff when the updater is registered + console.log('foo is being registered'); +} + +// register as an updater using `.updater` +app.updater('foo', updater); +//=> "foo is being registered" +``` + +**Should I use `.updater` or `.register`?** + +In general, it's recommended that you use the `.register` method. In most cases update is smart enough to figure out when to invoke updater functions. + +However, there are always exceptions. If you create custom code and notice that update can't find the information it needs. Try using the `.updater` method to invoke the function when the updater is registered. + +## Running updaters + +Updaters and their tasks can be run by command line or API. + +**Command line** + +To run globally or locally installed `updater-foo`, or an updater named `foo` in `updatefile.js`, run: + +```sh +$ update foo +``` + +**API** + +```js +var update = require('update'); +var app = update(); + +function fn() { + // do updater stuff +} + +// the `.register` method does not invoke the updater +app.register('foo', fn); + +// the `.updater` method invokes the updater immediately +app.updater('bar', fn); + +// run updaters foo and bar in series (both updaters will be invoked) +app.update(['foo', 'bar'], function(err) { + if (err) return console.log(err); +}); +``` + +## Resolving updaters + +Updaters can be published to npm and installed globally or locally. But there is no requirement that updaters must be published. You can also create custom updaters and register using the [.register](#register) or [.updater](#updater) methods. + +This provides a great deal of flexibility, but it also means that we need a strategy for _finding updaters_ when `update` is run from the command line. + +### Tasks and updaters + +1. When both a task and an updater have the same name _on the same instance_, Update will always try to run the task first (this is unlikely to happen unless you intend for it to - there are [reasons to do this](#naming-tips)) + +### Naming tips + +Since the [.build](tasks.md#build) method only runs tasks, you can use this to your advantage by aliasing sub-generators with tasks. + +**Don't do this** + +```js +module.exports = function(app) { + app.register('foo', function(foo) { + foo.task('default', function(cb) { + // do task stuff + cb(); + }); + }); + + // `.build` doesn't run updaters + app.build('foo', function(err) { + if (err) return console.log(err); + }); +}; +``` + +**Do this** + +```js +module.exports = function(app) { + app.register('foo', function(foo) { + foo.task('default', function(cb) { + // do task stuff + cb(); + }); + }); + + // `.update` will run updater `foo` + app.update('foo', function(err) { + if (err) return console.log(err); + }); +}; +``` + +**Or this** + +```js +module.exports = function(app) { + app.register('foo', function(foo) { + foo.task('default', function(cb) { + // do task stuff + cb(); + }); + }); + + app.task('foo', function(cb) { + app.update('foo', cb); + }); + + // `.build` will run task `foo`, which runs updater `foo` + app.build('foo', function(err) { + if (err) return console.log(err); + }); +}; +``` + +### Order of precendence + +When the command line is used, Update's CLI resolves updaters in the following order: + +1. [default updater](#default-updater): attempts to match given names to updaters and tasks registered on the `default` updater +2. built-in updaters: attempts to match given names to Update's [built-in updaters](cli/built-in-updaters.md) +3. locally installed updaters +4. globally installed updaters + +## Discovering updaters + +todo + +## Default updater + +If an updater is registered with the name `default` it will receive special treatment from Update and Update's CLI. More specifically, when Update's CLI looks for updaters or tasks to run, it will search for them on the `default` updater first. + +There is a catch... + +**Registering the "default" updater** + +_The only way to register a `default` updater is by creating an [updatefile.js](updatefile.md) in the current working directory._ + +When used by command line, Update's CLI will then use node's `require()` system to get the function exported by `updatefile.js` and use it as the `default` updater. + +## Related + +**Docs** + +* [tasks](tasks.md) +* [updatefile](updatefile.md) +* [installing-updaters](installing-updaters.md) +* [symlinking-updaters](symlinking-updaters.md) diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..fa32719 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,35 @@ +'use strict'; + +var gulp = require('gulp'); +var mocha = require('gulp-mocha'); +var istanbul = require('gulp-istanbul'); +var unused = require('gulp-unused'); +var eslint = require('gulp-eslint'); + +var lib = ['index.js', 'lib/*.js', 'lib/commands/*.js', 'bin/*.js']; + +gulp.task('coverage', function() { + return gulp.src(lib) + .pipe(istanbul()) + .pipe(istanbul.hookRequire()); +}); + +gulp.task('test', ['coverage'], function() { + return gulp.src('test/*.js') + .pipe(mocha({reporter: 'spec'})) + .pipe(istanbul.writeReports()); +}); + +gulp.task('lint', function() { + return gulp.src(['*.js', 'test/*.js', 'lib/**/*.js']) + .pipe(eslint()) + .pipe(eslint.format()); +}); + +gulp.task('unused', function() { + var keys = Object.keys(require('./lib/utils.js')); + return gulp.src(lib) + .pipe(unused({keys: keys})); +}); + +gulp.task('default', ['test', 'lint']); diff --git a/index.js b/index.js new file mode 100644 index 0000000..2681299 --- /dev/null +++ b/index.js @@ -0,0 +1,256 @@ +/*! + * update + * + * Copyright (c) 2016, Jon Schlinkert. + * Licensed under the MIT License. + */ + +'use strict'; + +var fs = require('fs'); +var os = require('os'); +var path = require('path'); +var Base = require('assemble-core'); +var utils = require('./lib/utils'); +var cli = require('./lib/cli'); + +/** + * Create a update application with `options`. + * + * ```js + * var update = require('update'); + * var app = update(); + * ``` + * @param {Object} `options` Settings to initialize with. + * @api public + */ + +function Update(options) { + if (!(this instanceof Update)) { + return new Update(options); + } + Base.call(this, options); + this.is('update'); + this.initUpdate(this); + this.initDefaults(); +} + +/** + * Inherit `Base` + */ + +Base.extend(Update); + +/** + * Initialize defaults, emit events before and after + */ + +Update.prototype.initUpdate = function() { + Update.emit('update.preInit', this); + Update.plugins(this); + Update.emit('update.postInit', this); +}; + +/** + * Initialize `Update` defaults + */ + +Update.prototype.initDefaults = function() { + this.define('generators', this.generators); + this.define('updater', this.generator); + this.updaters = this.generators; + var self = this; + + this.define('home', function() { + var args = [].slice.call(arguments); + var home = path.resolve(self.options.homedir || os.homedir()); + return path.resolve.apply(path, [home].concat(args)); + }); + + this.option('help', {configname: 'updater', appname: 'update'}); + this.define('update', this.generate); + this.define('getUpdater', function() { + return this.getGenerator.apply(this, arguments); + }); + + this.option('toAlias', function(name) { + return name.replace(/^updater?-(.*)$/, '$1'); + }); + + function isUpdater(name) { + return /^(updater|generate)?-/.test(name); + } + + // create `app.globals` store + Object.defineProperty(this, 'globals', { + configurable: true, + get: function() { + return new utils.Store('generate-globals', { + cwd: utils.resolveDir('~/') + }); + } + }); + + Object.defineProperty(this, 'common', { + configurable: true, + get: function() { + return utils.common; + } + }); + + this.onLoad(/(^|[\\\/])templates[\\\/]/, function(view, next) { + var userDefined = self.home('templates', view.basename); + if (utils.exists(userDefined)) { + view.contents = fs.readFileSync(userDefined); + view.homePath = userDefined; + view.isUserDefined = true; + } + if (/^templates[\\\/]/.test(view.relative)) { + view.path = path.join(self.cwd, view.basename); + } + utils.stripPrefixes(view); + utils.parser.parse(view, next); + }); + + this.option('lookup', function(name) { + var patterns = []; + if (!isUpdater(name)) { + patterns.push(`updater-${name}`); + } + return patterns; + }); + + this.on('unresolved', function(search, app) { + if (!isUpdater(search.name)) return; + var resolved = utils.resolve.file(search.name) || utils.resolve.file(search.name, {cwd: utils.gm}); + if (resolved) { + search.app = app.generator(search.name, require(resolved.path)); + } + }); + + this.on('option', function(key, val) { + if (key === 'dest') { + self.base.cwd = val; + self.cwd = val; + } + }); + + this.on('ask', function(val, key, question, answers) { + val = val || question.default; + if (typeof val === 'undefined') { + question.default = self.common.get(key); + } + }); + + this.on('task:starting', function(event, task) { + if (event && event.app) { + event.app.cwd = self.base.options.dest || self.base.cwd || event.app.cwd; + } + if (task && task.app) { + task.app.cwd = self.base.options.dest || self.base.cwd || task.app.cwd; + } + }); +}; + +/** + * Expose plugins on the constructor to allow other `base` + * apps to use the plugins before instantiating. + */ + +Update.prototype.configfile = function(cwd) { + return utils.configfile(cwd); +}; + +/** + * Get the list of updaters to run + */ + +Update.prototype.getUpdaters = function(names, options) { + if (utils.isObject(names)) { + options = names; + names = []; + } + options = options || {}; + var updaters = this.option('updaters'); + this.addUpdaters(names, options); + if (utils.isEmpty(updaters)) { + updaters = this.pkg.get('update.updaters'); + } + if (utils.isEmpty(updaters)) { + updaters = this.globals.get('updaters'); + } + if (options.remove) { + updaters = utils.remove(updaters, utils.toArray(options.remove)); + } + if (options.add) { + updaters = utils.union([], updaters, utils.toArray(options.add)); + } + return updaters; +}; + +/** + * Get the list of updaters to run + */ + +Update.prototype.addUpdaters = function(names, options) { + options = options || {}; + if (typeof names === 'string') { + names = utils.toArray(names); + } + if (options.config) { + this.pkg.union('update.updaters', names); + } + if (options.global) { + this.globals.union('updaters', names); + } +}; + +/** + * Expose plugins on the constructor to allow other `base` + * apps to use the plugins before instantiating. + */ + +Update.plugins = function(app) { + app.use(utils.logger()); + app.use(utils.generators()); + app.use(utils.store('update')); + app.use(utils.runtimes()); + app.use(utils.questions()); + app.use(utils.loader()); + app.use(utils.config()); + app.use(utils.cli()); +}; + +/** + * Get the updaters or tasks to run from user config + */ + +Update.resolveTasks = function(app, argv) { + var tasks = utils.arrayify(argv._); + if (tasks.length && utils.contains(['help', 'list', 'new', 'default'], tasks)) { + app.enable('silent'); + return tasks; + } + + if (tasks.length && !utils.contains(['help', 'list', 'new', 'default'], tasks)) { + return tasks; + } + + tasks = app.getUpdaters(argv.add, argv); + if (!tasks || !tasks.length) { + return ['init']; + } + return tasks; +}; + +/** + * Expose static `cli` method + */ + +Update.cli = cli; + +/** + * Expose `update` + */ + +module.exports = Update; diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..502793b --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,168 @@ +'use strict'; + +process.ORIGINAL_CWD = process.cwd(); + +var os = require('os'); +var path = require('path'); +var find = require('find-pkg'); +var log = require('log-utils'); +var utils = require('./utils'); +var argv = utils.parseArgs(process.argv.slice(2)); + +module.exports = function(Update, config, cb) { + if (typeof cb !== 'function') { + throw new TypeError('expected a callback function'); + } + if (!config || typeof config !== 'object') { + throw new TypeError('expected config to be an object'); + } + + var opts = utils.extend({}, config, argv); + + function logger() { + if (opts.silent) return; + console.log.apply(console, arguments); + } + + /** + * Resolve `package.json` filepath and use it for `cwd` + */ + + var pkgPath = find.sync(process.cwd()); + var cwd = pkgPath ? path.dirname(pkgPath) : process.cwd(); + + /** + * Set `cwd` + */ + + if (cwd !== process.cwd()) { + logger(log.timestamp, 'using cwd', log.yellow('~' + cwd)); + process.chdir(cwd); + } + + /** + * Get the Base ctor and instance to use + */ + + var base = resolveBase(); + if (!(base instanceof Update)) { + base = new Update(opts); + } + + /** + * Require the config file (instance or function) + */ + + base.set('cache.argv', opts); + + /** + * Check for user `~/update/updatefile.js` + */ + + var homeUpdatefile = path.resolve(os.homedir(), 'update/updatefile.js'); + + /** + * Check for config file + */ + + var defaults = path.resolve(__dirname, 'updatefile.js'); + var updatefile = path.resolve(cwd, 'updatefile.js'); + var fn = require(defaults); + + /** + * Invoke `app` + */ + + if (utils.exists(updatefile)) { + logger(log.timestamp, 'using file', log.green('~' + updatefile)); + var val = require(updatefile); + if (typeof val === 'function') { + function updater() { + if (!utils.isValid(this, 'default-updater')) return; + this.use(fn); + this.use(val); + } + base.register('default', updater); + } else if (val && val.isApp) { + base = val; + } + base.updatefile = true; + } else { + logger(log.timestamp, 'using file', log.green('~' + defaults)); + base.register('default', fn); + } + + /** + * Register updater in user home + */ + + if (utils.exists(homeUpdatefile)) { + base.register('home', require(homeUpdatefile)); + } + + /** + * Handle errors + */ + + if (!base) { + handleError(base, new Error('cannot run config file: ' + updatefile)); + } + if (typeof base !== 'function' && Object.keys(base).length === 0) { + handleError(base, new Error('expected a function or instance of Base to be exported')); + } + + /** + * Merge `argv` onto options + */ + + base.option(argv); + + /** + * Setup listeners + */ + + base.on('error', function(err) { + logger(err.stack); + }); + + base.on('build', function(event, build) { + if (!build || build.isSilent) return; + var prefix = event === 'finished' ? log.success + ' ' : ''; + logger(log.timestamp, event, build.key, prefix + log.red(build.time)); + }); + + base.on('task', function(event, task) { + if (!task || task.isSilent) return; + logger(log.timestamp, event, task.key, log.red(task.time)); + }); + + /** + * Resolve the "app" to use + */ + + function resolveBase() { + var file = {path: path.resolve(cwd, 'node_modules/update'), name: 'update'}; + if (utils.exists(file.path)) { + var Update = require(file.path); + var update = new Update(opts); + + if (typeof update._name === 'undefined') { + update.is('update'); + } + + Update.plugins(update); + return update; + } + } + + function handleError(app, err) { + if (app && app.hasListeners && app.hasListeners('error')) { + app.emit('error', err); + } else { + console.error(err.stack); + process.exit(1); + } + } + + cb(null, base); +}; diff --git a/lib/commands.js b/lib/commands.js new file mode 100644 index 0000000..d53bfef --- /dev/null +++ b/lib/commands.js @@ -0,0 +1,11 @@ +'use strict'; + +var commands = require('./commands/'); + +module.exports = function(app, options) { + for (var key in commands) { + if (commands.hasOwnProperty(key)) { + app.cli.map(key, commands[key](app, options)); + } + } +}; diff --git a/lib/commands/add.js b/lib/commands/add.js new file mode 100644 index 0000000..624901e --- /dev/null +++ b/lib/commands/add.js @@ -0,0 +1,44 @@ +'use strict'; + +var utils = require('../utils'); + +/** + * Remove names from the list of locally or globally stored updaters. + * + * ```sh + * # remove updater `foo` + * $ update -r foo + * # or + * $ update -rc foo + * # sugar for + * $ update --remove --config foo + * # remove globally stored updaters + * $ update -rg foo + * # sugar for + * $ update --remove --global foo + * ``` + * @name tasks + * @api public + * @cli public + */ + +module.exports = function(app, options) { + return function(names, key, config, next) { + var updaters = []; + + if (typeof names === 'string') { + names = utils.toArray(names); + } + + if (!config.config) { + updaters = app.globals.get('updaters') || []; + app.globals.set('updaters', updaters.concat(names)); + + } else { + updaters = app.pkg.get('update.updaters') || []; + app.pkg.union('update.updaters', updaters.concat(names)); + app.pkg.save(); + } + next(); + }; +}; diff --git a/lib/commands/index.js b/lib/commands/index.js new file mode 100644 index 0000000..23b2930 --- /dev/null +++ b/lib/commands/index.js @@ -0,0 +1 @@ +module.exports = require('export-files')(__dirname); diff --git a/lib/commands/remove.js b/lib/commands/remove.js new file mode 100644 index 0000000..392a219 --- /dev/null +++ b/lib/commands/remove.js @@ -0,0 +1,50 @@ +'use strict'; + +var utils = require('../utils'); + +/** + * Remove names from the list of locally or globally stored updaters. + * + * ```sh + * # remove updater `foo` + * $ update -r foo + * # or + * $ update -rc foo + * # sugar for + * $ update --remove --config foo + * # remove globally stored updaters + * $ update -rg foo + * # sugar for + * $ update --remove --global foo + * ``` + * @name tasks + * @api public + * @cli public + */ + +module.exports = function(app, options) { + return function(names, key, config, next) { + if (typeof names === 'string') { + names = utils.toArray(names); + } + + var updaters = names.map(function(name) { + return 'updaters.' + name; + }); + + if (!config.config) { + app.globals.del(updaters); + + } else { + var list = app.pkg.get('update.updaters'); + app.pkg.del('update.updaters'); + + var rest = utils.remove(list, names); + if (rest.length) { + app.pkg.set('update.updaters', rest); + app.pkg.save(); + } + } + next(); + }; +}; diff --git a/lib/commands/silent.js b/lib/commands/silent.js new file mode 100644 index 0000000..3bd0d44 --- /dev/null +++ b/lib/commands/silent.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function(app) { + return function(val, key, config, next) { + app.enable('silent'); + next(); + }; +}; diff --git a/lib/commands/version.js b/lib/commands/version.js new file mode 100644 index 0000000..abc843d --- /dev/null +++ b/lib/commands/version.js @@ -0,0 +1,10 @@ +'use strict'; + +var pkg = require('../../package'); + +module.exports = function(app) { + return function(val, key, config, next) { + console.log(app.log.cyan('Update v' + pkg.version)); + process.exit(); + }; +}; diff --git a/lib/copy.js b/lib/copy.js deleted file mode 100644 index 0f0915f..0000000 --- a/lib/copy.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -var fs = require('fs'); - -module.exports = function copy(src, dest) { - var res = fs.createWriteStream(dest); - fs.createReadStream(src).pipe(res); -}; \ No newline at end of file diff --git a/lib/copyFiles.js b/lib/copyFiles.js deleted file mode 100644 index dfdf33a..0000000 --- a/lib/copyFiles.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var path = require('path'); -var copy = require('./copy'); - -module.exports = function copyFiles(files, dest) { - for (var i = files.length - 1; i >= 0; i--) { - var fp = files[i]; - copy(fp, path.join(dest, path.basename(fp))); - } -}; \ No newline at end of file diff --git a/lib/delete.js b/lib/delete.js deleted file mode 100644 index b1a21ef..0000000 --- a/lib/delete.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var chalk = require('chalk'); -var symbol = require('log-symbols'); -var del = require('delete'); -var bold = chalk.bold; - -module.exports = function _delete(fp, opts) { - if (!fs.existsSync(fp)) { - if (opts && opts.silent !== true) { - console.log(symbol.error, bold(src), 'does not exist.'); - } - } else { - del.sync(fp, opts && opts.force); - console.log(symbol.success, 'deleted', bold(fp)); - } -}; \ No newline at end of file diff --git a/lib/list.js b/lib/list.js new file mode 100644 index 0000000..ac2d96b --- /dev/null +++ b/lib/list.js @@ -0,0 +1,33 @@ +'use strict'; + +var path = require('path'); +var strip = require('strip-color'); +var through = require('through2'); +var table = require('text-table'); + +module.exports = function(app) { + function bold(str) { + return app.log.underline(app.log.bold(str)); + } + + var list = [[bold('version'), bold('name'), bold('alias')]]; + return through.obj(function(file, enc, next) { + var pkgPath = path.resolve(file.path, 'package.json'); + var pkg = require(pkgPath); + list.push([app.log.gray(pkg.version), file.basename, app.log.cyan(file.alias)]); + next(); + }, function(cb) { + console.log(); + console.log(table(list, { + stringLength: function(str) { + return strip(str).length; + } + })); + + console.log(); + console.log(app.log.magenta(list.length + ' updaters installed')); + console.log(); + cb(); + }); +}; + diff --git a/lib/logging.js b/lib/logging.js deleted file mode 100644 index 03786ad..0000000 --- a/lib/logging.js +++ /dev/null @@ -1,26 +0,0 @@ -var chalk = require('chalk'); -var symbol = require('log-symbols'); -var merge = require('merge-deep'); - -module.exports = function (str, options) { - if (typeof str === 'object') { - options = str; - } - - options = merge({}, options); - var log = {}; - - log.success = function (updated, msg, fp) { - if (options.nocompare || updated !== str) { - var text = chalk.gray(msg.trim()) + (fp ? (' ' + fp) : ''); - console.log(' ' + symbol.success + ' ' + text); - } - }; - - log.info = function (msg, fp) { - var text = chalk.gray(msg.trim()) + (fp ? (' ' + fp) : ''); - console.log(' ' + symbol.info + ' ' + text); - }; - - return log; -} \ No newline at end of file diff --git a/lib/mkdir.js b/lib/mkdir.js deleted file mode 100644 index a47ff94..0000000 --- a/lib/mkdir.js +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * update - * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - -'use strict'; - -var fs = require('fs'); -var path = require('path'); - -/** - * Make the given directory and intermediates - * if they don't already exist. - * - * @param {String} `dirpath` - * @param {Number} `mode` - * @return {String} - * @api private - */ - -function mkdir(dir, mode) { - mode = mode || parseInt('0777', 8) & (~process.umask()); - if (!fs.existsSync(dir)) { - var parent = path.dirname(dir); - - if (fs.existsSync(parent)) { - fs.mkdirSync(dir, mode); - } else { - mkdir(parent); - fs.mkdirSync(dir, mode); - } - } -} \ No newline at end of file diff --git a/lib/move.js b/lib/move.js deleted file mode 100644 index d254f87..0000000 --- a/lib/move.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -var path = require('path'); -var rename = require('./rename'); -var mkdir = require('./mkdir'); - -module.exports = function move(src, dest, force) { - var dir = path.dirname(dest); - if (!fs.existsSync(dir)) { - mkdir(dir); - } - rename(src, dest, force); -}; \ No newline at end of file diff --git a/lib/rename.js b/lib/rename.js deleted file mode 100644 index 0653ea2..0000000 --- a/lib/rename.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); -var chalk = require('chalk'); -var symbol = require('log-symbols'); -var bold = chalk.bold; - -for (var key in symbol) { - if (symbol.hasOwnProperty(key)) { - symbol[key] = ' ' + symbol[key] + ' '; - } -} - -module.exports = function rename(src, dest, opts) { - opts = opts || {}; - - if (path.resolve(src) === path.resolve(dest)) { - if (opts.silent !== true) { - console.log(symbol.warning, bold(src), 'and', bold(dest), 'are the same path.'); - } - } else if (!fs.existsSync(src)) { - if (opts.silent !== true) { - console.log(symbol.error, bold(src), 'does not exist.'); - } - } else if (fs.existsSync(dest) && opts.force !== true) { - if (opts.silent !== true) { - console.log(symbol.warning, bold(dest), 'already exists (to force, pass `true` as the last argument).'); - } - } else { - console.log(symbol.success, 'renamed', bold(src), '=>', bold(dest)); - fs.renameSync(src, dest); - } -}; \ No newline at end of file diff --git a/lib/to-stream.js b/lib/to-stream.js deleted file mode 100644 index 19b3375..0000000 --- a/lib/to-stream.js +++ /dev/null @@ -1,9 +0,0 @@ -function toStream(options, cb) { - return through.obj(function (file, enc, cb) { - var str = file.contents.toString(); - - file.contents = new Buffer(str); - this.push(file); - cb(); - }); -} \ No newline at end of file diff --git a/lib/updatefile.js b/lib/updatefile.js new file mode 100644 index 0000000..f12e8ec --- /dev/null +++ b/lib/updatefile.js @@ -0,0 +1,167 @@ +'use strict'; + +var path = require('path'); +var isValid = require('is-valid-app'); +var choose = require('gulp-choose-files'); +var through = require('through2'); +var utils = require('./utils'); +var list = require('./list'); +var Update = require('..'); +var argv = require('yargs-parser')(process.argv.slice(2), utils.opts); + +module.exports = function(app, base) { + if (!isValid(app, 'update-builtins')) return; + var gm = path.resolve.bind(path, require('global-modules')); + var cwd = path.resolve.bind(path, app.cwd); + + /** + * Select the updaters to run every time `update` is run. Use `--add` to + * add additional updaters, and `--remove` to remove them. You can run this command + * whenever you want to update your preferences, like after installing new updaters. + * + * ```sh + * $ update init + * ``` + * @name init + * @api public + */ + + app.task('init', { silent: true }, function() { + var list = Update.resolveTasks(app, argv); + var updaters = []; + + console.log(); + console.log(' Current updaters:', app.log.cyan(list.join(', '))); + console.log(); + + return app.src([gm('updater-*'), cwd('node_modules/updater-*')]) + .pipe(through.obj(function(file, enc, next) { + file.basename = app.toAlias(file.basename); + next(null, file); + })) + .pipe(choose({message: 'Choose the updaters to run with the `update` command:'})) + .pipe(through.obj(function(file, enc, next) { + updaters.push(file.basename); + next(); + }, function(next) { + save(app, updaters); + next(); + })); + }); + + /** + * Display a list of currently installed updaters. + * + * ```sh + * $ update defaults:list + * # aliased as + * $ update list + * ``` + * @name list + * @api public + */ + + app.task('list', { silent: true }, function() { + return app.src([gm('updater-*'), cwd('node_modules/updater-*')]) + .pipe(through.obj(function(file, enc, next) { + file.alias = app.toAlias(file.basename); + next(null, file); + })) + .pipe(list(app)); + }); + + /** + * Display a help [menu](#help-menu) of available commands and flags. + * + * ```sh + * $ update defaults:help + * # aliased as + * $ update help + * ``` + * @name help + * @api public + */ + + app.task('help', { silent: true }, function(cb) { + base.cli.process({ help: true }, cb); + }); + + /** + * Show the list of updaters that are registered to run on the current project. + * + * ```sh + * $ update defaults:show + * # aliased as + * $ update show + * ``` + * @name show + * @api public + */ + + app.task('show', { silent: true }, function(cb) { + argv._ = []; + var list = Update.resolveTasks(app, argv); + console.log(); + console.log(' queued updaters:', `\n · ` + list.join('\n · ')); + console.log(); + cb(); + }); + + /** + * Default task for the built-in `defaults` generator. + * + * ```sh + * $ update help + * ``` + * @name help + * @api public + */ + + app.task('default', { silent: true }, ['help']); +}; + +/** + * Save answers to `init` prompts + */ + +function save(app, list) { + if (!list.length) { + console.log(' no updaters were saved.'); + return; + } + + if (app.options.c || app.options.config) { + app.pkg.set('update.updaters', list); + } else { + app.globals.set('updaters', list); + } + + var suffix = list.length > 1 ? 'updaters are' : 'updater is'; + var gray = app.log.gray; + var cyan = app.log.cyan; + var bold = app.log.bold; + var command = 'update'; + + var msg = gray('\n ---') + + '\n' + + `\n ${app.log.green(app.log.check)} Done, your default ${suffix}:` + + '\n' + + cyan(`\n · ` + list.join('\n · ')) + + '\n' + + '\n' + + gray(' ---') + + '\n' + + '\n' + + bold(' Cheetsheet:') + + '\n' + + '\n' + + (` $ ${command} --init`) + gray(' initialize update (start over)\n') + + (` $ ${command} --remove `) + gray(' remove updaters from your queue\n') + + (` $ ${command} --add `) + gray(' add updaters to your queue\n') + + (` $ ${command} show`) + gray(' show your queued updaters\n') + + (` $ ${command} list`) + gray(' list all installed updaters\n') + + console.log(msg); + console.log(); +} + diff --git a/lib/utils.js b/lib/utils.js index d272617..d4e9b3c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,9 +1,165 @@ 'use strict'; -exports.toString = function toString(file) { - return file && file.contents && file.contents.toString(); +var utils = require('lazy-cache')(require); +var fn = require; +require = utils; // eslint-disable-line + +/** + * Utils + */ + +require('assemble-loader', 'loader'); +require('arr-union', 'union'); +require('base-cli-process', 'cli'); +require('base-config-process', 'config'); +require('base-generators', 'generators'); +require('base-questions', 'questions'); +require('base-runtimes', 'runtimes'); +require('base-store', 'store'); +require('data-store', 'Store'); +require('common-config', 'common'); +require('extend-shallow', 'extend'); +require('fs-exists-sync', 'exists'); +require('isobject', 'isObject'); +require('is-valid-app', 'isValid'); +require('global-modules', 'gm'); +require('log-utils', 'log'); +require('resolve-dir'); +require('resolve-file', 'resolve'); +require('parser-front-matter', 'parser'); +require('yargs-parser', 'parse'); +require = fn; // eslint-disable-line + +utils.stripPrefixes = function(file) { + file.stem = file.stem.replace(/^_/, '.'); + file.stem = file.stem.replace(/^\$/, ''); +}; + +/** + * argv options + */ + +utils.opts = { + boolean: ['diff'], + alias: { + add: 'a', + config: 'c', + configfile: 'f', + diff: 'diffOnly', + global: 'g', + help: 'h', + init: 'i', + silent: 'S', + verbose: 'v', + version: 'V', + remove: 'r' + } +}; + +utils.parseArgs = function(argv) { + var obj = utils.parse(argv, utils.opts); + if (obj.init) { + obj._.push('init'); + delete obj.init; + } + if (obj.help) { + obj._.push('help'); + delete obj.help; + } + return obj; +}; + +utils.remove = function(arr, names) { + return arr.filter(function(ele) { + return names.indexOf(ele) === -1; + }); +}; + +/** + * Return true if any of the given `tasks` are in the specified `list` + */ + +utils.contains = function(list, tasks) { + return utils.arrayify(list).some(function(ele) { + return ~tasks.indexOf(ele); + }); +}; + +/** + * Return true if the given array is empty + */ + +utils.isEmpty = function(val) { + return utils.arrayify(val).length === 0; +}; + +/** + * Cast `val` to an array + */ + +utils.toArray = function(val) { + return typeof val === 'string' ? val.split(',') : val; +}; + +/** + * Cast `val` to an array + */ + +utils.arrayify = function(val) { + return val ? (Array.isArray(val) ? val : [val]) : []; +}; + +/** + * Add logging methods + */ + +utils.logger = function(options) { + return function() { + + function logger(prop, color) { + color = color || 'dim'; + return function(msg) { + var rest = [].slice.call(arguments, 1); + return console.log + .bind(console, utils.log.timestamp + (prop ? (' ' + utils.log[prop]) : '')) + .apply(console, [utils.log[color](msg)].concat(rest)); + }; + }; + + Object.defineProperty(this, 'log', { + configurable: true, + get: function() { + function log() { + return console.log.apply(console, arguments); + } + log.path = function(msg) { + return logger(null, 'dim').apply(null, arguments); + }; + log.time = function(msg) { + return logger(null, 'dim').apply(null, arguments); + }; + log.warn = function(msg) { + return logger('warning', 'yellow').apply(null, arguments); + }; + log.success = function() { + return logger('success', 'green').apply(null, arguments); + }; + + log.info = function() { + return logger('info', 'cyan').apply(null, arguments); + }; + + log.error = function() { + return logger('error', 'red').apply(null, arguments); + }; + log.__proto__ = utils.log; + return log; + } + }); + }; }; +/** + * Expose utils + */ -exports.contains = function contains(file, name) { - return file.path.indexOf(name) !== -1; -}; \ No newline at end of file +module.exports = utils; diff --git a/lib/verbmd.js b/lib/verbmd.js deleted file mode 100644 index 0211566..0000000 --- a/lib/verbmd.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -function fixInstall(str) { - var re = /## Install[\s\n]+{%= include/g; - str = str.replace(re, '{%= include'); - str = str.split('{%= include("install") %}').join('{%= include("install-npm", {save: true}) %}'); - return str; -} - -function fixHelpers(str) { - str = str.split('{%= jscomments').join('{%= apidocs'); - str = str.split('{%= comments').join('{%= apidocs'); - return str; -} - -function fixCopyright(str, year) { - var re = new RegExp('{%= copyright\\(\\) %}', 'g'); - return str.replace(re, '{%= copyright({year: ' + (year || '2015') + '}) %}'); -} - -module.exports = function (str, year) { - // str = fixCopyright(str, year); - str = fixInstall(str); - str = fixHelpers(str); - return str; -}; \ No newline at end of file diff --git a/package.json b/package.json index f2cb051..4d142c8 100644 --- a/package.json +++ b/package.json @@ -1,64 +1,151 @@ { "name": "update", - "description": "Update the year in all files in a project using glob patterns.", - "version": "0.3.2-beta", - "homepage": "https://github.com/jonschlinkert/update", - "author": { - "name": "Jon Schlinkert", - "url": "https://github.com/jonschlinkert" - }, - "repository": { - "type": "git", - "url": "git://github.com/jonschlinkert/update.git" - }, + "description": "Be scalable! Update is a new, open source developer framework and CLI for automating updates of any kind in code projects.", + "version": "0.7.4", + "homepage": "https://github.com/update/update", + "author": "Jon Schlinkert (https://github.com/jonschlinkert)", + "contributors": [ + "Brian Woodward (https://github.com/doowb)", + "Jon Schlinkert (http://twitter.com/jonschlinkert)" + ], + "repository": "update/update", "bugs": { - "url": "https://github.com/jonschlinkert/update/issues" + "url": "https://github.com/update/update/issues" }, - "licenses": [ - { - "type": "MIT", - "url": "https://github.com/jonschlinkert/update/blob/master/LICENSE" - } - ], + "license": "MIT", "files": [ + "bin", "index.js", - "cli.js", - "lib/" + "lib", + "LICENSE", + "README.md" ], "main": "index.js", "preferGlobal": true, "bin": { - "update": "./cli.js" + "update": "bin/update.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=5.0" }, "scripts": { "test": "mocha" }, "dependencies": { - "arr-diff": "^1.0.1", - "chalk": "^0.5.1", - "clone-deep": "^0.1.1", - "delete": "^0.1.5", - "gray-matter": "^1.2.4", - "log-symbols": "^1.0.1", - "merge-deep": "^0.1.5", - "parse-copyright": "^0.4.0", - "sort-object": "^1.0.0", - "through2": "^0.6.3", - "update-banner": "^0.1.0", - "update-license": "^0.3.0", - "update-package": "^0.1.1", - "verb": "^0.4.5" + "arr-union": "^3.1.0", + "assemble-core": "^0.25.0", + "assemble-loader": "^0.6.1", + "base-cli-process": "^0.1.18", + "base-config-process": "^0.1.9", + "base-generators": "^0.4.5", + "base-questions": "^0.7.3", + "base-runtimes": "^0.2.0", + "base-store": "^0.4.4", + "common-config": "^0.1.0", + "data-store": "^0.16.1", + "export-files": "^2.1.1", + "extend-shallow": "^2.0.1", + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0", + "global-modules": "^0.2.2", + "gulp-choose-files": "^0.1.3", + "is-valid-app": "^0.2.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "log-utils": "^0.2.1", + "parser-front-matter": "^1.4.1", + "resolve-dir": "^0.1.0", + "resolve-file": "^0.2.0", + "set-blocking": "^2.0.0", + "strip-color": "^0.1.0", + "text-table": "^0.2.0", + "through2": "^2.0.1", + "yargs-parser": "^2.4.1" }, "devDependencies": { - "del": "^1.1.1" + "base-runner": "^0.8.2", + "base-test-runner": "^0.2.0", + "base-test-suite": "^0.1.12", + "cross-spawn": "^4.0.0", + "generate-foo": "^0.1.5", + "graceful-fs": "^4.1.4", + "gulp": "^3.9.1", + "gulp-eslint": "^3.0.1", + "gulp-format-md": "^0.1.9", + "gulp-istanbul": "^1.0.0", + "gulp-mocha": "^2.2.0", + "gulp-unused": "^0.1.2", + "helper-changelog": "^0.3.0", + "is-absolute": "^0.2.5", + "load-pkg": "^3.0.1", + "mocha": "^2.5.3", + "npm-install-global": "^0.1.2", + "resolve": "^1.1.7", + "should": "^9.0.2", + "sinon": "^1.17.4", + "updater-example": "^0.1.2" }, "keywords": [ - "javascript", - "keys", - "object", - "sort" - ] + "convention", + "dev", + "develop", + "fix", + "lint", + "maintain", + "manage", + "standard", + "tools", + "up-to-date", + "update" + ], + "update": { + "run": true, + "add": [ + "keywords" + ] + }, + "verb": { + "run": true, + "toc": true, + "layout": "app", + "tasks": [ + "readme" + ], + "plugins": [ + "gulp-format-md" + ], + "helpers": [ + "helper-changelog" + ], + "related": { + "description": "Update shares a common architecture and plugin ecosystem with the following libraries:", + "list": [ + "assemble", + "base", + "generate", + "verb" + ] + }, + "reflinks": [ + "assemble", + "assemble-core", + "assemble-loader", + "base", + "consolidate", + "generate", + "gulp", + "handlebars", + "helper-changelog", + "lodash", + "pug", + "swig", + "templates", + "updater-example", + "verb", + "vinyl" + ], + "lint": { + "reflinks": true + } + } } diff --git a/plugins/banners.js b/plugins/banners.js deleted file mode 100644 index c83725f..0000000 --- a/plugins/banners.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var through = require('through2'); -var parse = require('parse-copyright'); -var banner = require('update-banner'); -var merge = require('merge-deep'); -var logger = require('../lib/logging'); - -module.exports = function (verb) { - return function bannersPlugin(options) { - var opts = merge({}, options); - - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - var str = file.contents.toString(); - var log = logger(str); - - if (/^\/\*[!*]/.test(str.trim()) || opts.banner) { - var copyright = parse(str); - if (copyright && copyright.length) { - file.data.copyright = copyright[0]; - } - str = banner(str, file.data); - log.success(str, 'updated banners in', file.relative); - } - - file.contents = new Buffer(str); - this.push(file); - cb(); - }); - }; -}; diff --git a/plugins/dotfiles.js b/plugins/dotfiles.js deleted file mode 100644 index baee580..0000000 --- a/plugins/dotfiles.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var through = require('through2'); -var utils = require('../lib/utils'); -var logger = require('../lib/logging'); - -module.exports = function dotfilesPlugin() { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - var str = file.contents.toString(); - var log = logger(str); - - if (utils.contains(file, '.gitattributes')) { - str = [ - '# Enforce Unix newlines', - '*.* text eol=lf', - '', - '# binaries', - '*.jpg binary', - '*.gif binary', - '*.png binary', - '*.jpeg binary' - ].join('\n'); - log.success(str, 'updated patterns in', file.relative); - } - - file.contents = new Buffer(str); - this.push(file); - cb(); - }); -}; - diff --git a/plugins/gitignore.js b/plugins/gitignore.js deleted file mode 100644 index caa2101..0000000 --- a/plugins/gitignore.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var through = require('through2'); -var utils = require('../lib/utils'); -var logger = require('../lib/logging'); - -module.exports = function dotfilesPlugin() { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - var str = file.contents.toString(); - console.log(str) - var log = logger(str); - - // if (utils.contains(file, '.gitattributes')) { - // str = '*.* text'; - // log.success(str, 'updated patterns in', file.relative); - // } - - file.contents = new Buffer(str); - this.push(file); - cb(); - }); -}; diff --git a/plugins/index.js b/plugins/index.js deleted file mode 100644 index 942fd0f..0000000 --- a/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * update - * - * Copyright (c) 2014-2015, Jon Schlinkert. - * Licensed under the MIT license. - */ - -'use strict'; - -module.exports = function (verb) { - return { - banners : require('./banners.js')(verb), - dotfiles : require('./dotfiles.js'), - index : require('./index.js'), - gitignore : require('./gitignore.js'), - jshint : require('./jshint.js'), - license : require('./license.js'), - pkg : require('./pkg.js')(verb), - tests : require('./tests.js'), - verbmd : require('./verbmd.js')(verb) - }; -}; diff --git a/plugins/jshint.js b/plugins/jshint.js deleted file mode 100644 index c04c69b..0000000 --- a/plugins/jshint.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -var through = require('through2'); -var merge = require('merge-deep'); -var sortObj = require('sort-object'); -var logger = require('../lib/logging'); - -module.exports = function jshintPlugin() { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - var obj = JSON.parse(file.contents.toString()); - var log = logger({nocompare: true}); - - delete obj.globals; - - obj = sortObj(merge(obj, { - "asi": false, - "boss": true, - "curly": true, - "eqeqeq": true, - "eqnull": true, - "esnext": true, - "immed": true, - "latedef": false, - "laxcomma": false, - "mocha": true, - "newcap": true, - "noarg": true, - "node": true, - "sub": true, - "undef": true, - "unused": true - })); - - file.contents = new Buffer(JSON.stringify(obj, null, 2)); - log.success(true, 'updated properties in', file.relative); - this.push(file); - cb(); - }); -}; diff --git a/plugins/license.js b/plugins/license.js deleted file mode 100644 index a96a836..0000000 --- a/plugins/license.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var through = require('through2'); -var update = require('update-license'); -var utils = require('../lib/utils'); -var logger = require('../lib/logging'); - -module.exports = function license() { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - var str = file.contents.toString(); - var log = logger(str); - - if (utils.contains(file, 'LICENSE')) { - str = update(str); - log.success(str, 'updated patterns in', file.relative); - } - - file.contents = new Buffer(str); - this.push(file); - cb(); - }); -}; diff --git a/plugins/pkg.js b/plugins/pkg.js deleted file mode 100644 index 98fa8a2..0000000 --- a/plugins/pkg.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -var path = require('path'); -var diff = require('arr-diff'); -var clone = require('clone-deep'); -var through = require('through2'); -var sortObj = require('sort-object'); -var update = require('update-package'); -var logger = require('../lib/logging'); - -module.exports = function pkgPlugin(verb) { - return function() { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer() || path.basename(file.path) !== 'package.json') { - this.push(file); - return cb(); - } - - try { - verb.cache.data = verb.cache.data || {}; - var licenses = verb.cache.data.licenses; - var license = verb.cache.data.license; - - if (Array.isArray(licenses)) { - licenses = [fixLicense(licenses[0])]; - } else if (typeof license === 'object') { - licenses = [fixLicense(license)]; - } - - var log = logger({nocompare: true}); - var str = file.contents.toString(); - var obj = JSON.parse(str); - - // run updates on package.json fields - var pkg = update(clone(obj)); - - var defaults = [ - 'name', - 'description', - 'version', - 'homepage', - 'author', - 'maintainers', - 'repository', - 'bugs', - 'license', - 'licenses', - 'files', - 'main', - 'private', - 'preferGlobal', - 'bin', - 'engines', - 'scripts', - 'dependencies', - 'devDependencies', - 'keywords' - ]; - - var keys = diff(Object.keys(pkg), defaults); - var res = sortObj(pkg, defaults.concat(keys)); - res.licenses = licenses; - delete res.license; - - if (res.scripts && res.scripts.test && /mocha -r/i.test(res.scripts.test)) { - res.scripts.test = 'mocha'; - } - - file.contents = new Buffer(JSON.stringify(res, null, 2)); - log.success(null, 'updated properties in', file.relative); - } catch (err) { - console.log('plugin:pkg', err); - } - - this.push(file); - cb(); - }); - }; -}; - -function fixLicense(license) { - if (license && license.url && license.url.indexOf('LICENSE-MIT') !== -1) { - license.url = license.url.split('LICENSE-MIT').join('LICENSE'); - } - return license; -} diff --git a/plugins/tests.js b/plugins/tests.js deleted file mode 100644 index d8ccc67..0000000 --- a/plugins/tests.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -var through = require('through2'); -var logger = require('../lib/logging'); - -module.exports = function testsPlugin() { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - var str = file.contents.toString(); - var log = logger(str); - - if (str.indexOf('var should') !== -1) { - str = fixShould(str, file.relative); - log.success(str, 'updated "should" statements in', file.relative); - } - - file.contents = new Buffer(str); - this.push(file); - cb(); - }); -}; - -function fixShould(str) { - var segs = str.split('var should = require(\'should\');'); - str = segs.join('require(\'should\');'); - return str; -} diff --git a/plugins/verbmd.js b/plugins/verbmd.js deleted file mode 100644 index dd3a201..0000000 --- a/plugins/verbmd.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var matter = require('gray-matter'); -var through = require('through2'); -var verbmd = require('../lib/verbmd'); -var logger = require('../lib/logging'); - -module.exports = function verbmdPlugin(verb) { - return function () { - return through.obj(function (file, enc, cb) { - if (file.isNull() || !file.isBuffer()) { - this.push(file); - return cb(); - } - - if (file.path.indexOf('verbfile')) { - var str = file.contents.toString(); - var log = logger(str); - - var obj = matter(str); - var keys = Object.keys(obj.data); - - if (keys.length && obj.data.hasOwnProperty('tags')) { - str = obj.content.replace(/^\s+/, ''); - log.success(str, 'stripped front-matter in', file.relative); - } - - str = verbmd(str); - - log.success(str, 'updated helpers in', file.relative); - file.contents = new Buffer(str); - } - - this.push(file); - cb(); - }); - }; -}; diff --git a/support/assemblefile.js b/support/assemblefile.js new file mode 100644 index 0000000..44c2c13 --- /dev/null +++ b/support/assemblefile.js @@ -0,0 +1,31 @@ +'use strict'; + +var path = require('path'); +var generators = require('base-generators'); +var paths = require('./lib/paths'); +var lib = require('./lib'); + +module.exports = function(app) { + var dest = paths.site(); + app.use(generators()); + app.use(lib.common()); + app.register('verb', require('./verbfile')); + + app.task('verb', function(cb) { + app.generate('verb', cb); + }); + + app.task('default', ['verb'], function() { + app.layouts(paths.tmpl('layouts/*.hbs')); + app.includes(paths.tmpl('includes/*.hbs')); + app.pages(paths.docs('**/*.md')); + return app.toStream('pages') + .pipe(lib.plugins.buildPaths(dest)) + .pipe(lib.plugins.lintPaths(dest)) + .pipe(app.renderFile()) + .pipe(app.dest(function(file) { + // file.path = path.join(file.dirname, file.stem, 'index.html'); + return dest; + })); + }); +}; diff --git a/support/docs/api.plugins.md b/support/docs/api.plugins.md new file mode 100644 index 0000000..d6558c4 --- /dev/null +++ b/support/docs/api.plugins.md @@ -0,0 +1,63 @@ +--- +title: Plugins +related: + api: ['updater', 'register'] + doc: [] +--- + +A plugin is function that takes an instance of `Update` and is registered with the `.use` method. See the [base-plugins][] documentation for additional details. + +### .use + +The `.use` method is used for registering plugins that should be immediately invoked. + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +function plugin(app) { + // "app" and "this" both expose the instance of update we created above +} + +app.use(plugin); +``` + +Once a plugin is invoked, it will not be called again. + +### .run + +If a plugin returns a function after it's invoked by `.use`, the function will be pushed onto an array allowing it to be called again by the `.run` method. + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +function plugin(app) { + // "app" and "this" both expose the instance of update we created above + return plugin; +} + +app.use(plugin); +``` + +We can now run all plugins that were pushed onto the `.fns` array on any arbitrary object: + +```js +var obj = {}; +app.run(obj); +``` + +Additionally: + +* If `obj` has a `.use` method, it will be used on each plugin (e.g. `obj.use(fn)`). Otherwise `fn(obj)`. +* If the plugin returns a function again and `obj` has a `.run` method, the plugin will be pushed onto the `obj.fns` array. + +This can continue indefinitely as long as the plugin returns a function and the receiving object has `.use`/`.run` functions. + +## Updaters + +When plugins are [registered by name](docs/updaters.md), they are referred to as "updaters". See the [updater documentation](docs/updaters.md) for more details. diff --git a/support/docs/api.register.md b/support/docs/api.register.md new file mode 100644 index 0000000..d704e12 --- /dev/null +++ b/support/docs/api.register.md @@ -0,0 +1,32 @@ +--- +title: Register +related: + api: ['updater', 'plugins'] +--- + +Register an updater function by name. Similar to [.updater](updater.md) but does not invoke the updater function. + +```js +app.register(name, fn); +``` + +**Params** + +* `name` **{String}**: name of the updater to register +* `fn` **{Function}**: updater function + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +// not invoked until called by `.update` +app.register('foo', function(app) { + // do updater stuff +}); + +app.update('foo', function(err) { + if (err) return console.log(err); +}); +``` diff --git a/support/docs/api.updater.md b/support/docs/api.updater.md new file mode 100644 index 0000000..287e16d --- /dev/null +++ b/support/docs/api.updater.md @@ -0,0 +1,34 @@ +--- +title: Updater +related: + cli: ['commands'] + api: ['register', 'plugins'] + doc: ['faq'] +--- + +Register an updater function by name. Similar to [.register](register.md) but immediately invokes the updater function upon registering it. + +```js +app.updater(name, fn); +``` + +**Params** + +* `name` **{String}**: name of the updater to register +* `updater` **{Function}**: updater function + +**Example** + +```js +var Update = require('update'); +var app = new Update(); + +// immediately invoked +app.updater('bar', function(app) { + // do updater stuff +}); + +app.update('bar', function(err) { + if (err) return console.log(err); +}); +``` diff --git a/support/docs/cli.built-in-tasks.md b/support/docs/cli.built-in-tasks.md new file mode 100644 index 0000000..56d5858 --- /dev/null +++ b/support/docs/cli.built-in-tasks.md @@ -0,0 +1,55 @@ +--- +title: Built in tasks +related: + doc: [] +--- + +Update only has a few built-in [tasks](docs/tasks.md) (these might be externalized at some point): + +* [init](#init): Choose the updaters to run by default each time `update` is run from the command line +* [list](#list): List all globally and locally installed updaters +* [show](#show): show the list of updaters that will run on the current project when the `update` command is given +* [new](#new): create a new `updatefile.js` in the current working directory +* [help](#help): show a help menu with all available commands + +## Usage + +### init + +Prompts you to choose one or more "default updaters" to run automatically each time to `update` command is given: + +```sh +$ update init +``` + +### list + +List all globally and locally installed updaters: + +```sh +$ update list +``` + +### show + +Show the list of updaters that will run on the current project when the `update` command is given: + +```sh +$ update show +``` + +### new + +Create a new `updatefile.js` in the current working directory: + +```sh +$ update new +``` + +### help + +Display a help menu with all available commands: + +```sh +$ update help +``` diff --git a/support/docs/cli.commands.md b/support/docs/cli.commands.md new file mode 100644 index 0000000..8dc6d92 --- /dev/null +++ b/support/docs/cli.commands.md @@ -0,0 +1,51 @@ +--- +title: Command line flags +related: + docs: [''] +--- + +Supported command line flags. + +## --run + +By default, when an `updatefile.js` exists in the current working directory, the `update` command will only run explicitly specified tasks or, if no tasks are explicitly defined, the `default` task in `updatefile.js`. + +The `--run` flag forces `update` to run stored tasks and the `default` task or explicitly specified tasks in `updatefile.js`. Stored tasks are executed first, in the order defined, then the `default` task or explicitly defined tasks. + +**Example** + +```sh +$ update --run +``` + +## --help + +See a help menu in the terminal: + +```sh +Usage: update [options] + +Command: Updater or tasks to run + +Examples: + + # run the "foo" updater + $ update foo + + # run the "bar" task on updater "foo" + $ update foo:bar + + # run multiple tasks on updater "foo" + $ update foo:bar,baz,qux + + # run a sub-updater on updater "foo" + $ update foo.abc + + # run task "xyz" on sub-updater "foo.abc" + $ update foo.abc:xyz + + Update attempts to automatically determine if "foo" is a task or updater. + If there is a conflict, you can force update to run updater "foo" + by specifying a task on the updater. Example: `update foo:default` +``` + diff --git a/support/docs/faq.md b/support/docs/faq.md new file mode 100644 index 0000000..8eab822 --- /dev/null +++ b/support/docs/faq.md @@ -0,0 +1,21 @@ +--- +title: Faq +related: + doc: ['features'] +--- + +## How is the name written? + +Update, with a capital "U", the rest lowercase. `updatefile.js` is all lowercase, no capital letters. + + + +## Aliases + +**What's an updater's alias, and what do they do?** + +Update tries to find globally installed updaters using an "alias" first, falling back on the updater's full name if not found by its alias. + +An updater's alias is created by stripping the substring `updater-` from the _full name_ of updater. Thus, when publishing an updater the naming convention `updater-foo` should be used (where `foo` is the alias, and `updater-foo` is the full name). + +Note that **no dots may be used in published updater names**. Aside from that, any characters considered valid by npm are fine. diff --git a/support/docs/features.md b/support/docs/features.md new file mode 100644 index 0000000..86780be --- /dev/null +++ b/support/docs/features.md @@ -0,0 +1,22 @@ +--- +title: Features +related: + doc: ['faq'] +--- + +Update offers an elegant and robust suite of methods, carefully organized to help you accomplish common activities in less time, including: + +* **unparalleled flow control**: through the use of [updaters][getting-started], [sub-updaters][getting-started] and [tasks][getting-started] +* **templates, scaffolds and boilerplates**: update a single file, initialize an entire project, or provide ad-hoc "components" throughout the duration of a project using any combination of [templates, scaffolds and boilerplates](#templates-scaffolds-and-boilerplates) +* **any engine**: use any template engine to render templates, including [handlebars][], [lodash][], [swig][] and [pug][] +* **prompts**: asks you for data when it can't find what it needs, and it's easy to customize prompts for any data you want +* **data**: gathers data from the user's environment to populate "hints" in user prompts and to use when rendering templates +* **streams**: interact with the file system, with full support for [gulp][] and [assemble][] plugins +* **smart plugins**: Update is built on [base][], so any "smart" plugin can be used +* **stores**: persist configuration settings, global defaults, project-specific defaults, answers to prompts, and so on + +Visit the [getting started guide][getting-started] to learn more. + + + +[getting-started]: https://github.com/update/getting-started diff --git a/support/docs/installing-the-cli.md b/support/docs/installing-the-cli.md new file mode 100644 index 0000000..b65bad9 --- /dev/null +++ b/support/docs/installing-the-cli.md @@ -0,0 +1,15 @@ +--- +title: Installing the cli +related: + doc: ['installing-updaters'] +--- + +To run update from the command line, you'll need to install Update's CLI globally first. You can do that now with the following command: + +```sh +$ npm install --global update +``` + +This adds the `update` command to your system path, allowing it to be run from any directory. + +You should now be able to use the `update` command to execute code in a local `updatefile.js` file, or to run any locally or globally installed updaters by their [aliases](tasks.md#alias-tasks) or full names. diff --git a/support/docs/installing-updaters.md b/support/docs/installing-updaters.md new file mode 100644 index 0000000..cd053ba --- /dev/null +++ b/support/docs/installing-updaters.md @@ -0,0 +1,9 @@ +--- +title: Installing updaters +related: + doc: ['installing-the-cli'] +--- + +Updaters are responsible for all of the "updating" that happens in update. You can find updaters to install by [searching npm](https://www.npmjs.com/browse/keyword/update-updater) for packages that have the keyword `update-updater`. + +TODO diff --git a/support/docs/introduction.md b/support/docs/introduction.md new file mode 100644 index 0000000..0de03da --- /dev/null +++ b/support/docs/introduction.md @@ -0,0 +1,89 @@ +--- +title: Introduction +draft: true +related: + doc: ['updaters', 'updatefile', 'tasks', 'features', 'faq'] +--- + + + +## What is update? + +Update is a new, open-source developer framework for automating updates of any kind in code projects. + +* normalize configuration settings, verbiage, or preferences across all of your projects +* update files that are typically excluded from the automated parts of the software lifecycle, and are often forgotten about after they're created. +* fix dates in copyrights, licenses and banners +* removing deprecated fields from project manifests +* updating settings in runtime config files, preferences in dotfiles, and so on. + +## How does it work? + +Update's API has methods for [creating](#creating-updaters), [registering](#registering-updaters), [resolving](#resolving-updaters) and [running](#running-updaters) updaters. + +### Who should use Update? + +* developers or organizations with many projects under their stewardship +* agencies or consultants who maintain and/or create client projects and would like to reduce time spent on maintainance +* anyone who cares about having consistency across all of their projects + +## Updaters + +All "updates" are accomplished using plugins called [updaters](#updaters). + +**What are updaters?** + +- Updaters are functions that are registered by name, and can be run by [command line](#command-line) or [API](#api). +- Updaters may be published to [npm](https://www.npmjs.com) using the `updater-foo` naming convention, where `foo` is the [alias](#aliases) of your updater. +- Published updaters can be installed locally or globally. + +## Command line + +### Running updaters + +To run updaters by command line, pass the [aliases](#aliases) or full [npm](https://www.npmjs.com) package names (if published) of the updaters to run after the `update` command. + +**Example** + +Run updaters `foo`, `bar` and `baz` in series: + +```sh +$ update foo bar baz +# or +$ update updater-foo updater-bar updater-baz +``` + +Note that _updaters are run in series_, so given the previous example, updater `bar` will not run until updater `foo` is completely finished executing. + +### Aliases + +Get the alias of an updater by removing the `updater-` substring from the begining of the full name. + +## Resolving updaters + +When run by command line, Update's CLI will attempt to find and run updaters matching the names you've given, by first searching in the local `updatefile.js`, then using node's `require()` system to find locally installed updaters, and last by searching for globally installed updaters. + +If any of the updaters specified is not found, _an error is thrown and the process will exit_. + +## API + +by passing the names of updaters to run to the `.update` + +## Update + +Updaters via CLI or API. (tasks are powered by [bach][], the same library used in [gulp][] v4.0). + +The main export of the library is the `Update` constructor function. + +Updaters themselves are just functions that take an instance of `Update`. + +Update gives you a way to automate the maintenance of files that are typically excluded from the automated parts of the software lifecycle, and thus are mostly forgotten about after they're created. + +For example, if we were to sift the files in the average code project into major generic buckets we would end up with something like this: + +* **code**: the actual source code of the project (compiled, lib, src, and so on) +* **dist**: the "deliverable" of the project (this could be HTML, CSS, minified JavaScript, or something similar for non-web projects) +* **docs**: documentation for the project +* **everything else**: LICENSE and copyright files, dotfiles, manifests, config files, and so on. + +Update maintains **everything else**. diff --git a/support/docs/layouts/default.md b/support/docs/layouts/default.md new file mode 100644 index 0000000..a06e910 --- /dev/null +++ b/support/docs/layouts/default.md @@ -0,0 +1,5 @@ +# <%= title %> + +{% body %} + +<%= relatedLinks(related) %> diff --git a/support/docs/nested-updaters.md b/support/docs/nested-updaters.md new file mode 100644 index 0000000..1006132 --- /dev/null +++ b/support/docs/nested-updaters.md @@ -0,0 +1,53 @@ +--- +title: Nested updaters +--- + +Updaters provide a convenient way of wrapping code that should be executed on-demand, whilst also "namespacing" the code being wrapped, and making it available to be executed using a consistent and intuitive syntax by either CLI or API. + + +TBC... + + +## TODO + +- [ ] explain how nested updaters work +- [ ] command line syntax +- [ ] API syntax + +## Pre-requisites + +- [plugins](api/plugins.md) +- [updaters](updaters.md) + +## Sub-updaters + +As with [plugins](api/plugins.md), updaters may be nested: _any updater can register other updaters, and any updater can be registered by other updaters._ We refer to nested updaters as **sub-updaters**. + +**Example** + +```js +app.register('foo', function(foo) { + // do updater stuff + this.register('bar', function(bar) { + // do updater stuff + this.register('baz', function(baz) { + // do updater stuff + this.task('default', function(cb) { + console.log(baz.namespace); + cb(); + }); + }); + }); +}); +``` + +## Run nested updaters + +Use dot-notation to get the updater you wish to run: + +```js +app.update('foo.bar.baz', function(err) { + if (err) return console.log(err); + +}); +``` diff --git a/support/docs/redirects.json b/support/docs/redirects.json new file mode 100644 index 0000000..63b9ef9 --- /dev/null +++ b/support/docs/redirects.json @@ -0,0 +1,3 @@ +{ + "old": "new" +} diff --git a/support/docs/symlinking-updaters.md b/support/docs/symlinking-updaters.md new file mode 100644 index 0000000..aca2717 --- /dev/null +++ b/support/docs/symlinking-updaters.md @@ -0,0 +1,71 @@ +--- +title: Symlinking updaters +related: + doc: ['tasks', 'updatefile', 'installing-updaters', 'updaters'] +--- + +While developing [updaters](updaters.md), you might find it useful to symlink them to global `node_modules` so that Update's CLI will find them and run them, as if they had been installed from npm using `npm install --global`. + +The following example shows you to do this. + +## Example + +**1. Create an updater project** + +Create a new project named `updater-aaa`. You can expedite this using [generate][] or Google's Yeoman or however you prefer. + +**2. Add `index.js`** + +In `index.js`, add the following code: + +```js +// -- index.js -- +module.exports = function(app) { + app.task('default', function(cb) { + console.log('updater', app.name, '> task', this.name); + cb(); + }); +}; +``` + +- `app.name` will display the name of the updater being run +- `this.name` will display the name of the task being run + +_(Also make sure the `index.js` is listed in the `main` property in package.json, so that node's `require()` system finds the file)_ + +**3. Symlink** + +Next, we need to symlink the module to global `node_modules`, so that `updater-aaa` is discoverable by Update's CLI. + +From the root of the `updater-aaa` project, run the following command: + +```sh +$ npm link +``` + +**4. Run** + +To test that `updater-aaa` was symlinked properly, run the following command: + +```sh +$ update aaa +``` + +You should see something like the following in the terminal + +```sh +updater updater-aaa > task default +``` + +If not, review the steps and make sure you did everything described. If you still can't get it working please [create an issue](../../../issues) so we can look into it. + +**Next steps** + +If you'd like to see how multiple updaters can work together, repeat the same steps described above to create and symlink `updater-bbb` and `updater-ccc`. + +Then run: + +```sh +update aaa bbb ccc +``` + diff --git a/support/docs/tasks.md b/support/docs/tasks.md new file mode 100644 index 0000000..1392a01 --- /dev/null +++ b/support/docs/tasks.md @@ -0,0 +1,171 @@ +--- +title: Tasks +related: + doc: + - link: updaters + title: Running updaters + anchor: '#running-updaters' + - updaters + - updatefile +--- + +Tasks are used for wrapping code that should be executed at a later point, either when specified by command line or explicitly run when using the API. + + + +## Creating tasks + +Tasks are asynchronous functions that are registered by name using the `.task` method, and can be run using the `.build` method. + +```js +app.task('foo', function(cb) { + // since tasks are asynchronous, you must call the callback when the task is complete + cb(); +}); +``` + +## Running tasks + +Tasks can be run by command line or API. + +### Command line + +Pass the names of the tasks to run after the `update` command. + +**Examples** + +Run task `foo`: + +```sh +update foo +``` + +Run tasks `foo`, `bar` and `baz`: + +```sh +update foo bar baz +``` + +**Conflict resolution** + +You might notice that [updaters](updaters.md) can also be run from the command line using the same syntax. Update can usually determine whether you meant to call tasks or updaters. Visit the [running updaters](updaters.md#running-updaters) documentation for more information. + +### Task API + +#### .task + +Create a task: + +```js +app.task(name, fn); +``` + +**Params** + +* `name` **{String}**: name of the task to register +* `fn` **{Function}**: asynchronous callback function, or es6 generator function + +**Example** + +```js +app.task('default', function(cb) { + // do task stuff (be sure to call the callback) + cb(); +}); +``` + +**Stream or callback** + +When using update's file system API (`.src`/`.dest` etc), you can optionally return a stream instead of calling a callback. Either a callback must be called, or a stream must be returned, otherwise update has no way of knowing when a task is complete. + +#### .build + +Run one or more tasks. + +**Params** + +* `names` **{String|Array|Function}**: names of one or more tasks to run, or callback function if you only want to run the [default task](#default-task) +* `callback`: callback function, invoked after all tasks have finished executing. The callback function exposes `err` as the only argument, with any errors that occurred during the execution of any tasks. + +**Example** + +```js +app.task('foo', function(cb) { + // do task stuff + cb(); +}); + +app.task('bar', function(cb) { + // do task stuff + cb(); +}); + +app.build(['foo', 'bar'], function(err) { + if (err) return console.log(err); + console.log('done'); +}); +``` + +#### .update + +The `.update` method may also be used to run tasks. However, `.update` can be used to run _tasks and updaters_, thus it will also look for updaters to run when a task is not found. + +_To ensure that only tasks are run, use the `.build` method._ + +See the [updaters documentation](updaters.md) for more details. + +### Task composition + +#### Task dependencies + +When a task has "dependencies", this means that one or more other tasks need to finish before the task is executed. + +Dependencies can be passed as the second argument to the `.task` method. + +**Example** + +In the following example, task `foo` has dependencies `bar` and `baz`: + +```js +app.task('foo', ['bar', 'baz'], function(cb) { + // do task stuff + cb(); +}); +``` + +Task `foo` will not execute until tasks `bar` and `baz` have completed. + +#### Alias tasks + +An "alias" task is a task with one or more dependencies and _no callback_. + +**Example** + +In this example, task `foo` is an alias for tasks `bar` and `baz`: + +```js +app.task('foo', ['bar', 'baz']); +``` + +In this example, task `foo` is an alias for task `baz` + +```js +app.task('foo', ['baz']); +``` + +### default task + +The `default` task is run automatically when a callback is passed as the only argument: + +```js +app.task('default', function(cb) { + // do task stuff + cb(); +}); + +// no need to specify "default", but you can if you want +app.build(function(err) { + if (err) return console.log(err); + console.log('done'); +}); +``` diff --git a/support/docs/tutorial.md b/support/docs/tutorial.md new file mode 100644 index 0000000..e3cdcd9 --- /dev/null +++ b/support/docs/tutorial.md @@ -0,0 +1,116 @@ +--- +title: Tutorial +draft: true +related: + doc: [] +--- + +The following intro only skims the surface of what update has to offer. For a more in-depth introduction, we highly recommend visiting the [getting started guide][getting-started]. + +**Create an updater** + +Add a `updatefile.js` to the current working directory with the following code: + +```js +module.exports = function(app) { + console.log('success!'); +}; +``` + +**Run an updater** + +Enter the following command: + +```sh +update +``` + +If successful, you should see `success!` in the terminal. + +**Create a task** + +Now, add a task to your updater. + +```js +module.exports = function(app) { + app.task('default', function(cb) { + console.log('success!'); + cb(); + }); +}; +``` + +Now, in the command line, run: + +```sh +$ update +# then try +$ update default +``` + +When a local `updatefile.js` exists, the `update` command is aliased to automatically run the `default` task if one exists. But you can also run the task with `update default`. + +**Run a task** + +Let's try adding more tasks to your updater: + +```js +module.exports = function(app) { + app.task('default', function(cb) { + console.log('default > success!'); + cb(); + }); + + app.task('foo', function(cb) { + console.log('foo > success!'); + cb(); + }); + + app.task('bar', function(cb) { + console.log('bar > success!'); + cb(); + }); +}; +``` + +Now, in the command line, run: + +```sh +$ update +# then try +$ update foo +# then try +$ update foo bar +``` + +**Run task dependencies** + +Now update your code to the following: + +```js +module.exports = function(app) { + app.task('default', ['foo', 'bar']); + + app.task('foo', function(cb) { + console.log('foo > success!'); + cb(); + }); + + app.task('bar', function(cb) { + console.log('bar > success!'); + cb(); + }); +}; +``` + +And run: + +```sh +$ update +``` + +You're now a master at running tasks with update! You can do anything with update tasks that you can do with [gulp][] tasks (we use and support gulp libraries after all!). + +**Next steps** + +Update does much more than this. For a more in-depth introduction, we highly recommend visiting the [getting started guide](https://github.com/update/getting-started). \ No newline at end of file diff --git a/support/docs/updatefile.md b/support/docs/updatefile.md new file mode 100644 index 0000000..e4ef3b0 --- /dev/null +++ b/support/docs/updatefile.md @@ -0,0 +1,81 @@ +--- +title: Updatefile +related: + doc: ['installing-updaters', 'updaters', 'tasks'] +--- + +If you're authoring (and maybe even publishing) an updater, you can use the same conventions you would with any other node.js library. If node.js/npm can find your module, then so can Update. You can write your code in `index.js`, or `lib/foo.js` or wherever you want. + +However, Update's CLI shows a little unfair favoratism for files with the name `updatefile.js`. + +When the current working directory has an `updatefile.js`, Update's CLI will try to run any tasks or code in that file _instead of running your [default updaters](cli/built-in-tasks.md#init)_. + +**Force default updaters to run** + +You can force Update's CLI to run all of your default updaters by passing the `--run` flag on the command line, or to _always run default updaters in a project that has an `updatefile.js`, you can add the following in package.json: + +```json +{ + "update": { + "run": true + } +} +``` + +Note that that this will run _both_ the default updaters, and the `updatefile.js`, in that order. + +## How the CLI uses updatefile.js + +Each time `update` is run, Update's CLI looks for an `updatefile.js` in the current working directory. + +**If `updatefile.js` exists** + +Update's CLI attempts to: + +* Load a local installation of the Update library using node's `require()` system, falling back to global installation if not found. +* Load the configuration from `updatefile.js` using node.js `require()` system +* Register it as the ["default" updater](updaters.md#default-updater) +* Execute any tasks or updaters you've specified for it to run. +* If multiple task or updater names are specified on the command line, Update's CLI will attempt to run all of the specified tasks and updaters. + +**If `updatefile.js` does not exist** + +Update's CLI attempts to: + +* Find any updaters you've specified for it to run by using node's `require()` system to search for locally and globally installed modules with the name `updater-*`. + + +## Creating an updatefile.js + +An `updatefile.js` may contain any custom JavaScript code, but must export a function that takes an instance of Update (`app`): + +**Example** + +```js +// -- updatefile.js -- +module.exports = function(app) { + // custom code here +}; +``` + +Inside this function, you can define [tasks](tasks.md), additional [updaters](updaters.md), or any other custom JavaScript code necessary for your updater: + +```js +module.exports = function(app) { + // register a task + app.task('default', function(cb) { + // do task stuff + cb(); + }); + + // register an updater + app.register('foo', function() { + + }); + + // register another updater + app.register('bar', function() { + + }); +}; +``` diff --git a/support/docs/updaters.md b/support/docs/updaters.md new file mode 100644 index 0000000..f2a8a80 --- /dev/null +++ b/support/docs/updaters.md @@ -0,0 +1,232 @@ +--- +title: Updaters +related: + doc: ['tasks', 'updatefile', 'installing-updaters', 'symlinking-updaters'] +--- + +This document describes how to create, register and run updaters. + + + +## TODO + +- [ ] document updater args +- [ ] explain how the `base` instance works +- [ ] document `env` + + +## What is an updater? + +Updaters are [plugins](api/plugins.md) that are registered by name. If you're not familiar with plugins yet, it might be a good idea to review the [plugins docs](api/plugins.md) first. + +The primary difference between "updaters" and "plugins" is how they're registered, but there are a few other minor differences: + +| | **Plugin** | **Updater** | +| --- | --- | --- | +| Registered with | [.use](api/plugins.md#use) method | [.register](#register) method or [.updater](#updater) method | +| Instance | Loaded onto "current" `Update` instance | A `new Update()` instance is created for every updater registered | +| Invoked | Immediately | `.register` deferred (lazy), `.updater` immediately | +| Run using | [.run](api/plugins.md#run): all plugins are run at once | `.update`: only specified plugin(s) are run | + +## Creating updaters + +An updater function takes an instance of `Update` as the first argument. + +**Example** + +```js +function updater(app) { + // do updater stuff +} +``` + + + +## Registering updaters + +Updaters may be registered using either of the following methods: + +* `.register`: if the plugin should not be invoked until it's called by `.update` (stays lazy while it's cached, this is preferred) +* `.updater`: if the plugin needs to be invoked immediately when registered + +### .register + +Register an updater function with the given `name` using the `.register` method. + +**Example** + +```js +var update = require('update'); +var app = update(); + +function updater(app) { + // do updater stuff when the updater is run with the `.update` method. + console.log('foo is being run'); +} + +// register as an updater with the `.register` method +app.register('foo', updater); + +// run the `foo` updater with the `.update` method +app.update('foo', function(err) { + if (err) return console.log(err); +}); +//=> "foo is being run" +``` + +### .updater + +Register an updater function with the given `name` using the `.updater` method. + +**Example** + +```js +var update = require('update'); +var app = update(); + +function updater(app) { + // do updater stuff when the updater is registered + console.log('foo is being registered'); +} + +// register as an updater using `.updater` +app.updater('foo', updater); +//=> "foo is being registered" +``` + +**Should I use `.updater` or `.register`?** + +In general, it's recommended that you use the `.register` method. In most cases update is smart enough to figure out when to invoke updater functions. + +However, there are always exceptions. If you create custom code and notice that update can't find the information it needs. Try using the `.updater` method to invoke the function when the updater is registered. + +## Running updaters + +Updaters and their tasks can be run by command line or API. + +**Command line** + +To run globally or locally installed `updater-foo`, or an updater named `foo` in `updatefile.js`, run: + +```sh +$ update foo +``` + +**API** + +```js +var update = require('update'); +var app = update(); + +function fn() { + // do updater stuff +} + +// the `.register` method does not invoke the updater +app.register('foo', fn); + +// the `.updater` method invokes the updater immediately +app.updater('bar', fn); + +// run updaters foo and bar in series (both updaters will be invoked) +app.update(['foo', 'bar'], function(err) { + if (err) return console.log(err); +}); +``` + +## Resolving updaters + +Updaters can be published to npm and installed globally or locally. But there is no requirement that updaters must be published. You can also create custom updaters and register using the [.register](#register) or [.updater](#updater) methods. + +This provides a great deal of flexibility, but it also means that we need a strategy for _finding updaters_ when `update` is run from the command line. + +### Tasks and updaters + +1. When both a task and an updater have the same name _on the same instance_, Update will always try to run the task first (this is unlikely to happen unless you intend for it to - there are [reasons to do this](#naming-tips)) + +### Naming tips + +Since the [.build](tasks.md#build) method only runs tasks, you can use this to your advantage by aliasing sub-generators with tasks. + +**Don't do this** + +```js +module.exports = function(app) { + app.register('foo', function(foo) { + foo.task('default', function(cb) { + // do task stuff + cb(); + }); + }); + + // `.build` doesn't run updaters + app.build('foo', function(err) { + if (err) return console.log(err); + }); +}; +``` + +**Do this** + +```js +module.exports = function(app) { + app.register('foo', function(foo) { + foo.task('default', function(cb) { + // do task stuff + cb(); + }); + }); + + // `.update` will run updater `foo` + app.update('foo', function(err) { + if (err) return console.log(err); + }); +}; +``` + +**Or this** + +```js +module.exports = function(app) { + app.register('foo', function(foo) { + foo.task('default', function(cb) { + // do task stuff + cb(); + }); + }); + + app.task('foo', function(cb) { + app.update('foo', cb); + }); + + // `.build` will run task `foo`, which runs updater `foo` + app.build('foo', function(err) { + if (err) return console.log(err); + }); +}; +``` + +### Order of precendence + +When the command line is used, Update's CLI resolves updaters in the following order: + +1. [default updater](#default-updater): attempts to match given names to updaters and tasks registered on the `default` updater +2. built-in updaters: attempts to match given names to Update's [built-in updaters](cli/built-in-updaters.md) +3. locally installed updaters +4. globally installed updaters + +## Discovering updaters + +todo + +## Default updater + +If an updater is registered with the name `default` it will receive special treatment from Update and Update's CLI. More specifically, when Update's CLI looks for updaters or tasks to run, it will search for them on the `default` updater first. + +There is a catch... + +**Registering the "default" updater** + +_The only way to register a `default` updater is by creating an [updatefile.js](updatefile.md) in the current working directory._ + +When used by command line, Update's CLI will then use node's `require()` system to get the function exported by `updatefile.js` and use it as the `default` updater. diff --git a/support/lib/_drafts/matter.js b/support/lib/_drafts/matter.js new file mode 100644 index 0000000..9597eaa --- /dev/null +++ b/support/lib/_drafts/matter.js @@ -0,0 +1,29 @@ + + // app.onLoad(/\.md$/, function(view, next) { + // if (/^#/.test(view.content)) { + // view.content = view.content.replace(/^#[^\n]+/, ''); + // } + // var data = '---\ntitle: '; + // var title = view.stem; + // var segs = title.split('.'); + // if (segs.length > 1) { + // title = segs[1]; + // } + // title = title.split('-').join(' '); + // title = title.charAt(0).toUpperCase() + title.slice(1); + // data += title; + // data += '\n'; + // data += 'layout: default\n'; + // data += 'related:\n'; + // data += ' doc: []\n'; + // data += '---\n\n'; + // view.content = data + view.content; + // next(); + // }); + + // app.task('default', function() { + // app.pages('docs/**/*.md'); + // return app.toStream('pages') + // // .pipe(app.renderFile()) + // .pipe(app.dest('docs')); + // }); diff --git a/support/lib/common.js b/support/lib/common.js new file mode 100644 index 0000000..b43179d --- /dev/null +++ b/support/lib/common.js @@ -0,0 +1,22 @@ +'use strict'; + +var path = require('path'); +var helpers = require('./helpers'); +var isValid = require('is-valid-app'); +var pkg = require('base-pkg'); + +module.exports = function(options) { + return function(app) { + if (!isValid(app, 'update-support-common')) return; + app.use(require('generate-collections')); + app.use(require('generate-defaults')); + app.use(require('verb-toc')); + app.use(helpers()); + app.use(pkg()); + + if (!app.docs) app.create('docs'); + app.option('renameKey', function(key, file) { + return file ? file.basename : path.basename(key); + }); + }; +}; diff --git a/support/lib/helpers.js b/support/lib/helpers.js new file mode 100644 index 0000000..9fbea96 --- /dev/null +++ b/support/lib/helpers.js @@ -0,0 +1,146 @@ +'use strict'; + +var path = require('path'); +var utils = require('./utils'); + +module.exports = function(options) { + return function(app) { + if (!utils.isValid(app, 'update-support-helpers')) return; + app.helpers(utils.helpers()); + app.helper('raw', function(str) { + console.log(str); + return str; + }); + + app.helper('hasValue', function(val, str) { + return utils.hasValue(val) ? str : ''; + }); + + app.helper('hasAny', function(arr, str) { + arr = utils.arrayify(arr); + var len = arr.length; + var idx = -1; + while (++idx < len) { + var ele = arr[idx] || []; + if (ele.length) { + return str; + } + } + return ''; + }); + + app.helper('relatedLinks', function(related) { + if (!related || typeof related !== 'object') { + return ''; + } + var links = app.getHelper('links').bind(this); + return relatedLinks(related, links); + }); + + app.helper('links', function(related, prop) { + var arr = related[prop] || (related[prop] = []); + arr = utils.arrayify(arr); + if (arr.length === 0) { + return ''; + } + + var fp = this.view.stem; + var dir = 'docs'; + var segs = fp.split('.'); + if (segs.length > 1) { + dir = segs[0]; + } + var links = arr.map(function(link) { + return createLink(dir, prop, link); + }); + return links.join('\n'); + }); + }; +}; + +function relatedLinks(related, links) { + var keys = Object.keys(related); + if (keys.length === 0) { + return ''; + } + + var hasLinks = false; + function reduce(acc, key) { + if (related[key].length === 0) { + return acc; + } + + hasLinks = true; + acc += `**${heading(key)}**\n${links(related, key)}\n`; + return acc; + } + + var list = `${keys.reduce(reduce, '')}`; + + if (!hasLinks) { + return ''; + } + + return `## Related\n${list}`; +} + +function createLink(dir, prop, link) { + var key = (prop === 'doc') ? 'docs' : prop; + link = normalizeLink(link); + + if (dir !== key) { + if (key === 'docs') { + key = ''; + } + link.filename = path.join('..', key, link.filename); + } else if (key !== 'docs' && dir !== key) { + link.filename = path.join(key, link.filename); + } + return `- [${link.title}](${link.filename}${link.anchor})`; +} + +function heading(title) { + switch (title.toLowerCase()) { + case 'doc': + title = 'docs'; + break; + case 'url': + title = 'links'; + break; + default: + if (/(i|s)$/.test(title.toLowerCase()) === false) { + title += 's'; + } + break; + } + + if (/s$/.test(title)) { + return utils.pascalcase(title); + } + return title.toUpperCase(); +} + +function normalizeLink(obj) { + if (typeof obj === 'string') { + obj = {link: obj}; + } + var link = obj.link; + var filename = link; + var name = link; + var anchor = ''; + var segs = link.split('#'); + if (segs.length === 2) { + name = segs[segs.length - 1]; + anchor = '#' + name; + filename = segs[0]; + } + + if (!/\.md$/.test(filename) && !/#/.test(filename)) { + filename += '.md'; + } + + obj.title = obj.title || name; + obj.filename = obj.filename || filename; + obj.anchor = obj.anchor || anchor; + return obj; +} diff --git a/support/lib/index.js b/support/lib/index.js new file mode 100644 index 0000000..3559238 --- /dev/null +++ b/support/lib/index.js @@ -0,0 +1,2 @@ +require('set-blocking')(true); +module.exports = require('export-files')(__dirname); diff --git a/support/lib/middleware.js b/support/lib/middleware.js new file mode 100644 index 0000000..5dac7d2 --- /dev/null +++ b/support/lib/middleware.js @@ -0,0 +1,55 @@ +'use strict'; + +var path = require('path'); +var isValid = require('is-valid-app'); +var utils = require('./utils'); + +module.exports = function(options) { + return function(app) { + if (!isValid(app, 'update-support-middleware')) return; + app.cache.views = {docs: []}; + + app.onLoad(/\.md$/, function(view, next) { + var related = view.data.related || (view.data.related = {}); + related.doc = utils.arrayify(related.doc); + related.api = utils.arrayify(related.api); + related.cli = utils.arrayify(related.cli); + related.url = utils.arrayify(related.url); + + if (view.content.indexOf('') !== -1) { + view.data.toc = true; + } + + view.data.layout = 'default'; + if (view.data.toc === true) { + view.data.toc = {render: true}; + } + if (view.stem.indexOf('docs.') === 0) { + app.cache.views.docs.push(view); + } + next(); + }); + + app.preRender(/docs[\\\/][^\\\/]+\.md$/, function(view, next) { + if (typeof view.data.title === 'undefined') { + next(new Error('`title` is missing in ' + view.path)); + return; + } + next(); + }); + + app.preWrite(/\.md$/, function(file, next) { + var segs = file.stem.split('.').filter(Boolean); + if (segs[0] === 'docs') { + segs.shift(); + file.stem = segs[0]; + } + if (segs.length > 1) { + file.path = path.resolve(file.base, segs.join('/') + file.extname); + } + file.content = file.content.trim(); + file.content += '\n'; + next(); + }); + }; +}; diff --git a/support/lib/paths.js b/support/lib/paths.js new file mode 100644 index 0000000..11b64a6 --- /dev/null +++ b/support/lib/paths.js @@ -0,0 +1,21 @@ +'use strict'; + +var path = require('path'); +var base = path.resolve(__dirname, '..'); +exports.cwd = require('memoize-path')(base); +exports.memo = require('memoize-path')(base); + +exports.docs = function(fp) { + var res = exports.memo('../docs')(fp); + return fp ? res() : res; +}; + +exports.site = function(fp) { + var res = exports.memo('../_gh_pages')(fp); + return fp ? res() : res; +}; + +exports.tmpl = function(fp) { + var res = exports.memo('templates')(fp); + return fp ? res() : res; +}; diff --git a/support/lib/plugins.js b/support/lib/plugins.js new file mode 100644 index 0000000..c2e515d --- /dev/null +++ b/support/lib/plugins.js @@ -0,0 +1,80 @@ +'use strict'; + +var path = require('path'); +var utils = require('./utils'); + +exports.buildPaths = function(dest) { + var paths = {api: [], cli: [], docs: [], url: [], path: [], dest: [], anchors: {}}; + var files = []; + + return utils.through.obj(function(file, enc, next) { + setDir(file, paths); + getAnchors(file, paths); + createDest(file, paths, dest); + paths.path.push(file.path); + files.push(file); + next(); + }, function(cb) { + var len = files.length; + var idx = -1; + while (++idx < len) { + var file = files[idx]; + getLinks(file, paths); + file.paths = paths; + this.push(file); + } + cb(); + }); +}; + +exports.lintPaths = function(dest) { + return utils.through.obj(function(file, enc, next) { + // console.log(file.paths); + next(null, file); + }); +}; + +function setDir(file, paths) { + file.name = file.stem; + var segs = file.relative.split('/'); + var dir = 'docs'; + var rest = file.relative; + if (segs.length > 1) { + dir = segs.shift(); + rest = segs.join('/'); + } + paths[dir] = paths[dir] || []; + paths[dir].push(rest); + file.rest = rest; + file.dir = dir; +} + +function createDest(view, paths, dest) { + var file = view.clone(); + file.dirname = path.join(file.dirname, file.stem); + file.basename = 'index.html'; + paths.dest.push(path.resolve(dest, file.relative)); +} + +function getAnchors(file, paths) { + var str = file.contents.toString(); + var matches = str.match(/^#+\s+([^\n]+)/gm); + if (matches) { + paths.anchors[file.rest] = paths.anchors[file.rest] || []; + matches.reduce(function(acc, str) { + var anchor = utils.slugify(str.replace(/^#+\s+/, '')); + if (acc.indexOf(anchor) === -1) { + acc.push(anchor); + } + return acc.sort(); + }, paths.anchors[file.rest]); + } +} + +function getLinks(file, paths) { + var md = new utils.Remarkable({paths: paths, file: file}); + md.use(utils.prettify); + md.use(utils.lintLinks()); + md.render(file.content); +} + diff --git a/support/lib/utils.js b/support/lib/utils.js new file mode 100644 index 0000000..5f3c791 --- /dev/null +++ b/support/lib/utils.js @@ -0,0 +1,120 @@ +'use strict'; + +var utils = module.exports = require('lazy-cache')(require); +var rules = require('pretty-remarkable/lib/rules'); + +var fn = require; +require = utils; + +require('is-valid-app', 'isValid'); +require('has-value'); +require('pretty-remarkable', 'prettify'); +require('remarkable', 'Remarkable'); +require('strip-color'); +require('pascalcase'); +require('template-helpers', 'helpers'); +require('through2', 'through'); + +utils.arrayify = function(val) { + return val ? (Array.isArray(val) ? val : [val]) : []; +}; + +/** + * Slugify the url part of a markdown link. + * + * @param {String} `anchor` The string to slugify + * @return {String} + * @api public + */ + +utils.slugify = function(anchor) { + anchor = utils.stripColor(anchor); + anchor = anchor.toLowerCase(); + anchor = anchor.split(/ /).join('-'); + anchor = anchor.split(/\t/).join('--'); + anchor = anchor.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join(''); + return anchor; +}; + +utils.links = function(rules) { + if (rules._added) return; + rules._added = true; + var open = rules.link_open; + rules.link_open = function(tokens, idx, options, env) { + open.apply(rules, arguments); + var token = tokens[idx]; + if (/[.\\\/]+issues/.test(token.href)) { + return; + } + + if (options.paths && !/http/.test(token.href)) { + var href = token.href.replace(/^\.?[\\\/]+/, '').split(/[\\\/]+/).join('/'); + var paths = options.paths; + var anchors = paths.anchors; + var file = options.file; + if (/#/.test(href)) { + var idx = href.indexOf('#'); + var val = href.slice(idx + 1); + var pre = href.slice(0, idx); + var anc = anchors[pre]; + if (anc && anc.indexOf(val) === -1) { + console.log(pre) + throw new Error(`cannot find anchor: #${val} in "${pre}" (defined in ${file.path})`); + } + } else { + var segs = href.split('/'); + var len = segs.length; + if (len === 1) { + segs.unshift(file.dir); + } else if (len === 2 && segs[0] === '..') { + segs[0] = 'docs'; + } else if (len > 2 && segs[0] === '..') { + segs.shift(); + } + + var seg = segs.shift(); + var rest = segs.join('/'); + var group = paths[seg]; + + if (typeof group === 'undefined') { + throw new Error(`directory group: "${seg}" is not defined`); + } + if (group.indexOf(rest) === -1 && !/issues/.test(rest)) { + throw new Error(`cannot find filepath: "${rest}" in "${seg}" (${file.path})`); + } + } + } + return ''; + }; + return rules; +}; + +utils.lintLinks = function(options) { + utils.links(rules); + + return function(md) { + md.renderer.renderInline = function(tokens, options, env) { + var len = tokens.length, i = 0; + var str = ''; + + while (len--) { + str += rules[tokens[i].type](tokens, i++, options, env, this); + } + return str; + }; + + md.renderer.render = function(tokens, options, env) { + var len = tokens.length, i = -1; + var str = ''; + + while (++i < len) { + if (tokens[i].type === 'inline') { + str += this.renderInline(tokens[i].children, options, env); + } else { + str += rules[tokens[i].type](tokens, i, options, env, this); + } + } + return str; + }; + }; +}; diff --git a/support/logo.png b/support/logo.png new file mode 100755 index 0000000..3009a36 Binary files /dev/null and b/support/logo.png differ diff --git a/support/package.json b/support/package.json new file mode 100644 index 0000000..72babd7 --- /dev/null +++ b/support/package.json @@ -0,0 +1,52 @@ +{ + "name": "update-docs", + "description": "Documentation for Update.", + "version": "0.0.0", + "homepage": "https://github.com/update/update-docs", + "author": "Jon Schlinkert (https://github.com/jonschlinkert)", + "repository": "update/update-docs", + "bugs": { + "url": "https://github.com/update/update-docs/issues" + }, + "private": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "test": "mocha" + }, + "devDependencies": { + "base-generators": "^0.4.1", + "copy": "^0.2.3", + "delete": "^0.3.2", + "export-files": "^2.1.1", + "gulp-drafts": "^0.2.0", + "gulp-format-md": "^0.1.9", + "gulp-reflinks": "^0.1.0", + "has-value": "^0.3.1", + "is-valid-app": "^0.2.0", + "memoize-path": "^0.1.2", + "pascalcase": "^0.1.1", + "pretty-remarkable": "^0.3.6", + "remarkable": "^1.6.2", + "set-blocking": "^2.0.0", + "template-helpers": "^0.6.3", + "through2": "^2.0.1", + "verb-toc": "^0.2.3" + }, + "dependencies": { + "generate-collections": "^0.3.4", + "generate-defaults": "^0.3.1", + "lazy-cache": "^2.0.1", + "strip-color": "^0.1.0" + }, + "verb": { + "plugins": [ + "gulp-format-md" + ], + "lint": { + "reflinks": true + } + } +} diff --git a/support/verbfile.js b/support/verbfile.js new file mode 100644 index 0000000..e436c4c --- /dev/null +++ b/support/verbfile.js @@ -0,0 +1,38 @@ +'use strict'; + +var path = require('path'); +var copy = require('copy'); +var del = require('delete'); +var drafts = require('gulp-drafts'); +var reflinks = require('gulp-reflinks'); +var format = require('gulp-format-md'); +var paths = require('./lib/paths'); +var lib = require('./lib'); + +module.exports = function(app) { + var dest = paths.site(); + app.use(lib.middleware()); + app.use(lib.common()); + + app.task('clean', function(cb) { + del(paths.docs(), {force: true}, cb); + }); + + app.task('copy', function(cb) { + copy('*.png', paths.docs(), cb); + }); + + app.task('docs', ['clean', 'copy'], function(cb) { + app.layouts('docs/layouts/*.md', {cwd: paths.cwd()}); + app.docs('docs/*.md', {cwd: paths.cwd(), layout: 'default'}); + + return app.toStream('docs') + .pipe(drafts()) + .pipe(app.renderFile('*')) + .pipe(reflinks()) + .pipe(format()) + .pipe(app.dest(paths.docs())); + }); + + app.task('default', ['docs']); +}; diff --git a/test.js b/test.js deleted file mode 100644 index b68bc8c..0000000 --- a/test.js +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * update - * - * Copyright (c) 2013-2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - -var assert = require('assert'); -var update = require('./'); - -describe('update', function () { - it('should update the year', function () { - assert.equal(update('Copyright (c) 2013, Jon Schlinkert.'), 'Copyright (c) 2013-2015, Jon Schlinkert.'); - assert.equal(update('Copyright (c) 2014, Jon Schlinkert.'), 'Copyright (c) 2014-2015, Jon Schlinkert.'); - assert.equal(update('Copyright (c) 2015, Jon Schlinkert.'), 'Copyright (c) 2015, Jon Schlinkert.'); - }); -}); \ No newline at end of file diff --git a/test/_suite.js b/test/_suite.js new file mode 100644 index 0000000..8a136aa --- /dev/null +++ b/test/_suite.js @@ -0,0 +1,21 @@ +'use strict'; + +var generate = require('..'); +var runner = require('base-test-runner')(); +var suite = require('base-test-suite'); + +/** + * Run the tests in `base-test-suite` + */ + +runner.on('templates', function(file) { + var fn = require(file.path); + if (typeof fn === 'function') { + fn(generate); + } else { + throw new Error('expected ' + file.path + ' to export a function'); + } +}); + +runner.addFiles('templates', suite.test.templates); +runner.addFiles('templates', suite.test['assemble-core']); diff --git a/test/app.cli.js b/test/app.cli.js new file mode 100644 index 0000000..9b2227a --- /dev/null +++ b/test/app.cli.js @@ -0,0 +1,20 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var update = require('..'); +var app; + +describe('app.cli', function() { + beforeEach(function() { + app = update({cli: true}); + }); + + describe('app.cli.map', function() { + it('should add a property to app.cli', function() { + app.cli.map('abc', function() {}); + assert.equal(app.cli.keys.pop(), 'abc'); + }); + }); +}); + diff --git a/test/app.doc.js b/test/app.doc.js new file mode 100644 index 0000000..abf0879 --- /dev/null +++ b/test/app.doc.js @@ -0,0 +1,23 @@ +'use strict'; + +require('mocha'); +require('should'); +var assert = require('assert'); +var generate = require('..'); +var app; + +describe('app', function() { + beforeEach(function() { + app = generate(); + app.create('docs'); + }); + + describe('add doc', function() { + it('should add docs to `app.views.docs`:', function() { + app.doc('a.hbs', {path: 'a.hbs', content: 'a'}); + app.doc('b.hbs', {path: 'b.hbs', content: 'b'}); + app.doc('c.hbs', {path: 'c.hbs', content: 'c'}); + assert(Object.keys(app.views.docs).length === 3); + }); + }); +}); diff --git a/test/app.docs.js b/test/app.docs.js new file mode 100644 index 0000000..cdf8955 --- /dev/null +++ b/test/app.docs.js @@ -0,0 +1,25 @@ +'use strict'; + +require('mocha'); +require('should'); +var assert = require('assert'); +var generate = require('..'); +var app; + +describe('app', function() { + beforeEach(function() { + app = generate(); + app.create('docs'); + }); + + describe('add docs', function() { + it('should add docs to `app.views.docs`:', function() { + app.docs({ + 'a.hbs': {path: 'a.hbs', content: 'a'}, + 'b.hbs': {path: 'b.hbs', content: 'b'}, + 'c.hbs': {path: 'c.hbs', content: 'c'}, + }); + assert.equal(Object.keys(app.views.docs).length, 3); + }); + }); +}); diff --git a/test/app.extendWith.js b/test/app.extendWith.js new file mode 100644 index 0000000..7228e75 --- /dev/null +++ b/test/app.extendWith.js @@ -0,0 +1,599 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var gm = require('global-modules'); +var npm = require('npm-install-global'); +var isAbsolute = require('is-absolute'); +var exists = require('fs-exists-sync'); +var resolve = require('resolve'); +var Base = require('..'); +var base; + +var fixture = path.resolve.bind(path, __dirname, 'fixtures/updaters'); +function resolver(search, app) { + try { + if (isAbsolute(search.name)) { + search.name = require.resolve(search.name); + } else { + search.name = resolve.sync(search.name, {basedir: gm}); + } + search.app = app.register(search.name, search.name); + } catch (err) {} +} + +describe('.extendWith', function() { + before(function(cb) { + if (!exists(path.resolve(gm, 'generate-bar'))) { + npm.install('generate-bar', cb); + } else { + cb(); + } + }); + + beforeEach(function() { + base = new Base(); + base.option('toAlias', function(name) { + return name.replace(/^generate-/, ''); + }); + + base.on('unresolved', resolver); + }); + + it.skip('should throw an error when an updater is not found', function(cb) { + try { + base.register('foo', function(app) { + app.extendWith('fofoofofofofof'); + }); + + base.getUpdater('foo'); + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, 'cannot find updater: "fofoofofofofof"'); + cb(); + } + }); + + it('should extend an updater with settings in the default updater', function(cb) { + var count = 0; + + base.register('foo', function(app) { + app.task('default', function(next) { + assert.equal(app.options.foo, 'bar'); + assert.equal(app.cache.data.foo, 'bar'); + count++; + next(); + }); + }); + + base.register('default', function(app) { + app.data({foo: 'bar'}); + app.option({foo: 'bar'}); + }); + + base.update('foo', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should not extend tasks by default', function(cb) { + var count = 0; + + base.register('foo', function(app) { + app.task('default', function(next) { + assert(app.tasks.hasOwnProperty('default')); + assert(!app.tasks.hasOwnProperty('a')); + assert(!app.tasks.hasOwnProperty('b')); + assert(!app.tasks.hasOwnProperty('c')); + count++; + next(); + }); + }); + + base.register('default', function(app) { + app.task('a', function(next) { + next(); + }); + app.task('b', function(next) { + next(); + }); + app.task('c', function(next) { + next(); + }); + }); + + base.update('foo', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should get a named updater', function(cb) { + var count = 0; + + base.register('foo', function(app) { + app.extendWith('bar'); + count++; + }); + + base.register('bar', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + assert.equal(count, 1); + cb(); + }); + + it('should extend an updater with a named updater', function(cb) { + base.register('foo', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('bar'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.register('bar', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + }); + + it('should extend an updater with an array of updaters', function(cb) { + base.register('foo', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith(['bar', 'baz', 'qux']); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.register('bar', function(app) { + app.task('a', function() {}); + }); + + base.register('baz', function(app) { + app.task('b', function() {}); + }); + + base.register('qux', function(app) { + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + }); + + describe('invoke updaters', function(cb) { + it('should extend with an updater instance', function(cb) { + base.register('foo', function(app) { + var bar = app.getUpdater('bar'); + app.extendWith(bar); + + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + assert(app.tasks.hasOwnProperty('c')); + cb(); + }); + + base.register('bar', function(app) { + app.isBar = true; + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + }); + + it('should invoke a named updater', function(cb) { + base.register('foo', function(app) { + app.extendWith('bar'); + + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + assert(app.tasks.hasOwnProperty('c')); + cb(); + }); + + base.register('bar', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + }); + }); + + describe('extend updaters', function(cb) { + it('should extend an updater with an updater invoked by name', function(cb) { + base.register('foo', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('bar'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.register('bar', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + }); + + it('should extend an updater with an updater invoked by alias', function(cb) { + base.register('foo', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('qux'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.register('generate-qux', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('qux'); + base.getUpdater('foo'); + }); + + it('should extend with an updater invoked by filepath', function(cb) { + base.register('foo', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith(fixture('qux')); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.getUpdater('foo'); + }); + + it('should extend with an updater invoked from node_modules by name', function(cb) { + base.register('abc', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('generate-foo'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.getUpdater('abc'); + }); + + it('should extend with an updater invoked from node_modules by name on a default instance', function() { + var app = new Base(); + + app.on('unresolved', resolver); + app.option('toAlias', function(name) { + return name.replace(/^generate-/, ''); + }); + + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('generate-foo'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + }); + + it('should use an updater from node_modules as a plugin', function() { + var app = new Base(); + + app.option('toAlias', function(name) { + return name.replace(/^generate-/, ''); + }); + + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.use(require('generate-foo')); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + }); + + it('should extend with an updater invoked from global modules by name', function(cb) { + base.register('zzz', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + app.extendWith('generate-bar'); + + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.getUpdater('zzz'); + }); + + it('should extend with an updater invoked from global modules by alias', function(cb) { + base.register('generate-bar'); + + base.register('zzz', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('bar'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.getUpdater('zzz'); + }); + }); + + describe('sub-updaters', function(cb) { + it('should invoke sub-updaters', function(cb) { + base.register('foo', function(app) { + app.register('one', function(app) { + app.task('a', function() {}); + }); + app.register('two', function(app) { + app.task('b', function() {}); + }); + + app.extendWith('one'); + app.extendWith('two'); + + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + cb(); + }); + + base.getUpdater('foo'); + }); + + it('should invoke a sub-updater on the base instance', function(cb) { + base.register('foo', function(app) { + app.extendWith('bar.sub'); + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + assert(app.tasks.hasOwnProperty('c')); + cb(); + }); + + base.register('bar', function(app) { + app.register('sub', function(sub) { + sub.task('a', function() {}); + sub.task('b', function() {}); + sub.task('c', function() {}); + }); + }); + + base.getUpdater('foo'); + }); + + it('should invoke a sub-updater from node_modules by name', function(cb) { + base.register('abc', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('xyz'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.register('xyz', function(app) { + app.extendWith('generate-foo'); + }); + + base.getUpdater('abc'); + }); + + it('should invoke a sub-updater from node_modules by alias', function(cb) { + base.register('generate-foo'); + + base.register('abc', function(app) { + assert(!app.tasks.a); + assert(!app.tasks.b); + assert(!app.tasks.c); + + app.extendWith('xyz'); + assert(app.tasks.a); + assert(app.tasks.b); + assert(app.tasks.c); + cb(); + }); + + base.register('xyz', function(app) { + app.extendWith('foo'); + }); + + base.getUpdater('abc'); + }); + + it('should invoke an array of sub-updaters', function(cb) { + base.register('foo', function(app) { + app.register('one', function(app) { + app.task('a', function() {}); + }); + app.register('two', function(app) { + app.task('b', function() {}); + }); + + app.extendWith(['one', 'two']); + + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + cb(); + }); + + base.getUpdater('foo'); + }); + + it('should invoke sub-updaters from sub-updaters', function(cb) { + base.register('foo', function(app) { + app.register('one', function(sub) { + sub.register('a', function(a) { + a.task('a', function() {}); + }); + }); + + app.register('two', function(sub) { + sub.register('a', function(a) { + a.task('b', function() {}); + }); + }); + + app.extendWith('one.a'); + app.extendWith('two.a'); + + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + cb(); + }); + + base.getUpdater('foo'); + }); + + it('should invoke an array of sub-updaters from sub-updaters', function(cb) { + base.register('foo', function(app) { + app.register('one', function(sub) { + sub.register('a', function(a) { + a.task('a', function() {}); + }); + }); + + app.register('two', function(sub) { + sub.register('a', function(a) { + a.task('b', function() {}); + }); + }); + + app.extendWith(['one.a', 'two.a']); + + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + cb(); + }); + + base.getUpdater('foo'); + }); + + it('should invoke sub-updater that invokes another updater', function(cb) { + base.register('foo', function(app) { + app.extendWith('bar'); + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + assert(app.tasks.hasOwnProperty('c')); + cb(); + }); + + base.register('bar', function(app) { + app.extendWith('baz'); + }); + + base.register('baz', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + }); + + base.getUpdater('foo'); + }); + + it('should invoke sub-updater that invokes another sub-updater', function(cb) { + base.register('foo', function(app) { + app.extendWith('bar.sub'); + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + assert(app.tasks.hasOwnProperty('c')); + cb(); + }); + + base.register('bar', function(app) { + app.register('sub', function(sub) { + sub.extendWith('baz.sub'); + }); + }); + + base.register('baz', function(app) { + app.register('sub', function(sub) { + sub.task('a', function() {}); + sub.task('b', function() {}); + sub.task('c', function() {}); + }); + }); + + base.getUpdater('foo'); + }); + + it('should invoke sub-updater that invokes another sub-updater', function(cb) { + base.register('foo', function(app) { + app.extendWith('bar.sub'); + assert(app.tasks.hasOwnProperty('a')); + assert(app.tasks.hasOwnProperty('b')); + assert(app.tasks.hasOwnProperty('c')); + cb(); + }); + + base.register('bar', function(app) { + app.register('sub', function(sub) { + sub.extendWith('baz.sub'); + }); + }); + + base.register('baz', function(app) { + app.register('sub', function(sub) { + sub.task('a', function() {}); + sub.task('b', function() {}); + sub.task('c', function() {}); + }); + }); + + base.getUpdater('foo'); + }); + }); +}); diff --git a/test/app.getUpdater.js b/test/app.getUpdater.js new file mode 100644 index 0000000..0b57e42 --- /dev/null +++ b/test/app.getUpdater.js @@ -0,0 +1,103 @@ +'use strict'; + +var path = require('path'); +var assert = require('assert'); +var Base = require('..'); +var base; + +var fixtures = path.resolve.bind(path, __dirname + '/fixtures'); + +describe('.updater', function() { + beforeEach(function() { + base = new Base(); + }); + + it('should get a updater from the base instance', function() { + base.register('abc', function() {}); + var updater = base.getUpdater('abc'); + assert(updater); + assert.equal(typeof updater, 'object'); + assert.equal(updater.name, 'abc'); + }); + + it('should fail when a updater is not found', function() { + var updater = base.getUpdater('whatever'); + assert(!updater); + }); + + it('should get a updater from the base instance from a nested updater', function() { + base.register('abc', function() {}); + base.register('xyz', function(app) { + app.register('sub', function(sub) { + var updater = base.getUpdater('abc'); + assert(updater); + assert.equal(typeof updater, 'object'); + assert.equal(updater.name, 'abc'); + }); + }); + base.getUpdater('xyz'); + }); + + it('should get a updater from the base instance using `this`', function() { + base.register('abc', function() {}); + base.register('xyz', function(app) { + app.register('sub', function(sub) { + var updater = this.getUpdater('abc'); + assert(updater); + assert.equal(typeof updater, 'object'); + assert.equal(updater.name, 'abc'); + }); + }); + base.getUpdater('xyz'); + }); + + it('should get a base updater from "app" from a nested updater', function() { + base.register('abc', function() {}); + base.register('xyz', function(app) { + app.register('sub', function(sub) { + var updater = app.getUpdater('abc'); + assert(updater); + assert.equal(typeof updater, 'object'); + assert.equal(updater.name, 'abc'); + }); + }); + base.getUpdater('xyz'); + }); + + it('should get a nested updater', function() { + base.register('abc', function(abc) { + abc.register('def', function(def) {}); + }); + + var updater = base.getUpdater('abc.def'); + assert(updater); + assert.equal(typeof updater, 'object'); + assert.equal(updater.name, 'def'); + }); + + it('should get a deeply nested updater', function() { + base.register('abc', function(abc) { + abc.register('def', function(def) { + def.register('ghi', function(ghi) { + ghi.register('jkl', function(jkl) { + jkl.register('mno', function() {}); + }); + }); + }); + }); + + var updater = base.getUpdater('abc.def.ghi.jkl.mno'); + assert(updater); + assert.equal(typeof updater, 'object'); + assert.equal(updater.name, 'mno'); + }); + + it('should get a updater that was registered by path', function() { + base.register('a', fixtures('updaters/a')); + var updater = base.getUpdater('a'); + + assert(updater); + assert(updater.tasks); + assert(updater.tasks.hasOwnProperty('default')); + }); +}); diff --git a/test/app.include.js b/test/app.include.js new file mode 100644 index 0000000..df7a101 --- /dev/null +++ b/test/app.include.js @@ -0,0 +1,26 @@ +'use strict'; + +require('mocha'); +require('should'); +var assert = require('assert'); +var generate = require('..'); +var app, len; + +describe('app', function() { + beforeEach(function() { + app = generate(); + if (typeof app.include === 'undefined') { + app.create('include'); + } + len = Object.keys(app.views.includes).length; + }); + + describe('add include', function() { + it('should add includes to `app.views.includes`:', function() { + app.include('a.hbs', {path: 'a.hbs', content: 'a'}); + app.include('b.hbs', {path: 'b.hbs', content: 'b'}); + app.include('c.hbs', {path: 'c.hbs', content: 'c'}); + assert((Object.keys(app.views.includes).length - len) === 3); + }); + }); +}); diff --git a/test/app.includes.js b/test/app.includes.js new file mode 100644 index 0000000..390660d --- /dev/null +++ b/test/app.includes.js @@ -0,0 +1,28 @@ +'use strict'; + +require('mocha'); +require('should'); +var assert = require('assert'); +var generate = require('..'); +var app, len; + +describe('app', function() { + beforeEach(function() { + app = generate(); + if (typeof app.include === 'undefined') { + app.create('include'); + } + len = Object.keys(app.views.includes).length; + }) + + describe('add includes', function() { + it('should add includes to `app.views.includes`:', function() { + app.includes({ + 'a.hbs': {path: 'a.hbs', content: 'a'}, + 'b.hbs': {path: 'b.hbs', content: 'b'}, + 'c.hbs': {path: 'c.hbs', content: 'c'}, + }); + assert((Object.keys(app.views.includes).length - len) === 3); + }); + }); +}); diff --git a/test/app.layout.js b/test/app.layout.js new file mode 100644 index 0000000..f763157 --- /dev/null +++ b/test/app.layout.js @@ -0,0 +1,24 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var assemble = require('..'); +var app; + +describe('.layout()', function() { + beforeEach(function() { + app = assemble(); + if (!app.layout) { + app.create('layout', {viewType: 'layout'}); + } + }); + + describe('add layout', function() { + it('should add layouts to `app.views.layouts`:', function() { + app.layout('a.hbs', {path: 'a.hbs', contents: new Buffer('a')}); + app.layout('b.hbs', {path: 'b.hbs', contents: new Buffer('b')}); + app.layout('c.hbs', {path: 'c.hbs', contents: new Buffer('c')}); + assert(Object.keys(app.views.layouts).length === 3); + }); + }); +}); diff --git a/test/app.layouts.js b/test/app.layouts.js new file mode 100644 index 0000000..d731a0b --- /dev/null +++ b/test/app.layouts.js @@ -0,0 +1,26 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var assemble = require('..'); +var app; + +describe('.layouts()', function() { + beforeEach(function() { + app = assemble(); + if (!app.layout) { + app.create('layout', {viewType: 'layout'}); + } + }); + + describe('add layouts', function() { + it('should add layouts to `app.views.layouts`:', function() { + app.layouts({ + 'a.hbs': {path: 'a.hbs', contents: new Buffer('a')}, + 'b.hbs': {path: 'b.hbs', contents: new Buffer('b')}, + 'c.hbs': {path: 'c.hbs', contents: new Buffer('c')}, + }); + assert(Object.keys(app.views.layouts).length === 3); + }); + }); +}); diff --git a/test/app.lookupGenerator.js b/test/app.lookupGenerator.js new file mode 100644 index 0000000..7b4d159 --- /dev/null +++ b/test/app.lookupGenerator.js @@ -0,0 +1,61 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var Base = require('..'); +var base; + +describe('.lookupGenerator', function() { + beforeEach(function() { + base = new Base(); + + base.option('toAlias', function(key) { + return key.replace(/^generate-(.*)/, '$1'); + }); + }); + + it('should get a generator using a custom lookup function', function() { + base.register('generate-foo', function() {}); + base.register('generate-bar', function() {}); + var gen = base.lookupGenerator('foo', function(key) { + return ['generate-' + key, 'verb-' + key + '-generator', key]; + }); + + assert(gen); + assert.equal(gen.env.name, 'generate-foo'); + assert.equal(gen.env.alias, 'foo'); + }); + + it('should get a generator using a custom lookup function passed on options', function() { + base.register('generate-foo', function() {}); + base.register('generate-bar', function() {}); + + var gen = base.getGenerator('foo', { + lookup: function(key) { + return ['generate-' + key, 'verb-' + key + '-generator', key]; + } + }); + + assert(gen); + assert.equal(gen.env.name, 'generate-foo'); + assert.equal(gen.env.alias, 'foo'); + }); + + it('should return undefined when nothing is found', function() { + var gen = base.lookupGenerator('fofofofofo', function(key) { + return ['generate-' + key, 'verb-' + key + '-generator', key]; + }); + + assert(!gen); + }); + + it('should throw an error when a function is not passed', function(cb) { + try { + base.lookupGenerator('foo'); + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, 'expected `fn` to be a lookup function'); + cb(); + } + }); +}); diff --git a/test/app.matchGenerator.js b/test/app.matchGenerator.js new file mode 100644 index 0000000..922d5de --- /dev/null +++ b/test/app.matchGenerator.js @@ -0,0 +1,57 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var spawn = require('cross-spawn'); +var exists = require('fs-exists-sync'); +var Base = require('..'); +var base; + +describe('.matchGenerator', function() { + before(function(cb) { + if (!exists(path.resolve(__dirname, '../node_modules/generate-foo'))) { + spawn('npm', ['install', '--global', 'generate-foo'], {stdio: 'inherit'}) + .on('error', cb) + .on('close', function(code, err) { + cb(err, code); + }); + } else { + cb(); + } + }); + + beforeEach(function() { + base = new Base(); + + base.option('toAlias', function(key) { + return key.replace(/^generate-(.*)/, '$1'); + }); + }); + + it('should match a generator by name', function() { + base.register('generate-foo'); + + var gen = base.matchGenerator('generate-foo'); + assert(gen); + assert.equal(gen.env.name, 'generate-foo'); + assert.equal(gen.env.alias, 'foo'); + }); + + it('should match a generator by alias', function() { + base.register('generate-foo'); + + var gen = base.matchGenerator('foo'); + assert(gen); + assert.equal(gen.env.name, 'generate-foo'); + assert.equal(gen.env.alias, 'foo'); + }); + + it('should match a generator by path', function() { + base.register('generate-foo'); + var gen = base.matchGenerator(require.resolve('generate-foo')); + assert(gen); + assert.equal(gen.env.name, 'generate-foo'); + assert.equal(gen.env.alias, 'foo'); + }); +}); diff --git a/test/app.page.js b/test/app.page.js new file mode 100644 index 0000000..0f314f1 --- /dev/null +++ b/test/app.page.js @@ -0,0 +1,31 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var assemble = require('..'); +var app; + +describe('.page()', function() { + beforeEach(function() { + app = assemble(); + if (!app.pages) { + app.create('pages'); + } + }); + + describe('add page', function() { + it('should add pages to `app.views.pages`:', function() { + app.page('a.hbs', {path: 'a.hbs', contents: new Buffer('a')}); + app.page('b.hbs', {path: 'b.hbs', contents: new Buffer('b')}); + app.page('c.hbs', {path: 'c.hbs', contents: new Buffer('c')}); + assert(Object.keys(app.views.pages).length === 3); + }); + }); + + describe('load', function() { + it('should load a page from a non-glob filepath', function() { + app.page('test/fixtures/pages/a.hbs'); + assert(Object.keys(app.views.pages).length === 1); + }); + }); +}); diff --git a/test/app.pages.js b/test/app.pages.js new file mode 100644 index 0000000..1f773a2 --- /dev/null +++ b/test/app.pages.js @@ -0,0 +1,35 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var loader = require('assemble-loader'); +var update = require('..'); +var app; + +describe('.pages()', function() { + beforeEach(function() { + app = update({cli: true}); + app.use(loader()); + if (!app.pages) { + app.create('pages'); + } + }); + + describe('add pages', function() { + it('should add pages to `app.views.pages`:', function() { + app.pages({ + 'a.hbs': {path: 'a.hbs', contents: new Buffer('a')}, + 'b.hbs': {path: 'b.hbs', contents: new Buffer('b')}, + 'c.hbs': {path: 'c.hbs', contents: new Buffer('c')}, + }); + assert.equal(Object.keys(app.views.pages).length, 3); + }); + }); + + describe('load pages', function() { + it('should load pages onto `app.views.pages`:', function() { + app.pages('test/fixtures/pages/*.hbs'); + assert.equal(Object.keys(app.views.pages).length, 3); + }); + }); +}); diff --git a/test/app.partial.js b/test/app.partial.js new file mode 100644 index 0000000..ba6ca57 --- /dev/null +++ b/test/app.partial.js @@ -0,0 +1,24 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var assemble = require('..'); +var app; + +describe('.partial()', function() { + beforeEach(function() { + app = assemble(); + if (!app.partials) { + app.create('partials', {viewType: 'partial'}); + } + }); + + describe('add partial', function() { + it('should add partials to `app.views.partials`:', function() { + app.partial('a.hbs', {path: 'a.hbs', contents: new Buffer('a')}); + app.partial('b.hbs', {path: 'b.hbs', contents: new Buffer('b')}); + app.partial('c.hbs', {path: 'c.hbs', contents: new Buffer('c')}); + assert(Object.keys(app.views.partials).length === 3); + }); + }); +}); diff --git a/test/app.partials.js b/test/app.partials.js new file mode 100644 index 0000000..9622fcf --- /dev/null +++ b/test/app.partials.js @@ -0,0 +1,27 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var assemble = require('..'); +var app; + +describe('.partials()', function() { + beforeEach(function() { + app = assemble(); + if (!app.partials) { + app.create('partials', {viewType: 'partial'}); + } + }); + + describe('add partials', function() { + it('should add partials to `app.views.partials`:', function() { + app.partials({ + 'a.hbs': {path: 'a.hbs', contents: new Buffer('a')}, + 'b.hbs': {path: 'b.hbs', contents: new Buffer('b')}, + 'c.hbs': {path: 'c.hbs', contents: new Buffer('c')}, + }); + + assert(Object.keys(app.views.partials).length === 3); + }); + }); +}); diff --git a/test/app.questions.js b/test/app.questions.js new file mode 100644 index 0000000..1c36472 --- /dev/null +++ b/test/app.questions.js @@ -0,0 +1,293 @@ +'use strict'; + +process.env.NODE_ENV = 'test'; + +require('mocha'); +var fs = require('fs'); +var assert = require('assert'); +var questions = require('base-questions'); +var config = require('base-config-process'); +var store = require('base-store'); +var App = require('..'); +var app, base, site; + +describe('app.questions', function() { + describe('plugin', function() { + beforeEach(function() { + base = new App(); + base.use(config()); + base.use(questions()); + base.use(store('base-questions-tests/base')); + + app = new App(); + app.use(config()); + app.use(questions()); + app.use(store('base-questions-tests/app')); + }); + + it('should expose a `questions` object on "app"', function() { + assert.equal(typeof app.questions, 'object'); + }); + + it('should expose a `set` method on "app.questions"', function() { + assert.equal(typeof app.questions.set, 'function'); + }); + it('should expose a `get` method on "app.questions"', function() { + assert.equal(typeof app.questions.get, 'function'); + }); + it('should expose an `ask` method on "app.questions"', function() { + assert.equal(typeof app.questions.ask, 'function'); + }); + + it('should expose an `ask` method on "app"', function() { + assert.equal(typeof app.ask, 'function'); + }); + it('should expose a `question` method on "app"', function() { + assert.equal(typeof app.question, 'function'); + }); + }); + + if (process.env.TRAVIS) { + return; + + describe('app.ask', function() { + beforeEach(function() { + app = new App(); + app.use(config()); + app.use(questions()); + app.use(store('base-questions-tests/ask')); + }); + + afterEach(function() { + app.store.del({force: true}); + app.questions.clear(); + app.cache.data = {}; + }); + + it.skip('should force all questions to be asked', function(cb) { + app.questions.option('init', 'author'); + app.ask({force: true}, function(err, answers) { + console.log(answers) + cb(); + }); + }); + + it('should store a question:', function() { + app.question('a', 'b'); + assert(app.questions); + assert(app.questions.cache); + assert(app.questions.cache.a); + assert.equal(app.questions.cache.a.name, 'a'); + assert.equal(app.questions.cache.a.message, 'b'); + }); + + it.skip('should re-init a specific question:', function(cb) { + this.timeout(20000); + app.question('a', 'b'); + app.question('c', 'd'); + app.question('e', 'f'); + app.data({a: 'b'}); + + app.questions.get('e') + .force() + + app.ask(function(err, answers) { + console.log(answers); + cb(); + }); + }); + + it('should ask a question defined on `ask`', function(cb) { + app.data('name', 'Brian Woodward'); + + app.ask('name', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.name, 'Brian Woodward'); + cb(); + }); + }); + + it('should ask a question and use a `cache.data` value to answer:', function(cb) { + app.question('a', 'this is a question'); + app.data('a', 'b'); + + app.ask('a', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.a, 'b'); + + app.data('a', 'zzz'); + app.ask('a', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.a, 'zzz'); + cb(); + }) + }); + }); + + it('should ask a question and use a `store.data` value to answer:', function(cb) { + app.question('a', 'this is another question'); + app.store.set('a', 'c'); + app.ask('a', function(err, answers) { + if (err) return cb(err); + assert(answers); + assert.equal(answers.a, 'c'); + cb(); + }) + }); + + it('should ask a question and use a config value to answer:', function(cb) { + app.question('a', 'b'); + app.config.process({data: {a: 'foo'}}, function(err) { + if (err) return cb(err); + + app.store.set('a', 'c'); + + app.ask('a', function(err, answer) { + if (err) return cb(err); + assert(answer); + assert.equal(answer.a, 'foo'); + cb(); + }); + }); + }); + + it('should prefer `cache.data` to `store.data`', function(cb) { + app.question('a', 'b'); + app.data('a', 'b'); + app.store.set('a', 'c'); + + app.ask('a', function(err, answer) { + if (err) return cb(err); + assert(answer); + assert.equal(answer.a, 'b'); + cb(); + }) + }); + + it('should update data with data loaded by config', function(cb) { + app.question('a', 'this is a question'); + app.data('a', 'b'); + + app.config.process({data: {a: 'foo'}}, function(err) { + if (err) return cb(err); + + app.ask('a', function(err, answer) { + if (err) return cb(err); + + assert(answer); + assert.equal(answer.a, 'foo'); + cb(); + }); + }); + }); + }); + + describe('session data', function() { + before(function() { + site = new App(); + site.use(config()); + site.use(questions()); + site.use(store('base-questions-tests/site')); + + app = new App(); + app.use(config()); + app.use(questions()); + app.use(store('base-questions-tests/ask')); + }); + + after(function() { + site.store.del({force: true}); + site.questions.clear(); + + app.store.del({force: true}); + app.questions.clear(); + }); + + it('[app] should ask a question and use a `cache.data` value to answer:', function(cb) { + app.question('package.name', 'this is a question'); + app.data('package.name', 'base-questions'); + + app.ask('package.name', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.package.name, 'base-questions'); + + app.data('package.name', 'foo-bar-baz'); + app.ask('package.name', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.package.name, 'foo-bar-baz'); + cb(); + }) + }); + }); + + it('[site] should ask a question and use a `cache.data` value to answer:', function(cb) { + site.question('package.name', 'this is a question'); + site.data('package.name', 'base-questions'); + + site.ask('package.name', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.package.name, 'base-questions'); + + site.data('package.name', 'foo-bar-baz'); + site.ask('package.name', function(err, answers) { + if (err) return cb(err) + assert.equal(answers.package.name, 'foo-bar-baz'); + cb(); + }) + }); + }); + + it('[app] should ask a question and use a `store.data` value to answer:', function(cb) { + app.question('author.name', 'this is another question'); + app.store.set('author.name', 'Brian Woodward'); + app.ask('author.name', function(err, answers) { + if (err) return cb(err); + assert(answers); + assert.equal(answers.author.name, 'Brian Woodward'); + cb(); + }) + }); + + it('[site] should ask a question and use a `store.data` value to answer:', function(cb) { + site.question('author.name', 'this is another question'); + site.store.set('author.name', 'Jon Schlinkert'); + site.ask('author.name', function(err, answers) { + if (err) return cb(err); + assert(answers); + assert.equal(answers.author.name, 'Brian Woodward'); + cb(); + }) + }); + + it('[app] should ask a question and use a config value to answer:', function(cb) { + app.question('foo', 'Username?'); + app.config.process({data: {foo: 'jonschlinkert'}}, function(err) { + if (err) return cb(err); + + app.store.set('foo', 'doowb'); + + app.ask('foo', function(err, answer) { + if (err) return cb(err); + assert(answer); + assert.equal(answer.foo, 'jonschlinkert'); + cb(); + }); + }); + }); + + it('[site] should ask a question and use a config value to answer:', function(cb) { + site.question('foo', 'Username?'); + site.config.process({data: {foo: 'doowb'}}, function(err) { + if (err) return cb(err); + + site.ask('foo', function(err, answer) { + if (err) return cb(err); + assert(answer); + assert.equal(answer.foo, 'doowb'); + cb(); + }); + }); + }); + }); + } +}); diff --git a/test/app.register.js b/test/app.register.js new file mode 100644 index 0000000..db12253 --- /dev/null +++ b/test/app.register.js @@ -0,0 +1,234 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var generators = require('base-generators'); +var Base = require('..'); +var base; + +var fixtures = path.resolve.bind(path, __dirname + '/fixtures'); + +describe('.register', function() { + beforeEach(function() { + base = new Base(); + }); + + describe('function', function() { + it('should register an updater a function', function() { + base.register('foo', function() {}); + var foo = base.getGenerator('foo'); + assert(foo); + assert.equal(foo.env.alias, 'foo'); + }); + + it('should get a task from an updater registered as a function', function() { + base.register('foo', function(foo) { + foo.task('default', function() {}); + }); + var updater = base.getGenerator('foo'); + assert(updater); + assert(updater.tasks); + assert(updater.tasks.hasOwnProperty('default')); + }); + + it('should get an updater from an updater registered as a function', function() { + base.register('foo', function(foo) { + foo.register('bar', function(bar) {}); + }); + var bar = base.getGenerator('foo.bar'); + assert(bar); + assert.equal(bar.env.alias, 'bar'); + }); + + it('should get a sub-updater from an updater registered as a function', function() { + base.register('foo', function(foo) { + foo.register('bar', function(bar) { + bar.task('something', function() {}); + }); + }); + var bar = base.getGenerator('foo.bar'); + assert(bar); + assert(bar.tasks); + assert(bar.tasks.hasOwnProperty('something')); + }); + + it('should get a deeply-nested sub-updater registered as a function', function() { + base.register('foo', function(foo) { + foo.register('bar', function(bar) { + bar.register('baz', function(baz) { + baz.register('qux', function(qux) { + qux.task('qux-one', function() {}); + }); + }); + }); + }); + + var qux = base.getGenerator('foo.bar.baz.qux'); + assert(qux); + assert(qux.tasks); + assert(qux.tasks.hasOwnProperty('qux-one')); + }); + + it('should expose the instance from each updater', function() { + base.register('foo', function(foo) { + foo.register('bar', function(bar) { + bar.register('baz', function(baz) { + baz.register('qux', function(qux) { + qux.task('qux-one', function() {}); + }); + }); + }); + }); + + var qux = base + .getGenerator('foo') + .getGenerator('bar') + .getGenerator('baz') + .getGenerator('qux'); + + assert(qux); + assert(qux.tasks); + assert(qux.tasks.hasOwnProperty('qux-one')); + }); + + it('should fail when an updater that does not exist is defined', function() { + base.register('foo', function(foo) { + foo.register('bar', function(bar) { + bar.register('baz', function(baz) { + baz.register('qux', function(qux) { + }); + }); + }); + }); + var fez = base.getGenerator('foo.bar.fez'); + assert.equal(typeof fez, 'undefined'); + }); + + it('should expose the `base` instance as the second param', function(cb) { + base.register('foo', function(foo, base) { + assert(base.updaters.hasOwnProperty('foo')); + cb(); + }); + base.getGenerator('foo'); + }); + + it('should expose sibling updaters on the `base` instance', function(cb) { + base.register('foo', function(foo, base) { + foo.task('abc', function() {}); + }); + base.register('bar', function(bar, base) { + assert(base.updaters.hasOwnProperty('foo')); + assert(base.updaters.hasOwnProperty('bar')); + cb(); + }); + + base.getGenerator('foo'); + base.getGenerator('bar'); + }); + }); + + describe('alias', function() { + it('should use a custom function to create the alias', function() { + base.option('toAlias', function(name) { + return name.slice(name.lastIndexOf('-') + 1); + }); + + base.register('base-abc-xyz', function() {}); + var xyz = base.getGenerator('xyz'); + assert(xyz); + assert.equal(xyz.env.alias, 'xyz'); + }); + }); + + describe('path', function() { + it('should register an updater function by name', function() { + base.register('foo', function() {}); + assert(base.updaters.hasOwnProperty('foo')); + }); + + it('should register an updater function by alias', function() { + base.register('abc', function() {}); + assert(base.updaters.hasOwnProperty('abc')); + }); + + it('should register an updater by dirname', function() { + base.register('a', fixtures('updaters/a')); + assert(base.updaters.hasOwnProperty('a')); + }); + + it('should register an updater from a configfile filepath', function() { + base.register('base-abc', fixtures('updaters/a/updatefile.js')); + assert(base.updaters.hasOwnProperty('base-abc')); + }); + + it('should throw when an updater does not expose the instance', function(cb) { + try { + base.register('not-exposed', require(fixtures('not-exposed.js'))); + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, `cannot resolve: 'not-exposed'`); + cb(); + } + }); + }); + + describe('instance', function() { + it('should register an instance', function() { + base.register('base-inst', new Base()); + assert(base.updaters.hasOwnProperty('base-inst')); + }); + + it('should get an updater that was registered as an instance', function() { + var foo = new Base(); + foo.task('default', function() {}); + base.register('foo', foo); + assert(base.getGenerator('foo')); + }); + + it('should register multiple instances', function() { + var foo = new Base(); + var bar = new Base(); + var baz = new Base(); + base.register('foo', foo); + base.register('bar', bar); + base.register('baz', baz); + assert(base.getGenerator('foo')); + assert(base.getGenerator('bar')); + assert(base.getGenerator('baz')); + }); + + it('should get tasks from an updater that was registered as an instance', function() { + var foo = new Base(); + foo.task('default', function() {}); + base.register('foo', foo); + var updater = base.getGenerator('foo'); + assert(updater); + assert(updater.tasks.hasOwnProperty('default')); + }); + + it('should get sub-updaters from an updater registered as an instance', function() { + var foo = new Base(); + foo.use(generators()); + foo.register('bar', function() {}); + base.register('foo', foo); + var updater = base.getGenerator('foo.bar'); + assert(updater); + }); + + it('should get tasks from sub-updaters registered as an instance', function() { + var foo = new Base(); + foo.use(generators()); + + foo.options.isFoo = true; + foo.register('bar', function(bar) { + bar.task('whatever', function() {}); + }); + + base.register('foo', foo); + var updater = base.getGenerator('foo.bar'); + assert(updater.tasks); + assert(updater.tasks.hasOwnProperty('whatever')); + }); + }); +}); diff --git a/test/app.store.js b/test/app.store.js new file mode 100644 index 0000000..7c44e4e --- /dev/null +++ b/test/app.store.js @@ -0,0 +1,335 @@ +'use strict'; + +require('mocha'); +var fs = require('fs'); +var path = require('path'); +var assert = require('assert'); +var store = require('base-store'); +var App = require('..'); +var app; + +describe('store', function() { + describe('methods', function() { + beforeEach(function() { + app = new App(); + app.use(store()); + app.store.create('app-data-tests'); + }); + + afterEach(function() { + app.store.data = {}; + app.store.del({force: true}); + app.options.cli = false; + }); + + it('should `.set()` a value on the store', function() { + app.store.set('one', 'two'); + assert.equal(app.store.data.one, 'two'); + }); + + it('should `.set()` an object', function() { + app.store.set({four: 'five', six: 'seven'}); + assert.equal(app.store.data.four, 'five'); + assert.equal(app.store.data.six, 'seven'); + }); + + it('should `.set()` a nested value', function() { + app.store.set('a.b.c.d', {e: 'f'}); + assert.equal(app.store.data.a.b.c.d.e, 'f'); + }); + + it('should `.union()` a value on the store', function() { + app.store.union('one', 'two'); + assert.deepEqual(app.store.data.one, ['two']); + }); + + it('should not union duplicate values', function() { + app.store.union('one', 'two'); + assert.deepEqual(app.store.data.one, ['two']); + + app.store.union('one', ['two']); + assert.deepEqual(app.store.data.one, ['two']); + }); + + it('should concat an existing array:', function() { + app.store.union('one', 'a'); + assert.deepEqual(app.store.data.one, ['a']); + + app.store.union('one', ['b']); + assert.deepEqual(app.store.data.one, ['a', 'b']); + + app.store.union('one', ['c', 'd']); + assert.deepEqual(app.store.data.one, ['a', 'b', 'c', 'd']); + }); + + it('should return true if a key `.has()` on the store', function() { + app.store.set('foo', 'bar'); + app.store.set('baz', null); + app.store.set('qux', undefined); + + assert(app.store.has('foo')); + assert(app.store.has('baz')); + assert(!app.store.has('bar')); + assert(!app.store.has('qux')); + }); + + it('should return true if a nested key `.has()` on the store', function() { + app.store.set('a.b.c.d', {x: 'zzz'}); + app.store.set('a.b.c.e', {f: null}); + app.store.set('a.b.g.j', {k: undefined}); + + assert(!app.store.has('a.b.bar')); + assert(app.store.has('a.b.c.d')); + assert(app.store.has('a.b.c.d.x')); + assert(!app.store.has('a.b.c.d.z')); + assert(app.store.has('a.b.c.e')); + assert(app.store.has('a.b.c.e.f')); + assert(!app.store.has('a.b.c.e.z')); + assert(app.store.has('a.b.g.j')); + assert(!app.store.has('a.b.g.j.k')); + assert(!app.store.has('a.b.g.j.z')); + }); + + it('should return true if a key exists `.hasOwn()` on the store', function() { + app.store.set('foo', 'bar'); + app.store.set('baz', null); + app.store.set('qux', undefined); + + assert(app.store.hasOwn('foo')); + assert(!app.store.hasOwn('bar')); + assert(app.store.hasOwn('baz')); + assert(app.store.hasOwn('qux')); + }); + + it('should return true if a nested key exists `.hasOwn()` on the store', function() { + app.store.set('a.b.c.d', {x: 'zzz'}); + app.store.set('a.b.c.e', {f: null}); + app.store.set('a.b.g.j', {k: undefined}); + + assert(!app.store.hasOwn('a.b.bar')); + assert(app.store.hasOwn('a.b.c.d')); + assert(app.store.hasOwn('a.b.c.d.x')); + assert(!app.store.hasOwn('a.b.c.d.z')); + assert(app.store.has('a.b.c.e.f')); + assert(app.store.hasOwn('a.b.c.e.f')); + assert(!app.store.hasOwn('a.b.c.e.bar')); + assert(!app.store.has('a.b.g.j.k')); + assert(app.store.hasOwn('a.b.g.j.k')); + assert(!app.store.hasOwn('a.b.g.j.foo')); + }); + + it('should `.get()` a stored value', function() { + app.store.set('three', 'four'); + assert.equal(app.store.get('three'), 'four'); + }); + + it('should `.get()` a nested value', function() { + app.store.set({a: {b: {c: 'd'}}}); + assert.equal(app.store.get('a.b.c'), 'd'); + }); + + it('should `.del()` a stored value', function() { + app.store.set('a', 'b'); + app.store.set('c', 'd'); + assert(app.store.data.hasOwnProperty('a')); + assert(app.store.data.hasOwnProperty('c')); + + app.store.del('a'); + app.store.del('c'); + assert(!app.store.data.hasOwnProperty('a')); + assert(!app.store.data.hasOwnProperty('c')); + }); + + it('should `.del()` multiple stored values', function() { + app.store.set('a', 'b'); + app.store.set('c', 'd'); + app.store.set('e', 'f'); + app.store.del(['a', 'c', 'e']); + assert.deepEqual(app.store.data, {}); + }); + }); +}); + +describe('create', function() { + beforeEach(function() { + app = new App({cli: true}); + app.use(store()); + app.store.create('abc'); + + // init the actual store json file + app.store.set('a', 'b'); + }); + + afterEach(function() { + app.store.data = {}; + app.store.del({force: true}); + app.options.cli = false; + }); + + it('should expose a `create` method', function() { + assert.equal(typeof app.store.create, 'function'); + }); + + it('should create a "sub-store" with the given name', function() { + var store = app.store.create('created'); + assert.equal(store.name, 'created'); + }); + + it('should create a "sub-store" with the project name when no name is passed', function() { + var store = app.store.create('app-store'); + assert.equal(store.name, 'app-store'); + }); + + it('should throw an error when a conflicting store name is used', function(cb) { + try { + app.store.create('create'); + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, 'Cannot create store: "create", since "create" is a reserved property key. Please choose a different store name.'); + cb(); + } + }); + + it('should add a store object to store[name]', function() { + app.store.create('foo'); + assert.equal(typeof app.store.foo, 'object'); + assert.equal(typeof app.store.foo.set, 'function'); + app.store.foo.del({force: true}); + }); + + it('should save the store in a namespaced directory under the parent', function() { + app.store.create('foo'); + var dir = path.dirname(app.store.path); + + assert.equal(app.store.foo.path, path.join(dir, 'update/foo.json')); + app.store.foo.set('a', 'b'); + app.store.foo.del({force: true}); + }); + + it('should set values on the custom store', function() { + app.store.create('foo'); + app.store.foo.set('a', 'b'); + assert.equal(app.store.foo.data.a, 'b'); + app.store.foo.del({force: true}); + }); + + it('should get values from the custom store', function() { + app.store.create('foo'); + app.store.foo.set('a', 'b'); + assert.equal(app.store.foo.get('a'), 'b'); + app.store.foo.del({force: true}); + }); +}); + +describe('events', function() { + beforeEach(function() { + app = new App({cli: true}); + app.use(store()); + app.store.create('abc'); + }); + + afterEach(function() { + app.store.data = {}; + app.store.del({force: true}); + app.options.cli = false; + }); + + it('should emit `set` when an object is set:', function() { + var keys = []; + app.store.on('set', function(key) { + keys.push(key); + }); + + app.store.set({a: {b: {c: 'd'}}}); + assert.deepEqual(keys, ['a']); + }); + + it('should emit `set` when a key/value pair is set:', function() { + var keys = []; + + app.store.on('set', function(key) { + keys.push(key); + }); + + app.store.set('a', 'b'); + assert.deepEqual(keys, ['a']); + }); + + it('should emit `set` when an object value is set:', function() { + var keys = []; + + app.store.on('set', function(key) { + keys.push(key); + }); + + app.store.set('a', {b: 'c'}); + assert.deepEqual(keys, ['a']); + }); + + it('should emit `set` when an array of objects is passed:', function(cb) { + var keys = []; + + app.store.on('set', function(key) { + keys.push(key); + }); + + app.store.set([{a: 'b'}, {c: 'd'}]); + assert.deepEqual(keys, ['a', 'c']); + cb(); + }); + + it('should emit `has`:', function(cb) { + var keys = []; + + app.store.on('has', function(val) { + assert(val); + cb(); + }); + + app.store.set('a', 'b'); + app.store.has('a'); + }); + + it('should emit `del` when a value is delted:', function(cb) { + app.store.on('del', function(keys) { + assert.deepEqual(keys, 'a'); + assert.equal(typeof app.store.get('a'), 'undefined'); + cb(); + }); + + app.store.set('a', {b: 'c'}); + assert.deepEqual(app.store.get('a'), {b: 'c'}); + app.store.del('a'); + }); + + it('should emit deleted keys on `del`:', function(cb) { + var arr = []; + + app.store.on('del', function(key) { + arr.push(key); + assert.equal(Object.keys(app.store.data).length, 0); + }); + + app.store.set('a', 'b'); + app.store.set('c', 'd'); + app.store.set('e', 'f'); + + app.store.del({force: true}); + assert.deepEqual(arr, ['a', 'c', 'e']); + cb(); + }); + + it('should throw an error if force is not passed', function(cb) { + app.store.set('a', 'b'); + app.store.set('c', 'd'); + app.store.set('e', 'f'); + + try { + app.store.del(); + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, 'options.force is required to delete the entire cache.'); + cb(); + } + }); +}); diff --git a/test/app.task.js b/test/app.task.js new file mode 100644 index 0000000..68ad212 --- /dev/null +++ b/test/app.task.js @@ -0,0 +1,191 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var Base = require('..'); +var base; + +describe('.generate', function() { + beforeEach(function() { + base = new Base(); + }); + + it('should register a task', function() { + var fn = function(cb) { + cb(); + }; + base.task('default', fn); + assert.equal(typeof base.tasks.default, 'object'); + assert.equal(base.tasks.default.fn, fn); + }); + + it('should register a task with an array of dependencies', function(cb) { + var count = 0; + base.task('foo', function(next) { + count++; + next(); + }); + base.task('bar', function(next) { + count++; + next(); + }); + base.task('default', ['foo', 'bar'], function(next) { + count++; + next(); + }); + assert.equal(typeof base.tasks.default, 'object'); + assert.deepEqual(base.tasks.default.deps, ['foo', 'bar']); + base.build('default', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run a glob of tasks', function(cb) { + var count = 0; + base.task('foo', function(next) { + count++; + next(); + }); + base.task('bar', function(next) { + count++; + next(); + }); + base.task('baz', function(next) { + count++; + next(); + }); + base.task('qux', function(next) { + count++; + next(); + }); + base.task('default', ['b*']); + assert.equal(typeof base.tasks.default, 'object'); + base.build('default', function(err) { + if (err) return cb(err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should register a task with a list of strings as dependencies', function() { + base.task('default', 'foo', 'bar', function(cb) { + cb(); + }); + assert.equal(typeof base.tasks.default, 'object'); + assert.deepEqual(base.tasks.default.deps, ['foo', 'bar']); + }); + + it('should run a task', function(cb) { + var count = 0; + base.task('default', function(cb) { + count++; + cb(); + }); + + base.build('default', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should throw an error when a task with unregistered dependencies is run', function(cb) { + base.task('default', ['foo', 'bar']); + base.build('default', function(err) { + assert(err); + cb(); + }); + }); + + it('should throw an error when `.build` is called without a callback function.', function() { + try { + base.build('default'); + throw new Error('Expected an error to be thrown.'); + } catch (err) { + } + }); + + it('should emit task events', function(cb) { + var events = []; + base.on('task:starting', function(task) { + events.push('starting.' + task.name); + }); + base.on('task:finished', function(task) { + events.push('finished.' + task.name); + }); + base.on('task:error', function(e, task) { + events.push('error.' + task.name); + }); + base.task('foo', function(cb) { + cb(); + }); + base.task('bar', ['foo'], function(cb) { + cb(); + }); + base.task('default', ['bar']); + base.build('default', function(err) { + if (err) return cb(err); + assert.deepEqual(events, [ + 'starting.default', + 'starting.bar', + 'starting.foo', + 'finished.foo', + 'finished.bar', + 'finished.default' + ]); + cb(); + }); + }); + + it('should emit an error event when an error is passed back in a task', function(cb) { + base.on('error', function(err) { + assert(err); + assert.equal(err.message, 'This is an error'); + }); + base.task('default', function(cb) { + return cb(new Error('This is an error')); + }); + base.build('default', function(err) { + if (err) return cb(); + cb(new Error('Expected an error')); + }); + }); + + it('should emit an error event when an error is thrown in a task', function(cb) { + base.on('error', function(err) { + assert(err); + assert.equal(err.message, 'This is an error'); + }); + base.task('default', function(cb) { + cb(new Error('This is an error')); + }); + base.build('default', function(err) { + assert(err); + cb(); + }); + }); + + it('should run dependencies before running the dependent task.', function(cb) { + var seq = []; + base.task('foo', function(cb) { + seq.push('foo'); + cb(); + }); + base.task('bar', function(cb) { + seq.push('bar'); + cb(); + }); + base.task('default', ['foo', 'bar'], function(cb) { + seq.push('default'); + cb(); + }); + + base.build('default', function(err) { + if (err) return cb(err); + assert.deepEqual(seq, ['foo', 'bar', 'default']); + cb(); + }); + }); +}); diff --git a/test/app.toAlias.js b/test/app.toAlias.js new file mode 100644 index 0000000..e5f8bf4 --- /dev/null +++ b/test/app.toAlias.js @@ -0,0 +1,41 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var Base = require('..'); +var base; + +describe('.toAlias', function() { + beforeEach(function() { + base = new Base(); + }); + + it('should not create an alias when no prefix is given', function() { + assert.equal(base.toAlias('foo-bar'), 'foo-bar'); + }); + + it('should create an alias using the `options.toAlias` function', function() { + var alias = base.toAlias('one-two-three', { + toAlias: function(name) { + return name.slice(name.lastIndexOf('-') + 1); + } + }); + assert.equal(alias, 'three'); + }); + + it('should create an alias using the given function', function() { + var alias = base.toAlias('one-two-three', function(name) { + return name.slice(name.lastIndexOf('-') + 1); + }); + assert.equal(alias, 'three'); + }); + + it('should create an alias using base.options.toAlias function', function() { + base.options.toAlias = function(name) { + return name.slice(name.lastIndexOf('-') + 1); + }; + + var alias = base.toAlias('one-two-three'); + assert.equal(alias, 'three'); + }); +}); diff --git a/test/app.update-array.js b/test/app.update-array.js new file mode 100644 index 0000000..1964250 --- /dev/null +++ b/test/app.update-array.js @@ -0,0 +1,931 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var config = require('base-config-process'); +var Base = require('..'); +var base; + +describe('.generate', function() { + beforeEach(function() { + base = new Base(); + }); + + describe('config.process', function(cb) { + it('should run tasks when the base-config plugin is used', function(cb) { + base.use(config()); + var count = 0; + base.task('default', function(next) { + count++; + next(); + }); + + base.generate('default', function(err) { + assert(!err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run handle errors when the base-config plugin is used', function(cb) { + base.use(config()); + var count = 0; + base.task('default', function(next) { + count++; + next(new Error('fooo')); + }); + + base.generate('default', function(err) { + assert.equal(err.message, 'fooo'); + assert.equal(count, 1); + cb(); + }); + }); + + it('should handle config errors when the base-config plugin is used', function(cb) { + base.use(config()); + var count = 0; + base.config.map('foo', function(val, key, config, next) { + count++; + next(new Error('fooo')); + }); + + base.set('cache.config', {foo: true}); + + base.task('default', function(next) { + count--; + next(); + }); + + base.generate('default', function(err) { + assert(err); + assert.equal(err.message, 'fooo'); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('generators', function(cb) { + it('should throw an error when a generator is not found', function(cb) { + base.generate('fdsslsllsfjssl', function(err) { + assert(err); + assert.equal('Cannot find generator: "fdsslsllsfjssl"', err.message); + cb(); + }); + }); + + it('should *not* throw an error when the default task is not found', function(cb) { + base.register('foo', function() {}); + base.generate('foo:default', function(err) { + assert(!err); + cb(); + }); + }); + + it('should not throw an error when a default generator is not found', function(cb) { + base.generate('default', function(err) { + assert(!err); + cb(); + }); + }); + + it('should not throw an error when a default task and default generator is not found', function(cb) { + base.generate('default:default', function(err) { + assert(!err); + cb(); + }); + }); + + // special case + it('should throw an error when a generator is not found in argv.cwd', function(cb) { + base.option('cwd', 'foo/bar/baz'); + base.generate('sflsjljskksl', function(err) { + assert(err); + assert.equal('Cannot find generator: "sflsjljskksl" in "foo/bar/baz"', err.message); + cb(); + }); + }); + + it('should not reformat error messages that are not about invalid tasks', function(cb) { + base.task('default', function(cb) { + cb(new Error('whatever')); + }); + + base.generate('default', function(err) { + assert(err); + assert.equal(err.message, 'whatever'); + cb(); + }); + }); + + it('should not throw an error when the default task is not defined', function(cb) { + base.register('foo', function() {}); + base.register('bar', function() {}); + base.generate('foo', ['default'], function(err) { + if (err) return cb(err); + + base.generate('bar', function(err) { + if (err) return cb(err); + + cb(); + }); + }); + }); + + it('should run a task on the instance', function(cb) { + base.task('abc123', function(next) { + next(); + }); + + base.generate('abc123', function(err) { + assert(!err); + cb(); + }); + }); + + it('should run same-named task instead of a generator', function(cb) { + base.register('123xyz', function(app) { + cb(new Error('expected the task to run first')); + }); + + base.task('123xyz', function() { + cb(); + }); + + base.generate('123xyz', function(err) { + assert(!err); + }); + }); + + it('should run a task instead of a generator with a default task', function(cb) { + base.register('123xyz', function(app) { + app.task('default', function() { + cb(new Error('expected the task to run first')); + }); + }); + base.task('123xyz', function() { + cb(); + }); + base.generate('123xyz', function(err) { + assert(!err); + }); + }); + + it('should run a task on a same-named generator when the task is specified', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.task('foo', function() { + cb(new Error('expected the generator to run')); + }); + + base.generate('foo:default', function(err) { + assert(!err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of tasks that includes a same-named generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.register('bar', function(app) { + app.task('baz', function(next) { + count++; + next(); + }); + }); + + base.task('foo', function() { + cb(new Error('expected the generator to run')); + }); + + base.generate(['foo:default', 'bar:baz'], function(err) { + assert(!err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should run a generator from a task with the same name', function(cb) { + base.register('foo', function(app) { + app.task('default', function() { + cb(); + }); + }); + + base.task('foo', function(cb) { + base.generate('foo', cb); + }); + + base.build('foo', function(err) { + if (err) cb(err); + }); + }); + + it('should run the default task on a generator', function(cb) { + base.register('foo', function(app) { + app.task('default', function(next) { + next(); + }); + }); + + base.generate('foo', function(err) { + assert(!err); + cb(); + }); + }); + + it('should run a stringified array of tasks on the instance', function(cb) { + var count = 0; + base.task('a', function(next) { + count++; + next(); + }); + base.task('b', function(next) { + count++; + next(); + }); + base.task('c', function(next) { + count++; + next(); + }); + + base.generate('a,b,c', function(err) { + assert.equal(count, 3); + assert(!err); + cb(); + }); + }); + + it('should run an array of tasks on the instance', function(cb) { + var count = 0; + base.task('a', function(next) { + count++; + next(); + }); + base.task('b', function(next) { + count++; + next(); + }); + base.task('c', function(next) { + count++; + next(); + }); + + base.generate(['a', 'b', 'c'], function(err) { + if (err) return cb(err); + assert.equal(count, 3); + assert(!err); + cb(); + }); + }); + + it('should run the default task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of generators', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.register('bar', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate(['foo', 'bar'], function(err) { + if (err) return cb(err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should run the default task on the default generator', function(cb) { + var count = 0; + base.register('default', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate(function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the specified task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('abc', function(next) { + count++; + next(); + }); + }); + + base.generate('foo:abc', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of tasks on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('a', function(next) { + count++; + next(); + }); + + app.task('b', function(next) { + count++; + next(); + }); + + app.task('c', function(next) { + count++; + next(); + }); + }); + + base.generate('foo:a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run the default tasks on an array of generators', function(cb) { + var count = 0; + base.register('a', function(app) { + this.task('default', function(cb) { + count++; + cb(); + }); + }); + + base.register('b', function(app) { + this.task('default', function(cb) { + count++; + cb(); + }); + }); + + base.register('c', function(app) { + this.task('default', function(cb) { + count++; + cb(); + }); + }); + + base.generate(['a', 'b', 'c'], function(err) { + if (err) return cb(err); + assert.equal(count, 3); + assert(!err); + cb(); + }); + }); + }); + + describe('options', function(cb) { + it('should pass options to generator.options', function(cb) { + var count = 0; + base.register('default', function(app, base, env, options) { + app.task('default', function(next) { + count++; + assert.equal(app.options.foo, 'bar'); + next(); + }); + }); + + base.generate({foo: 'bar'}, function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should expose options on generator options', function(cb) { + var count = 0; + base.register('default', function(app, base, env, options) { + app.task('default', function(next) { + count++; + assert.equal(options.foo, 'bar'); + next(); + }); + }); + + base.generate({foo: 'bar'}, function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should not mutate options on parent instance', function(cb) { + var count = 0; + base.register('default', function(app, base, env, options) { + app.task('default', function(next) { + count++; + assert.equal(options.foo, 'bar'); + assert(!base.options.foo); + next(); + }); + }); + + base.generate({foo: 'bar'}, function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('default tasks', function(cb) { + it('should run the default task on the default generator', function(cb) { + var count = 0; + base.register('default', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate(function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the default task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('specified tasks', function(cb) { + it('should run the specified task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('abc', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of tasks on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('a', function(next) { + count++; + next(); + }); + + app.task('b', function(next) { + count++; + next(); + }); + + app.task('c', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', 'a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + }); + + describe('sub-generators', function(cb) { + it('should run the default task on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the specified task string on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub:abc', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the specified task array on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an of stringified-tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.bar:a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run an array of tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.bar', ['a', 'b', 'c'], function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run an multiple tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.bar', 'a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run an multiple tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.register('qux', function(app) { + app.register('fez', function(fez) { + fez.task('default', function(next) { + count++; + next(); + }); + + fez.task('a', function(next) { + count++; + next(); + }); + + fez.task('b', function(next) { + count++; + next(); + }); + + fez.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate(['foo.bar:a,b,c', 'qux.fez:a,b,c'], function(err) { + if (err) return cb(err); + assert.equal(count, 6); + cb(); + }); + }); + }); + + describe('cross-generator', function(cb) { + it('should run a generator from another generator', function(cb) { + var res = ''; + + base.register('foo', function(app, two) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + res += 'foo > sub > default '; + base.generate('bar.sub', next); + }); + }); + }); + + base.register('bar', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + res += 'bar > sub > default '; + next(); + }); + }); + }); + + base.generate('foo.sub', function(err) { + if (err) return cb(err); + assert.equal(res, 'foo > sub > default bar > sub > default '); + cb(); + }); + }); + + it('should run the specified task on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub:abc', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('events', function(cb) { + it('should emit generate', function(cb) { + var count = 0; + + base.on('generate', function() { + count++; + }); + + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should expose the generator alias as the first parameter', function(cb) { + base.on('generate', function(name) { + assert.equal(name, 'sub'); + cb(); + }); + + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + next(); + }); + + sub.task('abc', function(next) { + next(); + }); + }); + }); + + base.generate('foo.sub:abc', function(err) { + if (err) return cb(err); + }); + }); + + it('should expose the tasks array as the second parameter', function(cb) { + base.on('generate', function(name, tasks) { + assert.deepEqual(tasks, ['abc']); + cb(); + }); + + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + next(); + }); + + sub.task('abc', function(next) { + next(); + }); + }); + }); + + base.generate('foo.sub:abc', function(err) { + if (err) return cb(err); + }); + }); + }); +}); diff --git a/test/app.update.js b/test/app.update.js new file mode 100644 index 0000000..b1403d9 --- /dev/null +++ b/test/app.update.js @@ -0,0 +1,961 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var config = require('base-config-process'); +var Base = require('..'); +var base; + +describe('.generate', function() { + beforeEach(function() { + base = new Base(); + }); + + describe('config.process', function(cb) { + it('should run tasks when the base-config plugin is used', function(cb) { + base.use(config()); + var count = 0; + base.task('default', function(next) { + count++; + next(); + }); + + base.generate('default', function(err) { + assert(!err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should handle errors when the base-config plugin is used', function(cb) { + base.use(config()); + var count = 0; + base.task('default', function(next) { + count++; + next(new Error('fooo')); + }); + + base.generate('default', function(err) { + assert.equal(err.message, 'fooo'); + assert.equal(count, 1); + cb(); + }); + }); + + it('should handle config errors when the base-config plugin is used', function(cb) { + base.use(config()); + var count = 0; + + base.config.map('foo', function(val, key, config, next) { + count++; + next(new Error('fooo')); + }); + + base.set('cache.config', {foo: true}); + base.task('default', function(next) { + count--; + next(); + }); + + base.generate('default', function(err) { + assert(err); + assert.equal(err.message, 'fooo'); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('generators', function(cb) { + it('should throw an error when a generator is not found', function(cb) { + base.generate('fdsslsllsfjssl', function(err) { + assert(err); + assert.equal('Cannot find generator: "fdsslsllsfjssl"', err.message); + cb(); + }); + }); + + it('should *not* throw an error when the default task is not found', function(cb) { + base.register('foo', function() {}); + base.generate('foo:default', function(err) { + assert(!err); + cb(); + }); + }); + + it('should not throw an error when a default generator is not found', function(cb) { + base.generate('default', function(err) { + assert(!err); + cb(); + }); + }); + + it('should not throw an error when a default task and default generator is not found', function(cb) { + base.generate('default:default', function(err) { + assert(!err); + cb(); + }); + }); + + // special case + it('should throw an error when a generator is not found in argv.cwd', function(cb) { + base.option('cwd', 'foo/bar/baz'); + base.generate('sflsjljskksl', function(err) { + assert(err); + assert.equal('Cannot find generator: "sflsjljskksl" in "foo/bar/baz"', err.message); + cb(); + }); + }); + + it('should throw an error when a task is not found (task array)', function(cb) { + var fn = console.error; + var res = []; + console.error = function(msg) { + res.push(msg); + }; + base.register('fdsslsllsfjssl', function() {}); + base.generate('fdsslsllsfjssl', ['foo'], function(err) { + console.error = fn; + if (err) return cb(err); + assert.equal(res[0], 'Cannot find task: "foo" in generator: "fdsslsllsfjssl"'); + cb(); + }); + }); + + it('should throw an error when a task is not found (task string)', function(cb) { + var fn = console.error; + var res = []; + console.error = function(msg) { + res.push(msg); + }; + base.register('fdsslsllsfjssl', function() {}); + base.generate('fdsslsllsfjssl:foo', function(err) { + console.error = fn; + if (err) return cb(err); + assert.equal(res[0], 'Cannot find task: "foo" in generator: "fdsslsllsfjssl"'); + cb(); + }); + }); + + it('should not reformat error messages that are not about invalid tasks', function(cb) { + base.task('default', function(cb) { + cb(new Error('whatever')); + }); + + base.generate('default', function(err) { + assert(err); + assert.equal(err.message, 'whatever'); + cb(); + }); + }); + + it('should not throw an error when the default task is not defined', function(cb) { + base.register('foo', function() {}); + base.register('bar', function() {}); + base.generate('foo', ['default'], function(err) { + if (err) return cb(err); + + base.generate('bar', function(err) { + if (err) return cb(err); + + cb(); + }); + }); + }); + + it('should run a task on the instance', function(cb) { + base.task('abc123', function(next) { + next(); + }); + + base.generate('abc123', function(err) { + assert(!err); + cb(); + }); + }); + + it('should run same-named task instead of a generator', function(cb) { + base.register('123xyz', function(app) { + cb(new Error('expected the task to run first')); + }); + + base.task('123xyz', function() { + cb(); + }); + + base.generate('123xyz', function(err) { + assert(!err); + }); + }); + + it('should run a task instead of a generator with a default task', function(cb) { + base.register('123xyz', function(app) { + app.task('default', function() { + cb(new Error('expected the task to run first')); + }); + }); + base.task('123xyz', function() { + cb(); + }); + base.generate('123xyz', function(err) { + assert(!err); + }); + }); + + it('should run a task on a same-named generator when the task is specified', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.task('foo', function() { + cb(new Error('expected the generator to run')); + }); + + base.generate('foo:default', function(err) { + assert(!err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of tasks that includes a same-named generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.register('bar', function(app) { + app.task('baz', function(next) { + count++; + next(); + }); + }); + + base.task('foo', function() { + cb(new Error('expected the generator to run')); + }); + + base.generate(['foo:default', 'bar:baz'], function(err) { + assert(!err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should run a generator from a task with the same name', function(cb) { + base.register('foo', function(app) { + app.task('default', function() { + cb(); + }); + }); + + base.task('foo', function(cb) { + base.generate('foo', cb); + }); + + base.build('foo', function(err) { + if (err) cb(err); + }); + }); + + it('should run the default task on a generator', function(cb) { + base.register('foo', function(app) { + app.task('default', function(next) { + next(); + }); + }); + + base.generate('foo', function(err) { + assert(!err); + cb(); + }); + }); + + it('should run a stringified array of tasks on the instance', function(cb) { + var count = 0; + base.task('a', function(next) { + count++; + next(); + }); + base.task('b', function(next) { + count++; + next(); + }); + base.task('c', function(next) { + count++; + next(); + }); + + base.generate('a,b,c', function(err) { + assert.equal(count, 3); + assert(!err); + cb(); + }); + }); + + it('should run an array of tasks on the instance', function(cb) { + var count = 0; + base.task('a', function(next) { + count++; + next(); + }); + base.task('b', function(next) { + count++; + next(); + }); + base.task('c', function(next) { + count++; + next(); + }); + + base.generate(['a', 'b', 'c'], function(err) { + if (err) return cb(err); + assert.equal(count, 3); + assert(!err); + cb(); + }); + }); + + it('should run the default task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of generators', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.register('bar', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate(['foo', 'bar'], function(err) { + if (err) return cb(err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should run the default task on the default generator', function(cb) { + var count = 0; + base.register('default', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate(function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the specified task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('abc', function(next) { + count++; + next(); + }); + }); + + base.generate('foo:abc', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of tasks on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('a', function(next) { + count++; + next(); + }); + + app.task('b', function(next) { + count++; + next(); + }); + + app.task('c', function(next) { + count++; + next(); + }); + }); + + base.generate('foo:a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run the default tasks on an array of generators', function(cb) { + var count = 0; + base.register('a', function(app) { + this.task('default', function(cb) { + count++; + cb(); + }); + }); + + base.register('b', function(app) { + this.task('default', function(cb) { + count++; + cb(); + }); + }); + + base.register('c', function(app) { + this.task('default', function(cb) { + count++; + cb(); + }); + }); + + base.generate(['a', 'b', 'c'], function(err) { + if (err) return cb(err); + assert.equal(count, 3); + assert(!err); + cb(); + }); + }); + }); + + describe('options', function(cb) { + it('should pass options to generator.options', function(cb) { + var count = 0; + base.register('default', function(app, base, env, options) { + app.task('default', function(next) { + count++; + assert.equal(app.options.foo, 'bar'); + next(); + }); + }); + + base.generate({foo: 'bar'}, function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should expose options on generator options', function(cb) { + var count = 0; + base.register('default', function(app, base, env, options) { + app.task('default', function(next) { + count++; + assert.equal(options.foo, 'bar'); + next(); + }); + }); + + base.generate({foo: 'bar'}, function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should not mutate options on parent instance', function(cb) { + var count = 0; + base.register('default', function(app, base, env, options) { + app.task('default', function(next) { + count++; + assert.equal(options.foo, 'bar'); + assert(!base.options.foo); + next(); + }); + }); + + base.generate({foo: 'bar'}, function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('default tasks', function(cb) { + it('should run the default task on the default generator', function(cb) { + var count = 0; + base.register('default', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate(function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the default task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('specified tasks', function(cb) { + it('should run the specified task on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('abc', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an array of tasks on a registered generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.task('default', function(next) { + count++; + next(); + }); + + app.task('a', function(next) { + count++; + next(); + }); + + app.task('b', function(next) { + count++; + next(); + }); + + app.task('c', function(next) { + count++; + next(); + }); + }); + + base.generate('foo', 'a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + }); + + describe('sub-generators', function(cb) { + it('should run the default task on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the specified task string on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub:abc', function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run the specified task array on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + + it('should run an of stringified-tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.bar:a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run an array of tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.bar', ['a', 'b', 'c'], function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run an multiple tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.bar', 'a,b,c', function(err) { + if (err) return cb(err); + assert.equal(count, 3); + cb(); + }); + }); + + it('should run an multiple tasks on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('bar', function(bar) { + bar.task('default', function(next) { + count++; + next(); + }); + + bar.task('a', function(next) { + count++; + next(); + }); + + bar.task('b', function(next) { + count++; + next(); + }); + + bar.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.register('qux', function(app) { + app.register('fez', function(fez) { + fez.task('default', function(next) { + count++; + next(); + }); + + fez.task('a', function(next) { + count++; + next(); + }); + + fez.task('b', function(next) { + count++; + next(); + }); + + fez.task('c', function(next) { + count++; + next(); + }); + }); + }); + + base.generate(['foo.bar:a,b,c', 'qux.fez:a,b,c'], function(err) { + if (err) return cb(err); + assert.equal(count, 6); + cb(); + }); + }); + }); + + describe('cross-generator', function(cb) { + it('should run a generator from another generator', function(cb) { + var res = ''; + + base.register('foo', function(app, two) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + res += 'foo > sub > default '; + base.generate('bar.sub', next); + }); + }); + }); + + base.register('bar', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + res += 'bar > sub > default '; + next(); + }); + }); + }); + + base.generate('foo.sub', function(err) { + if (err) return cb(err); + assert.equal(res, 'foo > sub > default bar > sub > default '); + cb(); + }); + }); + + it('should run the specified task on a registered sub-generator', function(cb) { + var count = 0; + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + count++; + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 1); + cb(); + }); + }); + }); + + describe('events', function(cb) { + it('should emit generate', function(cb) { + var count = 0; + + base.on('generate', function() { + count++; + }); + + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + next(); + }); + + sub.task('abc', function(next) { + count++; + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + assert.equal(count, 2); + cb(); + }); + }); + + it('should expose the generator alias as the first parameter', function(cb) { + base.on('generate', function(name) { + assert.equal(name, 'sub'); + cb(); + }); + + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + next(); + }); + + sub.task('abc', function(next) { + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + }); + }); + + it('should expose the tasks array as the second parameter', function(cb) { + base.on('generate', function(name, tasks) { + assert.deepEqual(tasks, ['abc']); + cb(); + }); + + base.register('foo', function(app) { + app.register('sub', function(sub) { + sub.task('default', function(next) { + next(); + }); + + sub.task('abc', function(next) { + next(); + }); + }); + }); + + base.generate('foo.sub', ['abc'], function(err) { + if (err) return cb(err); + }); + }); + }); +}); diff --git a/test/app.updater.js b/test/app.updater.js new file mode 100644 index 0000000..3126482 --- /dev/null +++ b/test/app.updater.js @@ -0,0 +1,291 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var Base = require('..'); +var base; + +var fixtures = path.resolve.bind(path, __dirname, 'fixtures'); + +describe('.updater', function() { + beforeEach(function() { + base = new Base(); + + base.option('toAlias', function(key) { + return key.replace(/^updater-(.*)/, '$1'); + }); + }); + + describe('updater', function() { + it('should get an updater by alias', function() { + base.register('updater-example', require('updater-example')); + var gen = base.getUpdater('example'); + assert(gen); + assert.equal(gen.env.name, 'updater-example'); + assert.equal(gen.env.alias, 'example'); + }); + + it('should get an updater using a custom lookup function', function() { + base.register('updater-foo', function() {}); + base.register('updater-bar', function() {}); + + var gen = base.getUpdater('foo', { + lookup: function(key) { + return ['updater-' + key, 'verb-' + key + '-updater', key]; + } + }); + + assert(gen); + assert.equal(gen.env.name, 'updater-foo'); + assert.equal(gen.env.alias, 'foo'); + }); + }); + + describe('register > function', function() { + it('should register an updater function by name', function() { + base.updater('foo', function() {}); + assert(base.updaters.hasOwnProperty('foo')); + }); + + it('should register an updater function by alias', function() { + base.updater('updater-abc', function() {}); + assert(base.updaters.hasOwnProperty('updater-abc')); + }); + }); + + describe('get > alias', function() { + it('should get an updater by alias', function() { + base.updater('updater-abc', function() {}); + var abc = base.updater('abc'); + assert(abc); + assert.equal(typeof abc, 'object'); + }); + }); + + describe('get > name', function() { + it('should get an updater by name', function() { + base.updater('updater-abc', function() {}); + var abc = base.updater('updater-abc'); + assert(abc); + assert.equal(typeof abc, 'object'); + }); + }); + + describe('updaters', function() { + it('should invoke a registered updater when `getGenerator` is called', function(cb) { + base.register('foo', function(app) { + app.task('default', function() {}); + cb(); + }); + base.getGenerator('foo'); + }); + + it('should expose the updater instance on `app`', function(cb) { + base.register('foo', function(app) { + app.task('default', function(next) { + assert.equal(app.get('a'), 'b'); + next(); + }); + }); + + var foo = base.getGenerator('foo'); + foo.set('a', 'b'); + foo.build('default', function(err) { + if (err) return cb(err); + cb(); + }); + }); + + it('should expose the "base" instance on `base`', function(cb) { + base.set('x', 'z'); + base.register('foo', function(app, base) { + app.task('default', function(next) { + assert.equal(base.get('x'), 'z'); + next(); + }); + }); + + var foo = base.getGenerator('foo'); + foo.set('a', 'b'); + foo.build('default', function(err) { + if (err) return cb(err); + cb(); + }); + }); + + it('should expose the "env" object on `env`', function(cb) { + base.register('foo', function(app, base, env) { + app.task('default', function(next) { + assert.equal(env.alias, 'foo'); + next(); + }); + }); + + base.getGenerator('foo').build('default', function(err) { + if (err) return cb(err); + cb(); + }); + }); + + it('should expose an app\'s updaters on app.updaters', function(cb) { + base.register('foo', function(app) { + app.register('a', function() {}); + app.register('b', function() {}); + + app.updaters.hasOwnProperty('a'); + app.updaters.hasOwnProperty('b'); + cb(); + }); + + base.getGenerator('foo'); + }); + + it('should expose all root updaters on base.updaters', function(cb) { + base.register('foo', function(app, b) { + b.updaters.hasOwnProperty('foo'); + b.updaters.hasOwnProperty('bar'); + b.updaters.hasOwnProperty('baz'); + cb(); + }); + + base.register('bar', function(app, base) {}); + base.register('baz', function(app, base) {}); + base.getGenerator('foo'); + }); + }); + + describe('cross-updaters', function() { + it('should get an updater from another updater', function(cb) { + base.register('foo', function(app, b) { + var bar = b.getGenerator('bar'); + assert(bar); + cb(); + }); + + base.register('bar', function(app, base) {}); + base.register('baz', function(app, base) {}); + base.getGenerator('foo'); + }); + + it('should set options on another updater instance', function(cb) { + base.updater('foo', function(app) { + app.task('default', function(next) { + assert.equal(app.option('abc'), 'xyz'); + next(); + }); + }); + + base.updater('bar', function(app, b) { + var foo = b.getGenerator('foo'); + foo.option('abc', 'xyz'); + foo.build(function(err) { + if (err) return cb(err); + cb(); + }); + }); + }); + }); + + describe('updaters > filepath', function() { + it('should register an updater function from a file path', function() { + var one = base.updater('one', fixtures('one/updatefile.js')); + assert(base.updaters.hasOwnProperty('one')); + assert(typeof base.updaters.one === 'object'); + assert.deepEqual(base.updaters.one, one); + }); + + it('should get a registered updater by name', function() { + var one = base.updater('one', fixtures('one/updatefile.js')); + assert.deepEqual(base.updater('one'), one); + }); + }); + + describe('tasks', function() { + it('should expose an updater\'s tasks on app.tasks', function(cb) { + base.register('foo', function(app) { + app.task('a', function() {}); + app.task('b', function() {}); + assert(app.tasks.a); + assert(app.tasks.b); + cb(); + }); + + base.getGenerator('foo'); + }); + + it('should get tasks from another updater', function(cb) { + base.register('foo', function(app, b) { + var baz = b.getGenerator('baz'); + var task = baz.tasks.aaa; + assert(task); + cb(); + }); + + base.register('bar', function(app, base) {}); + base.register('baz', function(app, base) { + app.task('aaa', function() {}); + }); + base.getGenerator('foo'); + }); + }); + + describe('namespace', function() { + it('should expose `app.namespace`', function(cb) { + base.updater('foo', function(app) { + assert(typeof app.namespace, 'string'); + cb(); + }); + }); + + it('should create namespace from updater alias', function(cb) { + base.updater('updater-foo', function(app) { + assert.equal(app.namespace, base._name + '.foo'); + cb(); + }); + }); + + it('should create sub-updater namespace from parent namespace and alias', function(cb) { + var name = base._name; + base.updater('updater-foo', function(app) { + assert.equal(app.namespace, name + '.foo'); + + app.updater('updater-bar', function(bar) { + assert.equal(bar.namespace, name + '.foo.bar'); + + bar.updater('updater-baz', function(baz) { + assert.equal(baz.namespace, name + '.foo.bar.baz'); + + baz.updater('updater-qux', function(qux) { + assert.equal(qux.namespace, name + '.foo.bar.baz.qux'); + cb(); + }); + }); + }); + }); + }); + + it('should expose namespace on `this`', function(cb) { + var name = base._name; + + base.updater('updater-foo', function(app, first) { + assert.equal(this.namespace, base._name + '.foo'); + + this.updater('updater-bar', function() { + assert.equal(this.namespace, base._name + '.foo.bar'); + + this.updater('updater-baz', function() { + assert.equal(this.namespace, base._name + '.foo.bar.baz'); + + this.updater('updater-qux', function() { + assert.equal(this.namespace, base._name + '.foo.bar.baz.qux'); + assert.equal(app.namespace, base._name + '.foo'); + assert.equal(first.namespace, base._name); + cb(); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/fixtures/def-gen.js b/test/fixtures/def-gen.js new file mode 100644 index 0000000..9955ea4 --- /dev/null +++ b/test/fixtures/def-gen.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function(app) { + app.task('default', function(cb) { + console.log('default'); + cb(); + }); +}; diff --git a/test/fixtures/not-exposed.js b/test/fixtures/not-exposed.js new file mode 100644 index 0000000..5cee793 --- /dev/null +++ b/test/fixtures/not-exposed.js @@ -0,0 +1,8 @@ +'use strict'; + +var Base = require('../..'); +var base = new Base({isApp: true}); + +base.register('not-exposed', function(app) { + +}); diff --git a/test/fixtures/one/package.json b/test/fixtures/one/package.json new file mode 100644 index 0000000..42509ea --- /dev/null +++ b/test/fixtures/one/package.json @@ -0,0 +1,9 @@ +{ + "name": "update-one", + "version": "0.0.0", + "private": true, + "description": "", + "main": "updatefile.js", + "license": "MIT", + "base": {} +} diff --git a/test/fixtures/one/templates/a.txt b/test/fixtures/one/templates/a.txt new file mode 100644 index 0000000..2ff8250 --- /dev/null +++ b/test/fixtures/one/templates/a.txt @@ -0,0 +1 @@ +one: aaa \ No newline at end of file diff --git a/test/fixtures/one/templates/x.txt b/test/fixtures/one/templates/x.txt new file mode 100644 index 0000000..139e1d8 --- /dev/null +++ b/test/fixtures/one/templates/x.txt @@ -0,0 +1 @@ +one: xxx \ No newline at end of file diff --git a/test/fixtures/one/templates/y.txt b/test/fixtures/one/templates/y.txt new file mode 100644 index 0000000..7308ea9 --- /dev/null +++ b/test/fixtures/one/templates/y.txt @@ -0,0 +1 @@ +one: yyy \ No newline at end of file diff --git a/test/fixtures/one/templates/z.txt b/test/fixtures/one/templates/z.txt new file mode 100644 index 0000000..04c378a --- /dev/null +++ b/test/fixtures/one/templates/z.txt @@ -0,0 +1 @@ +one: zzz \ No newline at end of file diff --git a/test/fixtures/one/updatefile.js b/test/fixtures/one/updatefile.js new file mode 100644 index 0000000..eb51b6d --- /dev/null +++ b/test/fixtures/one/updatefile.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = function(app, base, env) { + app.task('default', function(cb) { + console.log('one > default'); + cb(); + }); + + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + + app.register('foo', function(app) { + app.task('x', function() {}); + app.task('y', function() {}); + app.task('z', function() {}); + }); +}; diff --git a/test/fixtures/one/verbfile.js b/test/fixtures/one/verbfile.js new file mode 100644 index 0000000..546d004 --- /dev/null +++ b/test/fixtures/one/verbfile.js @@ -0,0 +1,19 @@ +'use strict'; + + +module.exports = function(app, base, env) { + app.task('default', function(cb) { + console.log('one > default'); + cb(); + }); + + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); + + app.register('foo', function(app) { + app.task('x', function() {}); + app.task('y', function() {}); + app.task('z', function() {}); + }); +}; \ No newline at end of file diff --git a/test/fixtures/package.json b/test/fixtures/package.json new file mode 100644 index 0000000..543ca49 --- /dev/null +++ b/test/fixtures/package.json @@ -0,0 +1,9 @@ +{ + "name": "update-tests", + "version": "0.0.0", + "private": true, + "description": "", + "main": "index.js", + "license": "MIT", + "base": {} +} diff --git a/test/fixtures/pages/a.hbs b/test/fixtures/pages/a.hbs new file mode 100644 index 0000000..51320bd --- /dev/null +++ b/test/fixtures/pages/a.hbs @@ -0,0 +1,2 @@ +

{{title}}

+

<%= title() %>

\ No newline at end of file diff --git a/test/fixtures/pages/b.hbs b/test/fixtures/pages/b.hbs new file mode 100644 index 0000000..51320bd --- /dev/null +++ b/test/fixtures/pages/b.hbs @@ -0,0 +1,2 @@ +

{{title}}

+

<%= title() %>

\ No newline at end of file diff --git a/test/fixtures/pages/c.hbs b/test/fixtures/pages/c.hbs new file mode 100644 index 0000000..51320bd --- /dev/null +++ b/test/fixtures/pages/c.hbs @@ -0,0 +1,2 @@ +

{{title}}

+

<%= title() %>

\ No newline at end of file diff --git a/test/fixtures/updater-foo/updatefile.js b/test/fixtures/updater-foo/updatefile.js new file mode 100644 index 0000000..887c058 --- /dev/null +++ b/test/fixtures/updater-foo/updatefile.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function(app, base, env) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); +}; diff --git a/test/fixtures/updater.js b/test/fixtures/updater.js new file mode 100644 index 0000000..de1c362 --- /dev/null +++ b/test/fixtures/updater.js @@ -0,0 +1,49 @@ +'use strict'; + +module.exports = function(app) { + app.register('updater-aaa', function(app) { + app.task('default', function(cb) { + console.log('update > default'); + cb(); + }); + + app.register('sub', function(sub) { + sub.task('default', function(cb) { + console.log('aaa > sub > default'); + cb(); + }); + + sub.register('bbb', function(bbb) { + bbb.task('default', function(cb) { + console.log('aaa > sub > bbb > default'); + cb(); + }); + }); + }); + }); + + app.register('updater-abc', 'test/fixtures/generators/a/generator.js'); + + app.register('updater-bbb', function(app) { + app.task('default', function(cb) { + app.update('aaa.sub.bbb', 'default', cb); + }); + }); + + app.register('updater-ccc', function(app) { + app.task('default', function(cb) { + app.update('abc', 'default', cb); + }); + }); + + app.register('updater-ddd', function(app) { + app.task('default', function(cb) { + app.update('abc.docs', 'x', cb); + }); + }); + + app.update('aaa.sub', ['default'], function(err) { + if (err) throw err; + console.log('done'); + }); +}; diff --git a/test/fixtures/updaters/a/.gitignore b/test/fixtures/updaters/a/.gitignore new file mode 100644 index 0000000..80a228c --- /dev/null +++ b/test/fixtures/updaters/a/.gitignore @@ -0,0 +1,15 @@ +*.DS_Store +*.sublime-* +_gh_pages +bower_components +node_modules +npm-debug.log +actual +test/actual +temp +tmp +TODO.md +vendor +.idea +benchmark +coverage diff --git a/test/fixtures/updaters/a/package.json b/test/fixtures/updaters/a/package.json new file mode 100644 index 0000000..b9bab76 --- /dev/null +++ b/test/fixtures/updaters/a/package.json @@ -0,0 +1,7 @@ +{ + "name": "updater-a", + "private": true, + "version": "0.1.0", + "files": ["index.js"], + "main": "updatefile.js" +} diff --git a/test/fixtures/updaters/a/post.hbs b/test/fixtures/updaters/a/post.hbs new file mode 100644 index 0000000..b2cb52b --- /dev/null +++ b/test/fixtures/updaters/a/post.hbs @@ -0,0 +1,5 @@ +--- +title: Post +--- + +This is SOME POST \ No newline at end of file diff --git a/test/fixtures/updaters/a/updatefile.js b/test/fixtures/updaters/a/updatefile.js new file mode 100644 index 0000000..0198ef6 --- /dev/null +++ b/test/fixtures/updaters/a/updatefile.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function(app) { + app.task('default', function(cb) { + console.log('fixtures/a > default'); + cb(); + }); +}; diff --git a/test/fixtures/updaters/a/verbfile.js b/test/fixtures/updaters/a/verbfile.js new file mode 100644 index 0000000..c3f4d75 --- /dev/null +++ b/test/fixtures/updaters/a/verbfile.js @@ -0,0 +1,10 @@ +'use strict'; + +var path = require('path'); + +module.exports = function(app) { + app.task('default', function(cb) { + console.log('fixtures/a > default'); + cb(); + }); +}; diff --git a/test/fixtures/updaters/qux/package.json b/test/fixtures/updaters/qux/package.json new file mode 100644 index 0000000..f160d17 --- /dev/null +++ b/test/fixtures/updaters/qux/package.json @@ -0,0 +1,7 @@ +{ + "name": "updater-qux", + "private": true, + "version": "0.1.0", + "files": ["updatefile.js"], + "main": "updatefile.js" +} diff --git a/test/fixtures/updaters/qux/updatefile.js b/test/fixtures/updaters/qux/updatefile.js new file mode 100644 index 0000000..887c058 --- /dev/null +++ b/test/fixtures/updaters/qux/updatefile.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function(app, base, env) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); +}; diff --git a/test/fixtures/updaters/qux/verbfile.js b/test/fixtures/updaters/qux/verbfile.js new file mode 100644 index 0000000..28979ee --- /dev/null +++ b/test/fixtures/updaters/qux/verbfile.js @@ -0,0 +1,11 @@ +'use strict'; + +/** + * testing... + */ + +module.exports = function(app, base, env) { + app.task('a', function() {}); + app.task('b', function() {}); + app.task('c', function() {}); +}; diff --git a/test/runner.js b/test/runner.js new file mode 100644 index 0000000..e680047 --- /dev/null +++ b/test/runner.js @@ -0,0 +1,111 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var argv = require('yargs-parser')(process.argv.slice(2)); +var runner = require('base-runner'); +var Generate = require('..'); +var base; + +var fixtures = path.resolve.bind(path, __dirname, 'fixtures'); +var config = { + name: 'foo', + runner: require(fixtures('package.json')), + configName: 'updater', + extensions: { + '.js': null + } +}; + +describe('.runner', function() { + var error = console.error; + + beforeEach(function() { + console.error = function() {}; + }); + + afterEach(function() { + console.error = error; + }); + + describe('errors', function() { + it('should throw an error when a callback is not passed', function(cb) { + try { + runner(); + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, 'expected a callback function'); + cb(); + } + }); + + it('should error when an options object is not passed', function(cb) { + runner(Generate, {}, null, function(err, app, runnerContext) { + assert(err); + assert.equal(err.message, 'expected the third argument to be an options object'); + cb(); + }); + }); + + it('should error when a liftoff config object is not passed', function(cb) { + runner(Generate, null, {}, function(err, app, runnerContext) { + assert(err); + assert.equal(err.message, 'expected the second argument to be a liftoff config object'); + cb(); + }); + }); + + it('should error when a Generate constructor is not passed', function(cb) { + runner(null, {}, {}, function(err, app, runnerContext) { + assert(err); + assert.equal(err.message, 'expected the first argument to be a Base constructor'); + cb(); + }); + }); + }); + + describe('runner', function() { + it('should set "env" on app.cache.runnerContext', function(cb) { + runner(Generate, config, argv, function(err, app, runnerContext) { + if (err) return cb(err); + assert(app.cache.runnerContext.env); + assert.equal(typeof app.cache.runnerContext.env, 'object'); + cb(); + }); + }); + + it('should set "config" on app.cache.runnerContext', function(cb) { + runner(Generate, config, argv, function(err, app, runnerContext) { + if (err) return cb(err); + assert(app.cache.runnerContext.config); + assert.equal(typeof app.cache.runnerContext.config, 'object'); + cb(); + }); + }); + + it('should set the configFile on app.cache.runnerContext.env', function(cb) { + runner(Generate, config, argv, function(err, app, runnerContext) { + if (err) return cb(err); + assert.equal(app.cache.runnerContext.env.configFile, 'updater.js'); + cb(); + }); + }); + + it('should set cwd on the instance', function(cb) { + runner(Generate, config, {cwd: fixtures()}, function(err, app, runnerContext) { + if (err) return cb(err); + assert.equal(app.cwd, fixtures()); + cb(); + }); + }); + + it('should resolve configpath from app.cwd and app.configFile', function(cb) { + runner(Generate, config, {cwd: fixtures()}, function(err, app, runnerContext) { + if (err) return cb(err); + assert.equal(app.cache.runnerContext.env.configPath, path.resolve(__dirname, 'fixtures/updater.js')); + cb(); + }); + }); + }); +}); diff --git a/test/support/ignore.js b/test/support/ignore.js new file mode 100644 index 0000000..7dcbb75 --- /dev/null +++ b/test/support/ignore.js @@ -0,0 +1,6 @@ +module.exports = [ + 'addEventListener', + 'removeEventListener', + 'removeAllListeners', + 'removeListener' +]; diff --git a/test/support/index.js b/test/support/index.js new file mode 100644 index 0000000..e2194a5 --- /dev/null +++ b/test/support/index.js @@ -0,0 +1,64 @@ +'use strict'; + +var path = require('path'); +var loadpkg = require('load-pkg'); +var assert = require('assert'); +var ignore = require('./ignore'); +var cache = {}; + +exports.containEql = function containEql(actual, expected) { + if (Array.isArray(expected)) { + var len = expected.length; + while (len--) { + exports.containEql(actual[len], expected[len]); + } + } else { + for (var key in expected) { + assert.deepEqual(actual[key], expected[key]); + } + } +}; + +exports.keys = function keys(obj) { + var arr = []; + for (var key in obj) { + if (ignore.indexOf(key) === -1) { + arr.push(key); + } + } + return arr; +}; + +exports.resolve = function(filepath) { + filepath = filepath || ''; + var key = 'app:' + filepath; + if (cache.hasOwnProperty(key)) { + return cache[key]; + } + + var pkg = loadpkg.sync(process.cwd()); + var prefix = pkg.name !== 'templates' + ? 'templates' + : ''; + + var base = filepath + ? path.join(prefix, filepath) + : process.cwd(); + + var fp = tryResolve(base); + + if (typeof fp === 'undefined') { + throw new Error('cannot resolve: ' + fp); + } + return (cache[key] = require(fp)); +}; + +function tryResolve(name) { + try { + return require.resolve(name); + } catch (err) {} + + try { + return require.resolve(path.resolve(name)); + } catch (err) {} +} diff --git a/test/support/spy.js b/test/support/spy.js new file mode 100644 index 0000000..b473c6e --- /dev/null +++ b/test/support/spy.js @@ -0,0 +1,31 @@ +'use strict'; + +var fs = require('graceful-fs'); +var sinon = require('sinon'); + +var errorfn = false; + +function maybeCallAsync(module, func) { + var original = module[func]; + return sinon.stub(module, func, function() { + var args = Array.prototype.slice.call(arguments); + args.unshift(module, func); + var err = typeof errorfn === 'function' && errorfn.apply(this, args); + if (!err) { + original.apply(this, arguments); + } else { + arguments[arguments.length - 1](err); + } + }); +} + +module.exports = { + setError: function(fn) { + errorfn = fn; + }, + chmodSpy: maybeCallAsync(fs, 'chmod'), + fchmodSpy: maybeCallAsync(fs, 'fchmod'), + futimesSpy: maybeCallAsync(fs, 'futimes'), + statSpy: maybeCallAsync(fs, 'stat'), + fstatSpy: maybeCallAsync(fs, 'fstat') +}; diff --git a/test/update.js b/test/update.js new file mode 100644 index 0000000..17378a4 --- /dev/null +++ b/test/update.js @@ -0,0 +1,35 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var Update = require('..'); +var update; + +describe('update', function() { + describe('cwd', function() { + beforeEach(function() { + update = new Update(); + }); + + it('should get the current working directory', function() { + assert.equal(update.cwd, process.cwd()); + }); + + it('should set the current working directory', function() { + update.cwd = 'test/fixtures'; + assert.equal(update.cwd, path.join(process.cwd(), 'test/fixtures')); + }); + }); + + describe('generator', function() { + beforeEach(function() { + update = new Update(); + }); + + it('should register the default generator', function() { + update.register('default', require('./fixtures/def-gen')); + assert(update.getGenerator('default')); + }); + }); +}); diff --git a/test/updaters.env.js b/test/updaters.env.js new file mode 100644 index 0000000..f2d2e97 --- /dev/null +++ b/test/updaters.env.js @@ -0,0 +1,127 @@ +'use strict'; + +require('mocha'); +var path = require('path'); +var assert = require('assert'); +var Base = require('..'); +var base; +var env; + +var fixtures = path.resolve.bind(path, __dirname + '/fixtures'); + +describe('env', function() { + describe('plugin', function() { + it('should work as a plugin', function() { + base = new Base(); + assert.equal(typeof base.createEnv, 'function'); + }); + }); + + describe('createEnv paths', function() { + beforeEach(function() { + base = new Base(); + }); + + describe('alias and function', function() { + it('should make the alias the exact name when the second arg is a function', function() { + var fn = function() {}; + var env = base.createEnv('foo-bar-baz', fn); + assert(env); + assert(env.alias); + assert.equal(env.alias, 'foo-bar-baz'); + }); + + it('should not change the name when the second arg is a function', function() { + var fn = function() {}; + var env = base.createEnv('foo-bar-baz', fn); + assert(env); + assert(env.name); + assert.equal(env.name, 'foo-bar-baz'); + }); + }); + + describe('alias and path', function() { + it('should set the env.name using the given name', function() { + var env = base.createEnv('foo', fixtures('updater-foo/updatefile.js')); + assert.equal(env.name, 'foo'); + }); + + it('should not change the name when the second arg is a function', function() { + var fn = function() {}; + var env = base.createEnv('foo-bar-baz', fn); + assert(env); + assert(env.name); + assert.equal(env.name, 'foo-bar-baz'); + }); + }); + }); + + describe('createEnv', function() { + beforeEach(function() { + base = new Base(); + }); + + it('should add an env object to the instance', function() { + var fn = function() {}; + env = base.createEnv('foo', fn); + assert(env); + }); + + it('should take options as the second arg', function() { + var fn = function() {}; + env = base.createEnv('foo', {}, fn); + assert(env); + }); + + it('should prime `env` if it doesn\'t exist', function() { + var fn = function() {}; + env = base.createEnv('foo', {}, fn); + assert(env); + }); + + it('should add an alias to the env object', function() { + var fn = function() {}; + env = base.createEnv('foo', {}, fn); + assert.equal(env.alias, 'foo'); + }); + + it('should not prefix the alias when a function is passed', function() { + var fn = function() {}; + delete base.prefix; + env = base.createEnv('foo', {}, fn); + assert.equal(env.name, 'foo'); + }); + + it('should not prefix a custom alias when a function is passed', function() { + var fn = function() {}; + base.prefix = 'whatever'; + env = base.createEnv('foo', {}, fn); + assert.equal(env.name, 'foo'); + }); + + it('should try to resolve an absolute path passed as the second arg', function() { + env = base.createEnv('foo', fixtures('updater.js')); + assert.equal(env.alias, 'foo'); + assert.equal(env.name, 'foo'); + }); + + it('should try to resolve a relative path passed as the second arg', function() { + env = base.createEnv('foo', fixtures('updater-foo/updatefile.js')); + assert.equal(env.key, 'foo'); + assert.equal(env.alias, 'foo'); + assert.equal(env.name, 'foo'); + }); + + it('should throw an error when the path is not resolved', function(cb) { + try { + var env = base.createEnv('foo', fixtures('whatever.js')); + env.invoke(); + env.path; + cb(new Error('expected an error')); + } catch (err) { + assert.equal(err.message, 'cannot resolve: \'' + fixtures('whatever.js') + '\''); + cb(); + } + }); + }); +}); diff --git a/test/updaters.events.js b/test/updaters.events.js new file mode 100644 index 0000000..00db2ae --- /dev/null +++ b/test/updaters.events.js @@ -0,0 +1,157 @@ +'use strict'; + +require('mocha'); +var assert = require('assert'); +var Base = require('..'); +var base; + +var updaters = require('..'); + +describe('updaters events', function() { + describe('generator', function() { + beforeEach(function() { + base = new Base(); + base.enable('silent'); + }); + + it('should emit generator when a generator is registered', function(cb) { + base.on('generator', function(generator) { + assert.equal(generator.alias, 'foo'); + cb(); + }); + + base.register('foo', function() {}); + }); + + it('should emit generator when base.updaters.get is called', function(cb) { + + base.on('generator', function(generator) { + assert.equal(generator.alias, 'foo'); + cb(); + }); + + base.register('foo', function() {}); + base.getGenerator('foo'); + }); + + it('should emit generator.get when base.updaters.get is called', function(cb) { + base.on('generator', function(generator) { + assert.equal(generator.alias, 'foo'); + cb(); + }); + + base.register('foo', function() {}); + base.getGenerator('foo'); + }); + + it('should emit error on base when a base generator emits an error', function(cb) { + var called = 0; + + base.on('error', function(err) { + assert.equal(err.message, 'whatever'); + called++; + }); + + base.register('foo', function(app) { + app.emit('error', new Error('whatever')); + }); + + base.getGenerator('foo'); + assert.equal(called, 1); + cb(); + }); + + it('should emit error on base when a base generator throws an error', function(cb) { + var called = 0; + + base.on('error', function(err) { + assert.equal(err.message, 'whatever'); + called++; + }); + + base.register('foo', function(app) { + app.task('default', function(cb) { + cb(new Error('whatever')); + }); + }); + + base.getGenerator('foo') + .build(function(err) { + assert(err); + assert.equal(called, 1); + cb(); + }); + + }); + + it('should emit errors on base from deeply nested updaters', function(cb) { + var called = 0; + + base.on('error', function(err) { + assert.equal(err.message, 'whatever'); + called++; + }); + + base.register('a', function() { + this.register('b', function() { + this.register('c', function() { + this.register('d', function() { + this.task('default', function(cb) { + cb(new Error('whatever')); + }); + }); + }); + }); + }); + + base.getGenerator('a.b.c.d') + .build(function(err) { + assert(err); + assert.equal(called, 1); + cb(); + }); + + }); + + it('should bubble up errors to all parent updaters', function(cb) { + var called = 0; + + function count() { + called++; + } + + base.on('error', function(err) { + assert.equal(err.message, 'whatever'); + called++; + }); + + base.register('a', function() { + this.on('error', count); + + this.register('b', function() { + this.on('error', count); + + this.register('c', function() { + this.on('error', count); + + this.register('d', function() { + this.on('error', count); + + this.task('default', function(cb) { + cb(new Error('whatever')); + }); + }); + }); + }); + }); + + base.getGenerator('a.b.c.d') + .build(function(err) { + assert(err); + assert.equal(called, 6); + assert.equal(err.message, 'whatever'); + cb(); + }); + }); + }); +}); diff --git a/verbfile.js b/verbfile.js deleted file mode 100644 index 540c853..0000000 --- a/verbfile.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -var del = require('del'); -var path = require('path'); -var verb = require('verb'); -var log = require('./lib/logging')({nocompare: true}); -var plugins = require('./plugins')(verb); -var parse = require('parse-copyright'); - - -verb.onLoad(/./, function (file, next) { - file.render = false; - file.readme = false; - next(); -}); - -verb.onLoad(/\.js$/, function (file, next) { - file.data.copyright = parse(file.content); - next(); -}); - -verb.copy('.verbrc.md', function (file) { - file.path = '.verb.md'; - log.success('renamed', file.relative); - return path.dirname(file.relative); -}); - -verb.copy('LICENSE-MIT', function (file) { - file.path = 'LICENSE'; - log.success('renamed', file.relative); - return path.dirname(file.relative); -}); - -verb.task('banners', function () { - verb.src(['*.js', 'test/*.js', 'lib/*.js'], {render: false}) - .pipe(plugins.tests()) - .pipe(plugins.banners()) - .pipe(verb.dest(function (file) { - return path.dirname(file.path); - })); -}); - -verb.task('verbfile', function () { - verb.src(['.verb{,rc}.md'], {render: false}) - .pipe(plugins.verbmd()) - .pipe(verb.dest(function (file) { - file.path = '.verb.md'; - return path.dirname(file.path); - })); -}); - -verb.task('jshint', function () { - verb.src('.jshintrc', {render: false}) - .pipe(plugins.jshint()) - .pipe(verb.dest(function (file) { - file.path = '.jshintrc'; - return path.dirname(file.path); - })); -}); - -verb.task('license', function () { - verb.src('LICENSE', {render: false}) - .pipe(plugins.license()) - .pipe(verb.dest(function (file) { - file.path = 'LICENSE'; - return path.dirname(file.path); - })); -}); - -verb.task('dotfiles', function () { - verb.src('.git*', {render: false, dot: true}) - .pipe(plugins.dotfiles()) - // .pipe(plugins.gitignore()) - .pipe(verb.dest(function (file) { - return path.dirname(file.path); - })) - .on('end', function (cb) { - var files = ['.npmignore', 'test/mocha.opts', '.verbrc.md', 'LICENSE-MIT']; - log.info('deleted', files.join(', ')); - del(files, cb); - }); -}); - -verb.task('pkg', function () { - verb.src('package.json', {render: false}) - .pipe(plugins.pkg()) - .pipe(verb.dest('.')); -}); - -verb.task('readme', function () { - verb.src('.verb.md') - .pipe(verb.dest('.')); -}); - -verb.task('default', [ - 'banners', - 'verbfile', - 'dotfiles', - 'jshint', - 'license', - 'pkg', - 'readme' -]); - -verb.run(); \ No newline at end of file