diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f37c99 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# Editor config +# http://EditorConfig.org + +# This EditorConfig overrides any parent EditorConfigs +root = true + +# Default rules applied to all file types +[*] + +# No trailing spaces, newline at EOF +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf + +# 2 space indentation +indent_style = space +indent_size = 2 + +# JavaScript-specific settings +[*.{js,ts}] +quote_type = double +continuation_indent_size = 2 +curly_bracket_next_line = false +indent_brace_style = BSD +spaces_around_operators = true +spaces_around_brackets = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4ca9ecd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +# Git attributes +# https://git-scm.com/docs/gitattributes +# https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes + +# Normalize line endings for all files that git determines to be text. +# https://git-scm.com/docs/gitattributes#gitattributes-Settostringvalueauto +* text=auto + +# Normalize line endings to LF on checkin, and do NOT convert to CRLF when checking-out on Windows. +# https://git-scm.com/docs/gitattributes#gitattributes-Settostringvaluelf +*.txt text eol=lf +*.html text eol=lf +*.md text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.map text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.xml text eol=lf +*.svg text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f38ac19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Git ignore +# https://git-scm.com/docs/gitignore + +# Private files +.env + +# Miscellaneous +*~ +*# +.DS_STORE +Thumbs.db +.netbeans +nbproject +.node_history + +# IDEs & Text Editors +.idea +.sublime-* +.vscode/settings.json +.netbeans +nbproject + +# Temporary files +.tmp +.temp +.grunt +.lock-wscript + +# Logs +/logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Dependencies +node_modules + +# Build output +/lib + +# Test output +/.nyc_output +/coverage diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 0000000..53f64a1 --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,14 @@ +# Mocha options +# https://mochajs.org/#configuring-mocha-nodejs +# https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml + +spec: + # Test fixtures + - test/fixtures/**/*.js + + # Test specs + - test/specs/**/*.spec.js + +bail: true +recursive: true +require: source-map-support/register diff --git a/.nycrc.yml b/.nycrc.yml new file mode 100644 index 0000000..b7e965e --- /dev/null +++ b/.nycrc.yml @@ -0,0 +1,10 @@ +# NYC config +# https://github.com/istanbuljs/nyc#configuration-files + +extension: + - .js + - .ts + +reporter: + - text + - lcov diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b4ab192 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,43 @@ +# Travis CI config +# http://docs.travis-ci.com/user/languages/javascript-with-nodejs/ +# https://docs.travis-ci.com/user/customizing-the-build/ +# https://docs.travis-ci.com/user/migrating-from-legacy/ + +filter_secrets: false +language: node_js + +node_js: + - 8 + - 10 + - 12 + +os: + - linux + - osx + - windows + +before_script: + - npm run lint + - npm run build + +script: + - npm run coverage + +after_success: + # send code-coverage data to Coveralls + - cat ./coverage/lcov.info | coveralls + +jobs: + include: + - stage: Deploy + name: Publish to npm + script: true + after_success: true + deploy: + provider: npm + email: $NPM_EMAIL + api_key: $NPM_API_KEY + skip_cleanup: true + on: + tags: true + branch: master diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fa2a800 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,53 @@ +// VSCode Launch Configuration +// https://code.visualstudio.com/docs/editor/debugging#_launch-configurations + +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Run Mocha", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "args": [ + "--timeout=60000", + "--retries=0", + ], + "outFiles": [ + "${workspaceFolder}/lib/**/*.js" + ], + "smartStep": true, + "skipFiles": [ + "/**/*.js" + ], + }, + + + { + "type": "node", + "request": "launch", + "name": "Run CLI", + "program": "${workspaceRoot}/bin/project-cli-name.js", + "args": [], + "env": { + "NODE_ENV": "development" + }, + "outputCapture": "std", + "outFiles": [ + "${workspaceFolder}/lib/**/*.js" + ], + "smartStep": true, + "skipFiles": [ + "/**/*.js" + ], + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..217bf21 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,37 @@ +// VSCode Tasks +// https://code.visualstudio.com/docs/editor/tasks + +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +{ + "version": "2.0.0", + "command": "npm", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc" + }, + + + { + "type": "npm", + "script": "test", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": "$tslint5" + }, + ] +} diff --git a/404.md b/404.md new file mode 100644 index 0000000..c4e870f --- /dev/null +++ b/404.md @@ -0,0 +1,3 @@ +--- +layout: 404 +--- diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0185ed2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 James Messinger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cca33eb --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +Node.js TypeScript Template +=========================== +This is a **template repo** for Node.js projects written in TypeScript. This template works for libraries and/or CLIs. + + + +Step 1: Copy this repo +--------------------------------------------- +Create a new git repo and copy the contents of this repo into it. + + + +Step 2: Delete unneeded files +--------------------------------------------- +If you **don't** need a CLI, then: + - Delete the following files and directories: + - `bin` + - `src/cli` + - `test/specs/cli.spec.js` + - `test/utils/project-cli-name.js` + - Delete the following fields in `package.json`: + - `bin` + - `files.bin` + - `devDependencies.chai-exec` + - `devDependencies.@types/command-line-args` + - `dependencies.command-line-args` + - Delete the `Run CLI` config from `.vscode/launch.json` + + + +Step 3: Replace placeholders +--------------------------------------------- +Replace all occurrences of the following placeholders in all files: + +|Placeholder |Description +|:----------------------------------|:------------------------------------------------------------ +|`project-package-name` |This is the name of the NPM package. It should also match the GitHub repo name. It should be kebab-cased. +|`project-cli-name` |The name of the CLI program for this project, if any. +|`projectExportName` |The name of the library's default export, if any. This should be a valid JavaScript identifier name. +|`Friendly Project Name` |This is the human friendly name of the project that is used in the ReadMe, descriptions, and docs pages +|`This is the project description` |A short, human friendly description of the project that is used in the ReadMe and package.json + + + +Step 4: TODOs +--------------------------------------------- +Find all "TODO" notes in the code and follow their instructions. + + + +Step 5: ReadMe +--------------------------------------------- +Delete this file and replace it with `README_md`. diff --git a/README_md b/README_md new file mode 100644 index 0000000..3b0756d --- /dev/null +++ b/README_md @@ -0,0 +1,84 @@ +Friendly Project Name +============================================== +### This is the project description + +[![Cross-Platform Compatibility](https://jstools.dev/img/badges/os-badges.svg)](https://travis-ci.com/JS-DevTools/project-package-name) +[![Build Status](https://api.travis-ci.com/JS-DevTools/project-package-name.svg?branch=master)](https://travis-ci.com/JS-DevTools/project-package-name) + +[![Coverage Status](https://coveralls.io/repos/github/JS-DevTools/project-package-name/badge.svg?branch=master)](https://coveralls.io/github/JS-DevTools/project-package-name) +[![Dependencies](https://david-dm.org/JS-DevTools/project-package-name.svg)](https://david-dm.org/JS-DevTools/project-package-name) + +[![npm](https://img.shields.io/npm/v/project-package-name.svg)](https://www.npmjs.com/package/project-package-name) +[![License](https://img.shields.io/npm/l/project-package-name.svg)](LICENSE) + + + +Features +-------------------------- +- Feature 1 +- Feature 2 +- Feature 3 + + + +Example +-------------------------- + +```javascript +import projectExportName from "project-package-name"; + +// TODO: Add a usage example here +``` + + + +Installation +-------------------------- +You can install `project-package-name` via [npm](https://docs.npmjs.com/about-npm/). + +```bash +npm install project-package-name +``` + + + +Usage +-------------------------- +TODO: Document the library's API and CLI usage + + + +Contributing +-------------------------- +Contributions, enhancements, and bug-fixes are welcome! [File an issue](https://github.com/JS-DevTools/project-package-name/issues) on GitHub and [submit a pull request](https://github.com/JS-DevTools/project-package-name/pulls). + +#### Building +To build the project locally on your computer: + +1. __Clone this repo__
+`git clone https://github.com/JS-DevTools/project-package-name.git` + +2. __Install dependencies__
+`npm install` + +3. __Build the code__
+`npm run build` + +4. __Run the tests__
+`npm test` + + + +License +-------------------------- +project-package-name is 100% free and open-source, under the [MIT license](LICENSE). Use it however you want. + + + +Big Thanks To +-------------------------- +Thanks to these awesome companies for their support of Open Source developers ❤ + +[![Travis CI](https://jstools.dev/img/badges/travis-ci.svg)](https://travis-ci.com) +[![SauceLabs](https://jstools.dev/img/badges/sauce-labs.svg)](https://saucelabs.com) +[![Coveralls](https://jstools.dev/img/badges/coveralls.svg)](https://coveralls.io) diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..5e38c86 --- /dev/null +++ b/_config.yml @@ -0,0 +1,26 @@ +remote_theme: JS-DevTools/gh-pages-theme + +title: Friendly Project Name +logo: https://jstools.dev/img/logos/logo.png + +author: + twitter: JSDevTools + +google_analytics: UA-68102273-3 + +twitter: + username: JSDevTools + card: summary + +defaults: + - scope: + path: "" + values: + image: https://jstools.dev/img/logos/card.png + - scope: + path: "test/**/*" + values: + sitemap: false + +plugins: + - jekyll-sitemap diff --git a/bin/project-cli-name.js b/bin/project-cli-name.js new file mode 100644 index 0000000..cd0bf1f --- /dev/null +++ b/bin/project-cli-name.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +"use strict"; +const { main } = require("../lib/cli"); +main(process.argv.slice(2)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..a3df5cf --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "project-package-name", + "version": "0.0.1", + "description": "This is the project description", + "keywords": [], + "author": { + "name": "James Messinger", + "url": "https://jamesmessinger.com" + }, + "license": "MIT", + "homepage": "https://jstools.dev/project-package-name", + "repository": { + "type": "git", + "url": "https://github.com/JS-DevTools/project-package-name.git" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "bin": { + "project-cli-name": "bin/project-cli-name.js" + }, + "files": [ + "bin", + "lib" + ], + "scripts": { + "clean": "shx rm -rf .nyc_output coverage lib", + "lint": "npm run lint:typescript && npm run lint:javascript", + "lint:typescript": "tslint -p tsconfig.json", + "lint:javascript": "eslint test", + "build": "tsc", + "watch": "tsc --watch", + "test": "mocha && npm run lint", + "coverage": "nyc node_modules/mocha/bin/mocha", + "upgrade": "npm-check -u && npm audit fix", + "bump": "bump --tag --push --all", + "release": "npm run upgrade && npm run clean && npm run build && npm test && npm run bump" + }, + "engines": { + "node": ">=8" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/command-line-args": "^5.0.0", + "@types/mocha": "^5.2.7", + "@types/node": "^12.0.7", + "chai": "^4.2.0", + "chai-exec": "^1.1.1", + "coveralls": "^3.0.4", + "eslint": "^5.16.0", + "eslint-config-modular": "^7.0.0", + "mocha": "^6.1.4", + "npm-check": "^5.9.0", + "nyc": "^14.1.1", + "shx": "^0.3.2", + "source-map-support": "^0.5.12", + "tslint": "^5.17.0", + "tslint-modular": "^1.4.1", + "typescript": "^3.5.1", + "typescript-tslint-plugin": "^0.5.0", + "version-bump-prompt": "^5.0.3" + }, + "dependencies": { + "command-line-args": "^5.1.1" + } +} diff --git a/src/cli/exit-code.ts b/src/cli/exit-code.ts new file mode 100644 index 0000000..c47b961 --- /dev/null +++ b/src/cli/exit-code.ts @@ -0,0 +1,10 @@ +/** + * CLI exit codes. + * + * @see https://nodejs.org/api/process.html#process_exit_codes + */ +export enum ExitCode { + Success = 0, + FatalError = 1, + InvalidArgument = 9 +} diff --git a/src/cli/help.ts b/src/cli/help.ts new file mode 100644 index 0000000..70bec5e --- /dev/null +++ b/src/cli/help.ts @@ -0,0 +1,27 @@ +import { manifest } from "./manifest"; + +const cli = Object.keys(manifest.bin)[0]; + +/** + * Text explaining how to use the CLI + */ +export const usageText = ` +Usage: ${cli} [options] [files...] + +options: + -v, --version Show the version number + + -q, --quiet Suppress unnecessary output + + -h, --help Show usage information + +files... + One or more files and/or globs to process (ex: README.md *.txt docs/**/*). +`; + +/** + * Text describing the program and how to use it + */ +export const helpText = ` +${manifest.name} - ${manifest.description} +${usageText}`; diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..8246d09 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,57 @@ +// tslint:disable: no-console +import { projectExportName } from ".."; +import { ExitCode } from "./exit-code"; +import { helpText } from "./help"; +import { manifest } from "./manifest"; +import { parseArgs } from "./parse-args"; + +/** + * The main entry point of the CLI + * + * @param args - The command-line arguments + */ +export function main(args: string[]): void { + try { + // Setup global error handlers + process.on("uncaughtException", errorHandler); + process.on("unhandledRejection", errorHandler); + + // Parse the command-line arguments + let { help, version, quiet, options } = parseArgs(args); + + if (help) { + // Show the help text and exit + console.log(helpText); + process.exit(ExitCode.Success); + } + else if (version) { + // Show the version number and exit + console.log(manifest.version); + process.exit(ExitCode.Success); + } + else { + let result = projectExportName(options); + + if (!quiet) { + console.log(result); + } + } + } + catch (error) { + errorHandler(error as Error); + } +} + +/** + * Prints errors to the console + */ +function errorHandler(error: Error): void { + let message = error.message || String(error); + + if (process.env.DEBUG || process.env.NODE_ENV === "development") { + message = error.stack || message; + } + + console.error(message); + process.exit(ExitCode.FatalError); +} diff --git a/src/cli/manifest.ts b/src/cli/manifest.ts new file mode 100644 index 0000000..15df56b --- /dev/null +++ b/src/cli/manifest.ts @@ -0,0 +1,16 @@ +// NOTE: We can't `import` the package.json file because it's outside of the "src" directory. +// tslint:disable-next-line: no-var-requires no-require-imports +export const manifest = require("../../package.json") as Manifest; + +/** + * The npm package manifest (package.json) + */ +export interface Manifest { + name: string; + version: string; + description: string; + bin: { + [bin: string]: string; + }; + [key: string]: unknown; +} diff --git a/src/cli/parse-args.ts b/src/cli/parse-args.ts new file mode 100644 index 0000000..fce258a --- /dev/null +++ b/src/cli/parse-args.ts @@ -0,0 +1,62 @@ +// tslint:disable: no-console +import * as commandLineArgs from "command-line-args"; +import { Options } from ".."; +import { ExitCode } from "./exit-code"; +import { usageText } from "./help"; + +/** + * The parsed command-line arguments + */ +export interface ParsedArgs { + help: boolean; + version: boolean; + quiet: boolean; + options: Options; +} + +/** + * Parses the command-line arguments + */ +export function parseArgs(argv: string[]): ParsedArgs { + try { + let args = commandLineArgs( + [ + { name: "greeting", type: String }, + { name: "subject", type: String }, + { name: "quiet", alias: "q", type: Boolean }, + { name: "version", alias: "v", type: Boolean }, + { name: "help", alias: "h", type: Boolean }, + { name: "files", type: String, multiple: true, defaultOption: true }, + ], + { argv } + ); + + if (args.greeting === null) { + throw new Error("The --greeting option requires a value."); + } + + if (args.subject === null) { + throw new Error("The --subject option requires a value."); + } + + return { + help: Boolean(args.help), + version: Boolean(args.version), + quiet: Boolean(args.quiet), + options: { + greeting: args.greeting as string | undefined, + subject: args.subject as string | undefined, + } + }; + } + catch (error) { + // There was an error parsing the command-line args + return errorHandler(error as Error); + } +} + +function errorHandler(error: Error): never { + console.error(error.message); + console.error(usageText); + return process.exit(ExitCode.InvalidArgument); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b2e0cad --- /dev/null +++ b/src/index.ts @@ -0,0 +1,13 @@ +import { projectExportName } from "./project-package-name"; + +export { Options } from "./settings"; +export { projectExportName }; + +// Export `projectExportName` as the default export +// tslint:disable: no-default-export +export default projectExportName; + +// CommonJS default export hack +if (typeof module === "object" && typeof module.exports === "object") { + module.exports = Object.assign(module.exports.default, module.exports); // tslint:disable-line: no-unsafe-any +} diff --git a/src/project-package-name.ts b/src/project-package-name.ts new file mode 100644 index 0000000..990a12c --- /dev/null +++ b/src/project-package-name.ts @@ -0,0 +1,17 @@ +import { Options, Settings } from "./settings"; + +/** + * This is the project description + * + * @returns - Options + */ +export function projectExportName(options?: Options): string { + let settings = new Settings(options); + + if (settings.greeting === "Goodbye") { + // Simulate a runtime error + throw new Error("Cannot say goodbye"); + } + + return `${settings.greeting}, ${settings.subject}.`; +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..d141cb5 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,33 @@ +/** + * Options that can be provided by callers. All fields are optional. + */ +export type Options = Partial; + +/** + * Normalized, sanitized, and complete settings, + * with default values for anything that wasn't specified by the caller. + */ +export class Settings { + /** + * The greeting to return. + * + * The default is `"Hello"`. + */ + public greeting = "Hello"; + + /** + * The name of the subject to greet. + * + * The default is `"world"`. + */ + public subject = "world"; + + /** + * Normalizes and sanitizes options provided by the caller, + * and applies default values for any settings that aren't specified. + */ + public constructor(options: Options = {}) { + options.greeting && (this.greeting = String(options.greeting)); + options.subject && (this.subject = String(options.subject)); + } +} diff --git a/src/typings/some-dependency/index.d.ts b/src/typings/some-dependency/index.d.ts new file mode 100644 index 0000000..f078c39 --- /dev/null +++ b/src/typings/some-dependency/index.d.ts @@ -0,0 +1,4 @@ +declare module "some-dependency" { + const foo: () => void; + export default foo; +} diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml new file mode 100644 index 0000000..338093a --- /dev/null +++ b/test/.eslintrc.yml @@ -0,0 +1,12 @@ +# ESLint config +# http://eslint.org/docs/user-guide/configuring +# https://jstools.dev/eslint-config-modular/ + +root: true + +extends: + - modular/best-practices + - modular/style + - modular/es6 + - modular/node + - modular/test diff --git a/test/fixtures/mocha-hooks.js b/test/fixtures/mocha-hooks.js new file mode 100644 index 0000000..b226149 --- /dev/null +++ b/test/fixtures/mocha-hooks.js @@ -0,0 +1,15 @@ +"use strict"; + +/** + * Perform one-time setup before any tests are run. + */ +before("Setup test environment", () => { + // TODO: Add any logic needed to initialize the test environment +}); + +/** + * Reset the test environment before each test. + */ +beforeEach("Reset test environment", () => { + // TODO: Add any logic needed to reset the test environment before each test +}); diff --git a/test/specs/api.spec.js b/test/specs/api.spec.js new file mode 100644 index 0000000..cacf635 --- /dev/null +++ b/test/specs/api.spec.js @@ -0,0 +1,36 @@ +"use strict"; + +const projectExportName = require("../../"); +const { expect } = require("chai"); + +describe("projectExportName() API", () => { + + it("should work without any arguments", () => { + let result = projectExportName(); + expect(result).to.equal("Hello, world."); + }); + + it("should accept a custom greeting", () => { + let result = projectExportName({ greeting: "Hi there" }); + expect(result).to.equal("Hi there, world."); + }); + + it("should accept a custom subject", () => { + let result = projectExportName({ subject: "Michael" }); + expect(result).to.equal("Hello, Michael."); + }); + + it("should accept a custom greeting and subject", () => { + let result = projectExportName({ greeting: "Yo", subject: "man" }); + expect(result).to.equal("Yo, man."); + }); + + it('should not allow a greeting of "goodbye"', () => { + function sayGoodbye () { + projectExportName({ greeting: "Goodbye" }); + } + + expect(sayGoodbye).to.throw("Cannot say goodbye"); + }); + +}); diff --git a/test/specs/cli.spec.js b/test/specs/cli.spec.js new file mode 100644 index 0000000..a14378a --- /dev/null +++ b/test/specs/cli.spec.js @@ -0,0 +1,125 @@ +"use strict"; + +const projectExportName = require("../utils/project-cli-name"); +const manifest = require("../../package.json"); +const { expect } = require("chai"); + +describe("project-cli-name", () => { + + it("should run without any arguments", () => { + // Run the CLI without any arguments. + let cli = projectExportName(""); + + // It should have printed the default greeting + expect(cli).to.have.stdout("Hello, world.\n"); + }); + + it("should error if an invalid argument is used", () => { + let cli = projectExportName("--fizzbuzz"); + + expect(cli).to.have.exitCode(9); + expect(cli).to.have.stdout(""); + expect(cli).to.have.stderr.that.matches(/^Unknown option: --fizzbuzz\n\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + + it("should error if an invalid shorthand argument is used", () => { + let cli = projectExportName("-qhzt"); + + expect(cli).to.have.exitCode(9); + expect(cli).to.have.stdout(""); + expect(cli).to.have.stderr.that.matches(/^Unknown option: -z\n\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + + it("should error if an argument is missing its value", () => { + let cli = projectExportName("--subject"); + + expect(cli).to.have.exitCode(9); + expect(cli).to.have.stdout(""); + expect(cli).to.have.stderr.that.matches(/^The --subject option requires a value\.\n\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + + it("should print a more detailed error if DEBUG is set", () => { + let cli = projectExportName("--greeting Goodbye", { env: { DEBUG: "true" }}); + + expect(cli).to.have.stdout(""); + expect(cli).to.have.exitCode(1); + expect(cli).to.have.stderr.that.matches(/^Error: Cannot say goodbye\n\s+at \w+/); + }); + +}); + +describe("project-cli-name --help", () => { + + it("should show usage text", () => { + let cli = projectExportName("--help"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout.that.contains(manifest.description); + expect(cli).to.have.stdout.that.matches(/\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + + it("should support -h shorthand", () => { + let cli = projectExportName("-h"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout.that.contains(manifest.description); + expect(cli).to.have.stdout.that.matches(/\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + + it("should ignore other arguments", () => { + let cli = projectExportName("--quiet --help --version"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout.that.contains(manifest.description); + expect(cli).to.have.stdout.that.matches(/\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + + it("should ignore other shorthand arguments", () => { + let cli = projectExportName("-qhv"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout.that.contains(manifest.description); + expect(cli).to.have.stdout.that.matches(/\nUsage: project-cli-name \[options\] \[files...\]\n/); + }); + +}); + +describe("project-cli-name --version", () => { + + it("should show the version number", () => { + let cli = projectExportName("--version"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout(manifest.version + "\n"); + }); + + it("should support -v shorthand", () => { + let cli = projectExportName("-v"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout(manifest.version + "\n"); + }); + + it("should ignore other arguments", () => { + let cli = projectExportName("--quiet --version"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout(manifest.version + "\n"); + }); + + it("should ignore other shorthand arguments", () => { + let cli = projectExportName("-qv"); + + expect(cli).to.have.exitCode(0); + expect(cli).to.have.stderr(""); + expect(cli).to.have.stdout(manifest.version + "\n"); + }); + +}); diff --git a/test/specs/exports.spec.js b/test/specs/exports.spec.js new file mode 100644 index 0000000..1945391 --- /dev/null +++ b/test/specs/exports.spec.js @@ -0,0 +1,31 @@ +"use strict"; + +const commonJSExport = require("../../"); +const { default: defaultExport, projectExportName: namedExport } = require("../../"); +const { expect } = require("chai"); + +describe("project-package-name package exports", () => { + + it("should export the projectExportName() function as the default CommonJS export", () => { + expect(commonJSExport).to.be.a("function"); + expect(commonJSExport.name).to.equal("projectExportName"); + }); + + it("should export the projectExportName() function as the default ESM export", () => { + expect(defaultExport).to.be.a("function"); + expect(defaultExport).to.equal(commonJSExport); + }); + + it("should export the projectExportName() function as a named export", () => { + expect(namedExport).to.be.a("function"); + expect(namedExport).to.equal(commonJSExport); + }); + + it("should not export anything else", () => { + expect(commonJSExport).to.have.same.keys( + "default", + "projectExportName", + ); + }); + +}); diff --git a/test/utils/project-cli-name.js b/test/utils/project-cli-name.js new file mode 100644 index 0000000..b815fdc --- /dev/null +++ b/test/utils/project-cli-name.js @@ -0,0 +1,16 @@ +"use strict"; + +const chai = require("chai"); +const chaiExec = require("chai-exec"); + +// Add the Chai-Exec plugin for testing the CLI +chai.use(chaiExec); + +// Setup Chai Exec to run our CLI by default, +// so we don't have to specify it for every test. +chaiExec.defaults = { + command: "node", + args: "bin/project-cli-name.js", +}; + +module.exports = chaiExec; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..916a49e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "moduleResolution": "node", + + "outDir": "lib", + "sourceMap": true, + "declaration": true, + + "newLine": "LF", + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + + "typeRoots": [ + "node_modules/@types", + "src/typings" + ], + + "plugins": [ + { "name": "typescript-tslint-plugin" } + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/tslint.yaml b/tslint.yaml new file mode 100644 index 0000000..2b7c77f --- /dev/null +++ b/tslint.yaml @@ -0,0 +1,8 @@ +# TSLint config +# https://palantir.github.io/tslint/usage/configuration/ +# https://jstools.dev/tslint-modular/ + +extends: + - tslint-modular/best-practices + - tslint-modular/style + - tslint-modular/node