1
0
Fork 0

feat: new project from template

This commit is contained in:
ssube 2019-11-13 08:01:51 -06:00
commit 33c2936275
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
46 changed files with 5525 additions and 0 deletions

35
.codeclimate.yml Normal file
View File

@ -0,0 +1,35 @@
version: "2"
checks:
method-complexity:
config:
threshold: 6 # 5 is *just* too low and flags a number of otherwise readable methods
method-lines:
config:
threshold: 30 # with object literals, lines are not a great measure
exclude_patterns:
- CHANGELOG.md
- config/
- docs/api/
- docs/dev/style.md
- src/migration/
- vendor/
plugins:
duplication:
enabled: true
config:
languages:
typescript:
mass_threshold: 225
eslint:
enabled: true
config:
config: config/eslint.json
fixme:
enabled: true
markdownlint:
enabled: true
shellcheck:
enabled: true

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.nyc_output/
docs/api/
node_modules/
out/api
out/cache/
out/coverage/
out/tmp/

40
.github/ISSUE_TEMPLATE/type_bug.md vendored Normal file
View File

@ -0,0 +1,40 @@
---
name: Bug Report
about: report a problem
title: ''
labels: status/new, type/bug
assignees: ''
---
# Bug
Something is broken!
## Summary
(quick summary of the issue)
## Steps to Reproduce
- install
- configure
- run
- ...
- :(
## Actual Behavior
```none
example
```
## Expected Behavior
```none
example
```
## Details
(detailed description of the expected behavior and what goes wrong)

33
.github/ISSUE_TEMPLATE/type_feature.md vendored Normal file
View File

@ -0,0 +1,33 @@
---
name: Feature Suggestion
about: Suggest an idea for this project
title: ''
labels: status/new, type/feature
assignees: ''
---
# Feature
Something is missing!
## Summary
An elevator pitch of the feature and why it will be useful.
## Scope
- [ ] items to be implemented
- [ ] additional details
## Use Case
At least one concrete use case for the feature, ideally with step-by-step example.
Bonus points if you can refer to another issue here.
## Questions
Outstanding questions, potential problems, and further research that might be needed.
New or updated dependencies should be noted here.

19
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
# Merge
Something is ready to be merged!
## Issues
List the issue(s) resolved or addressed by this change.
Changes MUST be related to an issue. If one does not exist,
please create one with the appropriate `type/` label.
## Checks
Please ensure all required status checks have succeeded:
- [ ] Gitlab pipeline has passed
- [ ] lint warnings have not increased
- [ ] documentation has been written
- [ ] tests have been added and consistently pass

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
.awcache/
.licenses/
.nyc_output/
node_modules/
out/
temp/
# types
*.bak
*.pid
*.swp
*.tmp
package-lock.json
yarn-error.log

159
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,159 @@
include:
- local: /config/gitlab/ci-tools.yml
- local: /config/gitlab/ci-rules.yml
stages:
- status-pre
- build
- image
- publish
- status-post
# build jobs
build-node:
stage: build
extends:
- .build-node
script:
- make ci
artifacts:
when: always
expire_in: 1 day
paths:
- out/
cache:
key: "${CI_COMMIT_REF_SLUG}"
policy: pull-push
paths:
- node_modules/
- out/api
- out/cache
- out/tmp
build-image-alpine-branch:
extends:
- .build-docker
stage: image
except:
- tags
script:
- ./scripts/docker-build.sh --push --default
variables:
IMAGE_ARCH: alpine
build-image-alpine-tag:
extends:
- .build-docker
stage: image
only:
- tags
script:
- ./scripts/docker-build.sh --push --default
variables:
IMAGE_ARCH: alpine
build-image-stretch-branch:
stage: image
extends: [.build-docker]
except:
- tags
script:
- ./scripts/docker-build.sh --push
variables:
IMAGE_ARCH: stretch
build-image-stretch-tag:
stage: image
extends:
- .build-docker
only:
- tags
script:
- ./scripts/docker-build.sh --push
variables:
IMAGE_ARCH: stretch
# publish jobs
publish-npm:
stage: publish
extends:
- .build-node
- .deploy-tags
dependencies:
- build-node
script:
- npm publish
# commit status
climate-pending:
stage: status-pre
extends:
- .build-climate
script:
- cc-test-reporter before-build
climate-success:
stage: status-post
extends:
- .build-climate
dependencies:
- build-node
script:
- make upload-climate
codecov-success:
stage: status-post
extends:
- .build-codecov
when: on_success
dependencies:
- build-node
script:
- make upload-codecov
github-pending:
stage: status-pre
extends:
- .build-curl
script:
- ./scripts/github-status.sh pending
github-failure:
stage: status-post
extends:
- .build-curl
when: on_failure
script:
- ./scripts/github-status.sh failure
github-success:
stage: status-post
extends:
- .build-curl
when: on_success
script:
- ./scripts/github-status.sh success
sonar-success:
stage: status-post
extends:
- .build-sonar
when: on_success
script:
- make node_modules
- sonar-scanner
-Dsonar.projectKey=ssube_rollup-template
-Dsonar.projectVersion=${CI_COMMIT_REF_SLUG}
-Dsonar.organization=ssube-github
-Dsonar.sources=src/,test/
-Dsonar.host.url=https://sonarcloud.io
-Dsonar.login=${SONAR_SECRET}
-Dsonar.typescript.lcov.reportPaths=out/coverage/lcov.info

36
.npmignore Normal file
View File

@ -0,0 +1,36 @@
.awcache/
.git/
.github/
.nyc_output/
config/
deploy/
node_modules/
out/cache/
out/coverage/
out/coverage-*
out/docs/
out/typings/
out/*.db
out/*.html
out/*.json
out/test-*
out/tmp/
scripts/
src/
temp/
test/
vendor/
.codeclimate.yml
.dockerignore
.gitlab-ci.yml
.gitmodules
.mdlrc
Dockerfile
licensed.yml
Makefile
renovate.json
tsconfig.json
yarn-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://artifacts.apextoaster.com/repository/group-npm/

16
Dockerfile.alpine Normal file
View File

@ -0,0 +1,16 @@
FROM node:12-alpine
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
WORKDIR /app
RUN yarn install --production
COPY . /app
RUN yarn global add file:$(pwd)
ENV PATH="${PATH}:$(yarn global bin)"
ENTRYPOINT [ "node", "/app/out/index.js" ]
CMD [ "--help" ]

16
Dockerfile.stretch Normal file
View File

@ -0,0 +1,16 @@
FROM node:12-stretch
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
WORKDIR /app
RUN yarn install --production
COPY . /app
RUN yarn global add file:$(pwd)
ENV PATH="${PATH}:$(yarn global bin)"
ENTRYPOINT [ "node", "/app/out/index.js" ]
CMD [ "--help" ]

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2018 Sean Sube
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.

166
Makefile Executable file
View File

@ -0,0 +1,166 @@
# Git
export GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
export GIT_COMMIT ?= $(shell git rev-parse HEAD)
export GIT_OPTIONS ?=
export GIT_REMOTES ?= $(shell git remote -v | awk '{ print $1; }' | sort | uniq)
export GIT_TAG ?= $(shell git tag -l --points-at HEAD | head -1)
# Paths
# resolve the makefile's path and directory, from https://stackoverflow.com/a/18137056
export MAKE_PATH ?= $(abspath $(lastword $(MAKEFILE_LIST)))
export ROOT_PATH ?= $(dir $(MAKE_PATH))
export CONFIG_PATH ?= $(ROOT_PATH)/config
export DOCS_PATH ?= $(ROOT_PATH)/docs
export SCRIPT_PATH ?= $(ROOT_PATH)/scripts
export SOURCE_PATH ?= $(ROOT_PATH)/src
export TARGET_PATH ?= $(ROOT_PATH)/out
export TARGET_LOG ?= $(TARGET_PATH)/make.log
export TARGET_MAIN ?= $(TARGET_PATH)/index.js
export TEST_PATH ?= $(ROOT_PATH)/test
export VENDOR_PATH ?= $(ROOT_PATH)/vendor
# CI
export CI_COMMIT_REF_SLUG ?= $(GIT_BRANCH)
export CI_COMMIT_SHA ?= $(GIT_COMMIT)
export CI_COMMIT_TAG ?= $(GIT_TAG)
export CI_ENVIRONMENT_SLUG ?= local
export CI_JOB_ID ?= 0
export CI_PROJECT_PATH ?= $(shell ROOT_PATH=$(ROOT_PATH) ${SCRIPT_PATH}/ci-project-path.sh)
export CI_RUNNER_DESCRIPTION ?= $(shell hostname)
export CI_RUNNER_ID ?= $(shell hostname)
export CI_RUNNER_VERSION ?= 0.0.0
# Debug
export DEBUG_BIND ?= 127.0.0.1
export DEBUG_PORT ?= 9229
# Versions
export NODE_VERSION := $(shell node -v || echo "none")
export RUNNER_VERSION := $(CI_RUNNER_VERSION)
# Node options
NODE_BIN := $(ROOT_PATH)/node_modules/.bin
NODE_CMD ?= $(shell env node)
NODE_DEBUG ?= --inspect-brk=$(DEBUG_BIND):$(DEBUG_PORT) --nolazy
NODE_INFO := $(shell node -v)
# Tool options
COVER_OPTS ?= --reporter=lcov --reporter=text-summary --reporter=html --report-dir="$(TARGET_PATH)/coverage" --exclude-after-remap
MOCHA_OPTS ?= --check-leaks --colors --sort --ui bdd
RELEASE_OPTS ?= --commit-all
.PHONY: all clean clean-deps clean-target configure help todo
.PHONY: build build-bundle build-docs build-image test test-check test-cover test-watch
.PHONY: yarn-install yarn-upgrade git-push git-stats license-check release release-dry upload-climate upload-codecov
all: build test ## builds, bundles, and tests the application
@echo Success!
clean: ## clean up everything added by the default target
clean: clean-deps clean-target
clean-deps: ## clean up the node_modules directory
rm -rf node_modules
clean-target: ## clean up the target directory
rm -rf $(TARGET_PATH)
configure: ## create the target directory and other files not in git
mkdir -p $(TARGET_PATH)
node_modules: yarn-install
# from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help: ## print this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort \
| sed 's/^.*\/\(.*\)/\1/' \
| awk 'BEGIN {FS = ":[^:]*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
todo:
@echo "Remaining tasks:"
@echo ""
@grep -i "todo" -r docs/ src/ test/ || true
@echo ""
@echo "Pending tests:"
@echo ""
@grep "[[:space:]]xit" -r test/ || true
@echo "Casts to any:"
@echo ""
@grep "as any" -r src/ test/ || true
@echo ""
# Build targets
build: ## builds, bundles, and tests the application
build: build-bundle build-docs
build-bundle: node_modules
$(NODE_BIN)/rollup --config $(CONFIG_PATH)/rollup.js
sed -i '1s;^;#! /usr/bin/env node\n\n;' $(TARGET_PATH)/index.js
build-docs: ## generate html docs
$(NODE_BIN)/api-extractor run --config $(CONFIG_PATH)/api-extractor.json --local -v
$(NODE_BIN)/api-documenter markdown -i $(TARGET_PATH)/api -o $(DOCS_PATH)/api
build-image: ## build a docker image
$(SCRIPT_PATH)/docker-build.sh --push
test: ## run mocha unit tests
test: test-cover
test-check: ## run mocha unit tests with coverage reports
$(NODE_BIN)/nyc $(COVER_OPTS) $(NODE_BIN)/mocha $(MOCHA_OPTS) $(TARGET_PATH)/test.js
test-cover: ## run mocha unit tests with coverage reports
test-cover: test-check
sed -i $(TARGET_PATH)/coverage/lcov.info \
-e '/external ".*"$$/,/end_of_record/d' \
-e '/ sync$$/,/end_of_record/d' \
-e '/test sync/,/end_of_record/d' \
-e '/node_modules/,/end_of_record/d' \
-e '/bootstrap$$/,/end_of_record/d' \
-e '/universalModuleDefinition/,/end_of_record/d'
sed -n '/^SF/,$$p' -i $(TARGET_PATH)/coverage/lcov.info
sed '1s;^;TN:\n;' -i $(TARGET_PATH)/coverage/lcov.info
test-watch:
$(NODE_BIN)/nyc $(COVER_OPTS) $(NODE_BIN)/mocha $(MOCHA_OPTS) --watch $(TARGET_PATH)/test-bundle.js
yarn-install: ## install dependencies from package and lock file
yarn
yarn-global: ## install bundle as a global tool
yarn global add file:$(ROOT_PATH)
yarn-update: ## check yarn for outdated packages
yarn upgrade-interactive --latest
# release targets
git-push: ## push to both gitlab and github (this assumes you have both remotes set up)
git push $(GIT_OPTIONS) github $(GIT_BRANCH)
git push $(GIT_OPTIONS) gitlab $(GIT_BRANCH)
# from https://gist.github.com/amitchhajer/4461043#gistcomment-2349917
git-stats: ## print git contributor line counts (approx, for fun)
git ls-files | while read f; do git blame -w -M -C -C --line-porcelain "$$f" |\
grep -I '^author '; done | sort -f | uniq -ic | sort -n
license-check: ## check license status
licensed cache
licensed status
release: ## create a release
$(NODE_BIN)/standard-version --sign $(RELEASE_OPTS)
GIT_OPTIONS=--tags $(MAKE) git-push
release-dry: ## test creating a release
$(NODE_BIN)/standard-version --sign $(RELEASE_OPTS) --dry-run
upload-climate:
cc-test-reporter format-coverage -t lcov -o $(TARGET_PATH)/coverage/codeclimate.json -p $(ROOT_PATH) $(TARGET_PATH)/coverage/lcov.info
cc-test-reporter upload-coverage --debug -i $(TARGET_PATH)/coverage/codeclimate.json -r "$(shell echo "${CODECLIMATE_SECRET}" | base64 -d)"
upload-codecov:
codecov --disable=gcov --file=$(TARGET_PATH)/coverage/lcov.info --token=$(shell echo "${CODECOV_SECRET}" | base64 -d)
include $(shell find $(ROOT_PATH) -name '*.mk' | grep -v node_modules)

149
README.md Normal file
View File

@ -0,0 +1,149 @@
# Rollup Template
This project contains the configuration and build scripts for most of
my Typescript projects, with scripts to create a new project and keep
existing ones up-to-date. Even this readme is a template for others.
## Features
- build scripts with `make`
- build pipeline with `gitlab`
- update github commit status
- publish docker images from branches & tags
- publish npm packages from tags
- bundled with `rollup`
- type checked with `typescript`
- style checked with `eslint` (with `tslint` rules and other plugins)
- tested with `mocha` (with source map support and helpers for async leak tracking)
- code coverage measured with `nyc`
- changelog generated with `standard-release`
### Intentionally Omitted Features
- everything frontend: React, CSS, etc
- heavy backend libraries: ORMs, etc
## Contents
- [Rollup Template](#rollup-template)
- [Features](#features)
- [Intentionally Omitted Features](#intentionally-omitted-features)
- [Contents](#contents)
- [Status](#status)
- [Releases](#releases)
- [Usage](#usage)
- [To Setup](#to-setup)
- [To Build](#to-build)
- [To Release](#to-release)
- [External Services](#external-services)
- [Maintenance Bots](#maintenance-bots)
- [External Secrets](#external-secrets)
## Status
[![Pipeline status](https://img.shields.io/gitlab/pipeline/ssube/rollup-template.svg?gitlab_url=https%3A%2F%2Fgit.apextoaster.com&logo=gitlab)](https://git.apextoaster.com/ssube/rollup-template/commits/master)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=ssube_rollup-template&metric=ncloc)](https://sonarcloud.io/dashboard?id=ssube_rollup-template)
[![Test coverage](https://codecov.io/gh/ssube/rollup-template/branch/master/graph/badge.svg)](https://codecov.io/gh/ssube/rollup-template)
[![MIT license](https://img.shields.io/github/license/ssube/rollup-template.svg)](https://github.com/ssube/rollup-template/blob/master/LICENSE.md)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fssube%2Frollup-template.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fssube%2Frollup-template?ref=badge_shield)
[![Open bug count](https://img.shields.io/github/issues-raw/ssube/rollup-template/type-bug.svg)](https://github.com/ssube/rollup-template/issues?q=is%3Aopen+is%3Aissue+label%3Atype%2Fbug)
[![Open issue count](https://img.shields.io/github/issues-raw/ssube/rollup-template.svg)](https://github.com/ssube/rollup-template/issues?q=is%3Aopen+is%3Aissue)
[![Closed issue count](https://img.shields.io/github/issues-closed-raw/ssube/rollup-template.svg)](https://github.com/ssube/rollup-template/issues?q=is%3Aissue+is%3Aclosed)
[![Renovate badge](https://badges.renovateapi.com/github/ssube/rollup-template)](https://renovatebot.com)
[![Dependency status](https://img.shields.io/david/ssube/rollup-template.svg)](https://david-dm.org/ssube/rollup-template)
[![Dev dependency status](https://img.shields.io/david/dev/ssube/rollup-template.svg)](https://david-dm.org/ssube/rollup-template?type=dev)
[![Known vulnerabilities](https://snyk.io/test/github/ssube/rollup-template/badge.svg)](https://snyk.io/test/github/ssube/rollup-template)
[![Maintainability score](https://api.codeclimate.com/v1/badges/0ca333e0379bda050d84/maintainability)](https://codeclimate.com/github/ssube/rollup-template/maintainability)
[![Technical debt ratio](https://img.shields.io/codeclimate/tech-debt/ssube/rollup-template.svg)](https://codeclimate.com/github/ssube/rollup-template/trends/technical_debt)
[![Quality issues](https://img.shields.io/codeclimate/issues/ssube/rollup-template.svg)](https://codeclimate.com/github/ssube/rollup-template/issues)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ssube/rollup-template.svg?logo=lgtm)](https://lgtm.com/projects/g/ssube/rollup-template/context:javascript)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/ssube/rollup-template.svg)](https://lgtm.com/projects/g/ssube/rollup-template/alerts/)
## Releases
[![github release link](https://img.shields.io/badge/github-release-blue?logo=github)](https://github.com/ssube/rollup-template/releases)
[![github release version](https://img.shields.io/github/tag/ssube/rollup-template.svg)](https://github.com/ssube/rollup-template/releases)
[![github commits since release](https://img.shields.io/github/commits-since/ssube/rollup-template/v0.2.4.svg)](https://github.com/ssube/rollup-template/compare/v0.2.4...master)
[![npm package link](https://img.shields.io/badge/npm-package-blue?logo=npm)](https://www.npmjs.com/package/@apextoaster/rollup-template)
[![npm release version](https://img.shields.io/npm/v/@apextoaster/rollup-template.svg)](https://www.npmjs.com/package/@apextoaster/rollup-template)
[![Typescript definitions](https://img.shields.io/npm/types/@apextoaster/rollup-template.svg)](https://www.npmjs.com/package/@apextoaster/rollup-template)
## Usage
### To Setup
To create a new repository from this template:
- create your new repo on Github & Gitlab (your server or Gitlab.com)
- `git clone git@github.com:ssube/rollup-template.git your-project`
- `cd your-project`
- `git remote add github git@github.com:yourname/your-project.git`
- `git remote add gitlab git@gitlab.com:yourname/your-project.git`
- set up repository mirroring in Gitlab
- set up [some maintenance bots](#maintenance-bots)
- `make git-push`
- install your dependencies
- write some code
### To Build
Once your project is set up:
- `make` to bundle and test
- commit
- `make git-push`
The `git-push` target pushes to Github first, to avoid conflicts with changes
from bots and other contributors.
### To Release
When your project is ready to release:
- `make release-dry` to make sure your changelog and options look right
- `make release`
Additional options can be passed with the `RELEASE_OPTS` variable. Frequently
used options include `--release-as minor` and `--prerelease`.
## External Services
This template works with or expects a few external services, namely a Gitlab
CI server (self-hosted or using Gitlab.com).
### Maintenance Bots
Good tests and clever bots can eliminate the most painful parts of project
maintenance. This repository is configured to work with:
- [CodeCov](https://codecov.io/)
- [Code Climate](https://codeclimate.com/)
- [LGTM](https://lgtm.com/)
- [Renovate](https://renovatebot.com/)
- [Snyk](https://snyk.io/)
- [SonarCloud](https://sonarcloud.io/)
None of these are required, but Renovate and Snyk can be very helpful when
dependencies release a security patch.
## External Secrets
This template expects a few secrets to exist in the environment, including
tokens for the [external services](#external-services).
| Name | Description |
| ------------------- | ----------------------------------------------------- |
| CC_TEST_REPORTER_ID | code climate ID |
| CODECLIMATE_SECRET | code climate token |
| CODECOV_SECRET | codecov token |
| DOCKER_SECRET | docker config, required for publishing images |
| GITHUB_SECRET | github.com token, required for publishing status |
| NPM_SECRET | npmjs.com token, required for publishing npm packages |
| SONAR_SECRET | sonarcloud token |
Secrets should be provided as environment variables, with the secret value
`base64`-encoded.

43
config/api-extractor.json Normal file
View File

@ -0,0 +1,43 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"projectFolder": "..",
"mainEntryPointFilePath": "<projectFolder>/out/src/index.d.ts",
"apiReport": {
"enabled": true,
"reportFolder": "<projectFolder>/out/",
"reportTempFolder": "<projectFolder>/out/tmp/"
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "<projectFolder>/out/api/<unscopedPackageName>.api.json"
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/out/index.d.ts",
"betaTrimmedFilePath": "<projectFolder>/out/index-beta.d.ts",
"publicTrimmedFilePath": "<projectFolder>/out/index-public.d.ts"
},
"tsdocMetadata": {
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
}
}
}
}

389
config/eslint.json Normal file
View File

@ -0,0 +1,389 @@
{
"env": {
"es6": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": [
"eslint-plugin-chai",
"eslint-plugin-chai-expect",
"eslint-plugin-chai-expect-keywords",
"eslint-plugin-import",
"eslint-plugin-mocha",
"eslint-plugin-no-null",
"eslint-plugin-sonarjs",
"@typescript-eslint",
"@typescript-eslint/tslint"
],
"rules": {
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": [
"error",
{
"default": "generic"
}
],
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-types": [
"error",
{
"types": {
"null": "Use 'undefined' instead of 'null'"
}
}
],
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit",
"overrides": {
"constructors": "no-public"
}
}
],
"@typescript-eslint/indent": [
"error",
2,
{
"ObjectExpression": "first",
"FunctionDeclaration": {
"parameters": "first"
},
"FunctionExpression": {
"parameters": "first"
},
"SwitchCase": 1
}
],
"@typescript-eslint/interface-name-prefix": "error",
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": [
"error",
{
"default": [
"public-static-method",
"public-static-field",
"public-instance-field",
"protected-instance-field",
"public-constructor",
"public-instance-method",
"protected-instance-method"
]
}
],
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"no-param-reassign": "error",
"@typescript-eslint/no-parameter-properties": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/no-use-before-declare": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/semi": [
"error",
"always"
],
"space-in-parens": [
"error",
"never"
],
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"arrow-parens": [
"error",
"always"
],
"camelcase": "error",
"complexity": [
"error",
{
"max": 12
}
],
"constructor-super": "error",
"curly": "error",
"default-case": "error",
"dot-notation": "error",
"eol-last": "error",
"eqeqeq": [
"error",
"always"
],
"guard-for-in": "error",
"id-blacklist": [
"error",
"any",
"Number",
"String",
"Boolean",
"Undefined"
],
"id-match": "error",
"import/no-default-export": "error",
"import/no-deprecated": "error",
"import/no-extraneous-dependencies": "off",
"import/no-internal-modules": "off",
"import/order": [
"error",
{
"groups": [
[
"builtin",
"external"
],
[
"index",
"parent",
"sibling",
"unknown"
]
]
}
],
"max-classes-per-file": [
"off",
1
],
"max-len": [
"error",
{
"code": 180
}
],
"max-lines": [
"error",
500
],
"new-parens": "error",
"no-bitwise": "off",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": "error",
"no-debugger": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-empty": "error",
"no-eval": "error",
"no-extra-bind": "error",
"no-fallthrough": "off",
"no-invalid-this": "error",
"no-irregular-whitespace": "error",
"no-magic-numbers": [
"error",
{
"ignore": [
-3,
-2,
-1,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
20,
30,
40,
50,
60,
70,
80,
90,
100
]
}
],
"no-multiple-empty-lines": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-null/no-null": "error",
"no-plusplus": [
"error",
{
"allowForLoopAfterthoughts": true
}
],
"no-redeclare": "error",
"no-restricted-syntax": [
"error",
"ForInStatement"
],
"no-return-await": "error",
"no-sequences": "error",
"no-shadow": [
"error",
{
"hoist": "all"
}
],
"no-sparse-arrays": "error",
"no-template-curly-in-string": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-useless-constructor": "error",
"no-var": "error",
"no-void": "error",
"max-params": [
"error",
4
],
"object-shorthand": "error",
"one-var": [
"error",
"never"
],
"prefer-const": "error",
"prefer-object-spread": "error",
"@typescript-eslint/prefer-readonly": "error",
"quote-props": [
"error",
"consistent-as-needed"
],
"radix": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"spaced-comment": "error",
"use-isnan": "error",
"valid-typeof": "off",
"sonarjs/max-switch-cases": "error",
"sonarjs/cognitive-complexity": "error",
"sonarjs/no-all-duplicated-branches": "error",
"sonarjs/no-collapsible-if": "error",
"sonarjs/no-collection-size-mischeck": "error",
"sonarjs/no-duplicate-string": "error",
"sonarjs/no-duplicated-branches": "error",
"sonarjs/no-element-overwrite": "error",
"sonarjs/no-identical-conditions": "error",
"sonarjs/no-identical-expressions": "error",
"sonarjs/no-identical-functions": "error",
"sonarjs/no-inverted-boolean-check": "error",
"sonarjs/no-redundant-boolean": "error",
"sonarjs/no-redundant-jump": "error",
"sonarjs/no-same-line-conditional": "error",
"sonarjs/no-useless-catch": "error",
"sonarjs/prefer-immediate-return": "error",
"@typescript-eslint/tslint/config": [
"error",
{
"rules": {
"ban": [
true,
{
"message": "use lodash isString",
"name": [
"util",
"isString"
]
},
{
"message": "use lodash isNil",
"name": [
"util",
"isNullOrUndefined"
]
}
],
"import-spacing": true,
"jsdoc-format": [
true,
"check-multiline-start"
],
"no-boolean-literal-compare": true,
"no-dynamic-delete": true,
"no-inferred-empty-object-type": true,
"no-reference-import": true,
"object-literal-sort-keys": true,
"one-line": [
true,
"check-catch",
"check-else",
"check-finally",
"check-open-brace",
"check-whitespace"
],
"prefer-switch": true,
"strict-type-predicates": true,
"trailing-comma": [
true,
{
"esSpecCompliant": true,
"multiline": {
"arrays": "always",
"functions": "never",
"object": "always"
},
"singleline": "never"
}
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast",
"check-type-operator",
"check-rest-spread"
]
}
}
]
}
}

View File

@ -0,0 +1,7 @@
.deploy-branches:
except:
- tags
.deploy-tags:
only:
- tags

View File

@ -0,0 +1,61 @@
.build-curl:
image: apextoaster/base:1.2
tags:
- platform:k8s
- runner:shared
.build-climate:
image: apextoaster/code-climate:0.6
tags:
- platform:k8s
- runner:shared
allow_failure: false
variables:
CI_BRANCH: "${CI_COMMIT_REF_NAME}"
GIT_BRANCH: "${CI_COMMIT_REF_NAME}"
GIT_COMMIT_SHA: "${CI_COMMIT_SHA}"
.build-codecov:
image: apextoaster/codecov:3.1
tags:
- platform:k8s
- runner:shared
allow_failure: false
.build-docker:
image: apextoaster/docker:18.09
services:
- apextoaster/docker-dind:18.09
tags:
- platform:k8s
- runner:shared
allow_failure: false
before_script:
- mkdir ${HOME}/.docker
- echo "${DOCKER_SECRET}" | base64 -d > ${HOME}/.docker/config.json
script:
- ${CI_PROJECT_DIR}/scripts/docker-build.sh --push
after_script:
- rm -rfv ${HOME}/.docker
variables:
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://localhost:2375
.build-node:
image: apextoaster/node:10.1
tags:
- platform:k8s
- runner:shared
allow_failure: false
before_script:
- echo "${NPM_SECRET}" | base64 -d > ${HOME}/.npmrc
.build-sonar:
image: apextoaster/sonar-scanner:3.3
tags:
- platform:k8s
- runner:shared
allow_failure: false

4
config/mocha.json Executable file
View File

@ -0,0 +1,4 @@
{
"reporter": ["json"],
"ui": ["bdd"]
}

View File

@ -0,0 +1,7 @@
{
"names": [
"async_hooks",
"chai",
"sinon"
]
}

View File

@ -0,0 +1 @@
{}

6
config/rollup-named.json Normal file
View File

@ -0,0 +1,6 @@
{
"node_modules/chai/index.js": [
"expect",
"use"
]
}

3
config/rollup-stub.json Normal file
View File

@ -0,0 +1,3 @@
{
"names": []
}

128
config/rollup.js Normal file
View File

@ -0,0 +1,128 @@
import { join, sep } from 'path';
import commonjs from 'rollup-plugin-commonjs';
import { eslint } from 'rollup-plugin-eslint';
import json from 'rollup-plugin-json';
import multiEntry from 'rollup-plugin-multi-entry';
import externals from 'rollup-plugin-node-externals';
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import typescript from 'rollup-plugin-typescript2';
import yaml from 'rollup-plugin-yaml';
const debug = process.env['DEBUG'] === 'TRUE';
const metadata = require('../package.json');
const external = require('./rollup-external.json').names;
const globals = require('./rollup-globals.json');
const namedExports = require('./rollup-named.json');
const stubNames = require('./rollup-stub.json').names;
const passStub = 'require("pass-stub")';
const stubs = stubNames.reduce((p, c) => (p[c] = passStub, p), {});
const rootPath = process.env['ROOT_PATH'];
const targetPath = process.env['TARGET_PATH'];
const bundle = {
external,
input: {
include: [
join(rootPath, 'src', 'index.ts'),
join(rootPath, 'test', 'harness.ts'),
join(rootPath, 'test', '**', 'Test*.ts'),
],
},
manualChunks(id) {
if (id.includes(`${sep}test${sep}`)) {
return 'test';
}
if (id.match(/commonjs-external/i) || id.match(/commonjsHelpers/)) {
return 'vendor';
}
if (id.includes(`${sep}node_modules${sep}`)) {
return 'vendor';
}
if (id.includes(`${sep}src${sep}index`)) {
return 'index';
}
if (id.includes(`${sep}src${sep}`)) {
return 'main';
}
if (debug) {
console.log('file belongs to no chunk', id);
}
return 'nochunk';
},
output: {
dir: targetPath,
chunkFileNames: '[name].js',
entryFileNames: 'entry-[name].js',
format: 'cjs',
globals,
sourcemap: true,
},
plugins: [
multiEntry(),
json(),
yaml(),
externals({
builtins: true,
deps: true,
devDeps: false,
peerDeps: false,
}),
replace({
delimiters: ['require("', '")'],
values: stubs,
}),
replace({
delimiters: ['require(\'', '\')'],
values: stubs,
}),
replace({
delimiters: ['{{ ', ' }}'],
values: {
BUILD_JOB: process.env['CI_JOB_ID'],
BUILD_RUNNER: process.env['CI_RUNNER_DESCRIPTION'],
GIT_BRANCH: process.env['CI_COMMIT_REF_SLUG'],
GIT_COMMIT: process.env['CI_COMMIT_SHA'],
NODE_VERSION: process.env['NODE_VERSION'],
PACKAGE_NAME: metadata.name,
PACKAGE_VERSION: metadata.version,
},
}),
resolve({
preferBuiltins: true,
}),
commonjs({
namedExports,
}),
eslint({
configFile: join('.', 'config', 'eslint.json'),
exclude: [
join('node_modules', '**'),
join('src', 'resource'),
join('src', '**', '*.json'),
join('src', '**', '*.yml'),
],
include: [
join('**', '*.ts'),
],
throwOnError: true,
}),
typescript({
cacheRoot: join(targetPath, 'cache', 'rts2'),
rollupCommonJSResolveHack: true,
}),
],
};
export default [
bundle,
];

45
config/tsconfig.json Executable file
View File

@ -0,0 +1,45 @@
{
"compileOnSave": false,
"compilerOptions": {
"allowJs": false,
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationMap": true,
"experimentalDecorators": true,
"importHelpers": true,
"jsx": "react",
"lib": [
"dom",
"es2017"
],
"module": "es6",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"outDir": "../out",
"sourceMap": true,
"strict": true,
"strictNullChecks": true,
"target": "es2017",
"types": [
"chai-as-promised",
"mocha",
"node",
"rollup-resources",
"sinon-chai"
],
"typeRoots": [
"../node_modules/@types",
"../node_modules",
"../vendor"
]
},
"exclude": [
"../node_modules"
],
"include": [
"../src/**/*",
"../test/**/*"
]
}

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^0.1.3",
"@microsoft/api-documenter": "^7.5.8",
"@microsoft/api-extractor": "^7.5.6",
"@types/chai": "^4.2.5",
"@types/chai-as-promised": "^7.1.2",
"@types/mocha": "^5.2.7",
"@types/sinon-chai": "^3.2.3",
"@types/source-map-support": "^0.5.0",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/eslint-plugin-tslint": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"eslint-plugin-chai": "^0.0.1",
"eslint-plugin-chai-expect": "^2.0.1",
"eslint-plugin-chai-expect-keywords": "^1.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-mocha": "^6.2.1",
"eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-sonarjs": "^0.5.0",
"mocha": "^6.2.2",
"nyc": "^14.1.1",
"rollup": "^1.27.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-multi-entry": "^2.1.0",
"rollup-plugin-node-externals": "^2.1.2",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-tslint": "^0.2.2",
"rollup-plugin-typescript2": "^0.25.2",
"rollup-plugin-yaml": "^2.0.0",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"source-map-support": "^0.5.16",
"standard-version": "^7.0.0",
"tslint": "^5.20.1",
"tslint-clean-code": "^0.2.10",
"tslint-consistent-codestyle": "^1.16.0",
"tslint-etc": "^1.9.2",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-sonarts": "^1.9.0",
"typescript": "^3.7.2"
},
"peerDependencies": {
"js-yaml": "^3.13.1"
}
}

8
renovate.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": [
"config:base",
":semanticCommitTypeAll(update)",
":semanticCommitScopeDisabled"
],
"branchPrefix": "update/"
}

1
run.mk Normal file
View File

@ -0,0 +1 @@
ci: all

7
scripts/ci-project-path.sh Executable file
View File

@ -0,0 +1,7 @@
#! /bin/bash
PROJECT="${ROOT_PATH}"
PARENT="$(dirname "${PROJECT}")"
PROJECT_PATH="$(basename "${PARENT}")/$(basename "${PROJECT}")"
echo "${PROJECT_PATH}"

27
scripts/docker-build.sh Executable file
View File

@ -0,0 +1,27 @@
#! /bin/bash
IMAGE_PUSH="${1:---skip}"
IMAGE_DEFAULT="${2:---skip}"
IMAGE_NAME="${CI_PROJECT_PATH}"
IMAGE_TAG="$(echo "${CI_COMMIT_TAG:-${CI_COMMIT_REF_SLUG}}" | sed -r 's/[^-_a-zA-Z0-9\\.]/-/g')"
IMAGE_SHORT="${IMAGE_NAME}:${IMAGE_TAG}"
IMAGE_FULL="${IMAGE_NAME}:${IMAGE_TAG}-${IMAGE_ARCH}"
echo "Building image: ${IMAGE_FULL}"
docker build -f "Dockerfile.${IMAGE_ARCH}" -t "${IMAGE_FULL}" .
if [[ "${IMAGE_PUSH}" == "--push" ]];
then
echo "Pushing image: ${IMAGE_FULL}"
docker push "${IMAGE_FULL}"
fi
if [[ "${IMAGE_DEFAULT}" == "--default" ]];
then
echo "Pushing image (default architecture): ${IMAGE_SHORT}"
docker tag "${IMAGE_FULL}" "${IMAGE_SHORT}"
docker push "${IMAGE_SHORT}"
fi

19
scripts/github-status.sh Executable file
View File

@ -0,0 +1,19 @@
#! /bin/sh
STATUS="${1}"
CI_COMMIT_SHA="${CI_COMMIT_SHA:-$(git rev-parse HEAD)}"
STATUS_BODY="$(cat <<EOF
{
"state": "${STATUS}",
"target_url": "${CI_PIPELINE_URL}",
"description": "CI pipeline ${STATUS}!",
"context": "gitlab/build"
}
EOF
)"
printf "Reporting status for %s...\n%s" "${CI_COMMIT_SHA}" "${STATUS_BODY}"
printf "%s" "${STATUS_BODY}" | curl -d @- \
-H "Authorization: token $(printf "%s" "${GITHUB_SECRET}" | base64 -d)" \
-i "https://api.github.com/repos/${CI_PROJECT_PATH}/statuses/${CI_COMMIT_SHA}"

12
src/index.ts Normal file
View File

@ -0,0 +1,12 @@
import { main } from './app';
const STATUS_ERROR = 1;
/**
* This is the main entry-point to the program and the only file not included in the main bundle.
*/
main(process.argv).then((status) => process.exit(status)).catch((err: Error) => {
// eslint-disable-next-line no-console
console.error('uncaught error during main:', err);
process.exit(STATUS_ERROR);
});

5
src/lib.ts Normal file
View File

@ -0,0 +1,5 @@
import { VERSION_INFO } from './version';
export default {
VERSION_INFO,
};

3
src/resource/json.json Normal file
View File

@ -0,0 +1,3 @@
{
"foo": {}
}

1
src/resource/yaml.yml Normal file
View File

@ -0,0 +1 @@
foo: {}

14
src/schema.ts Normal file
View File

@ -0,0 +1,14 @@
import { DEFAULT_SAFE_SCHEMA, Schema } from 'js-yaml';
import { envType } from './type/Env';
import { includeType } from './type/Include';
import { regexpType } from './type/Regexp';
import { streamType } from './type/Stream';
export const CONFIG_ENV = 'SALTY_HOME';
export const CONFIG_SCHEMA = Schema.create([DEFAULT_SAFE_SCHEMA], [
envType,
includeType,
regexpType,
streamType,
]);

17
src/type/Env.ts Normal file
View File

@ -0,0 +1,17 @@
import { Type as YamlType } from 'js-yaml';
import { NotFoundError } from '../../error/NotFoundError';
export const envType = new YamlType('!env', {
kind: 'scalar',
resolve(name: string) {
if (Reflect.has(process.env, name)) {
return true;
} else {
throw new NotFoundError(`environment variable not found: ${name}`);
}
},
construct(name: string) {
return Reflect.get(process.env, name);
},
});

47
src/type/Include.ts Normal file
View File

@ -0,0 +1,47 @@
import { existsSync, readFileSync, realpathSync } from 'fs';
import { SAFE_SCHEMA, safeLoad, Type as YamlType } from 'js-yaml';
import { BaseError } from 'noicejs';
import { join } from 'path';
import { NotFoundError } from '../../error/NotFoundError';
// work around the circular dependency by setting the schema later
export const includeSchema = {
schema: SAFE_SCHEMA,
};
export const includeType = new YamlType('!include', {
kind: 'scalar',
resolve(path: string) {
try {
const canonical = resolvePath(path);
// throws in node 11+
if (existsSync(canonical)) {
return true;
} else {
throw new NotFoundError('included file does not exist');
}
} catch (err) {
throw new NotFoundError('included file does not exist', err);
}
},
construct(path: string): unknown {
try {
return safeLoad(readFileSync(resolvePath(path), {
encoding: 'utf-8',
}), {
schema: includeSchema.schema,
});
} catch (err) {
throw new BaseError('error including file', err);
}
},
});
export function resolvePath(path: string): string {
if (path[0] === '.') {
return realpathSync(join(__dirname, path));
} else {
return realpathSync(path);
}
}

21
src/type/Regexp.ts Normal file
View File

@ -0,0 +1,21 @@
import { Type as YamlType } from 'js-yaml';
import { isNil } from 'lodash';
import { InvalidArgumentError } from '../../error/InvalidArgumentError';
export const REGEXP_REGEXP = /^\/(.+)\/([gimsuy]*)$/;
export const regexpType = new YamlType('!regexp', {
kind: 'scalar',
resolve(value: string) {
return REGEXP_REGEXP.test(value);
},
construct(value: string): RegExp {
const match = REGEXP_REGEXP.exec(value);
if (isNil(match)) {
throw new InvalidArgumentError('invalid regexp');
}
const [/* input */, expr, flags] = Array.from(match);
return new RegExp(expr, flags);
},
});

22
src/type/Stream.ts Normal file
View File

@ -0,0 +1,22 @@
import { Type as YamlType } from 'js-yaml';
import { NotFoundError } from '../../error/NotFoundError';
const ALLOWED_STREAMS = new Set([
'stdout',
'stderr',
]);
export const streamType = new YamlType('!stream', {
kind: 'scalar',
resolve(name: string) {
if (ALLOWED_STREAMS.has(name) && Reflect.has(process, name)) {
return true;
} else {
throw new NotFoundError(`process stream not found: ${name}`);
}
},
construct(name: string) {
return Reflect.get(process, name);
},
});

15
src/version.ts Normal file
View File

@ -0,0 +1,15 @@
export const VERSION_INFO = {
build: {
job: '{{ BUILD_JOB }}',
node: '{{ NODE_VERSION }}',
runner: '{{ BUILD_RUNNER }}',
},
git: {
branch: '{{ GIT_BRANCH }}',
commit: '{{ GIT_COMMIT }}',
},
package: {
name: '{{ PACKAGE_NAME }}',
version: '{{ PACKAGE_VERSION }}',
},
};

22
test/harness.ts Normal file
View File

@ -0,0 +1,22 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinonChai from 'sinon-chai';
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install({
environment: 'node',
handleUncaughtExceptions: true,
hookRequire: true,
});
/**
* This will break the whole test run if any test leaks an unhandled rejection.
*/
process.on('unhandledRejection', (reason, promise) => {
// eslint-disable-next-line no-console
console.error('unhandled error during tests', reason);
process.exit(1);
});
chai.use(chaiAsPromised);
chai.use(sinonChai);

154
test/helpers/async.ts Normal file
View File

@ -0,0 +1,154 @@
import { AsyncHook, createHook } from 'async_hooks';
// this will pull Mocha internals out of the stacks
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
const { stackTraceFilter } = require('mocha/lib/utils');
const filterStack = stackTraceFilter();
type AsyncMochaTest = (this: Mocha.Context | void) => Promise<void>;
type AsyncMochaSuite = (this: Mocha.Suite) => Promise<void>;
function isNil<T>(val: T | null | undefined): val is null | undefined {
/* eslint-disable-next-line no-null/no-null */
return val === null || val === undefined;
}
export interface TrackedResource {
source: string;
triggerAsyncId: number;
type: string;
}
function debugMode() {
return Reflect.has(process.env, 'DEBUG');
}
/**
* Async resource tracker using node's internal hooks.
*
* This probably won't work in a browser. It does not hold references to the resource, to avoid leaks.
* Adapted from https://gist.github.com/boneskull/7fe75b63d613fa940db7ec990a5f5843#file-async-dump-js
*/
export class Tracker {
public static getStack(): string {
const err = new Error();
if (isNil(err.stack)) {
return 'no stack trace available';
} else {
return filterStack(err.stack);
}
}
private readonly hook: AsyncHook;
private readonly resources: Map<number, TrackedResource>;
constructor() {
this.resources = new Map();
this.hook = createHook({
destroy: (id: number) => {
this.resources.delete(id);
},
init: (id: number, type: string, triggerAsyncId: number) => {
const source = Tracker.getStack();
// @TODO: exclude async hooks, including this one
this.resources.set(id, {
source,
triggerAsyncId,
type,
});
},
promiseResolve: (id: number) => {
this.resources.delete(id);
},
});
}
public clear() {
this.resources.clear();
}
public disable() {
this.hook.disable();
}
/* eslint-disable no-console, no-invalid-this */
public dump() {
console.error(`tracking ${this.resources.size} async resources`);
this.resources.forEach((res, id) => {
console.error(`${id}: ${res.type}`);
if (debugMode()) {
console.error(res.source);
console.error('\n');
}
});
}
public enable() {
this.hook.enable();
}
public get size(): number {
return this.resources.size;
}
}
/**
* Describe a suite of async tests. This wraps mocha's describe to track async resources and report leaks.
*/
export function describeLeaks(description: string, cb: AsyncMochaSuite): Mocha.Suite {
return describe(description, function trackSuite(this: Mocha.Suite) {
const tracker = new Tracker();
beforeEach(() => {
tracker.enable();
});
afterEach(() => {
tracker.disable();
const leaked = tracker.size;
// @TODO: this should only exclude the single Immediate set by the Tracker
if (leaked > 1) {
tracker.dump();
const msg = `test leaked ${leaked - 1} async resources`;
if (debugMode()) {
throw new Error(msg);
} else {
/* eslint-disable-next-line no-console */
console.warn(msg);
}
}
tracker.clear();
});
const suite: PromiseLike<void> | undefined = cb.call(this);
if (isNil(suite) || !Reflect.has(suite, 'then')) {
/* eslint-disable-next-line no-console */
console.error(`test suite '${description}' did not return a promise`);
}
return suite;
});
}
/**
* Run an asynchronous test with unhandled rejection guards.
*
* This function may not have any direct test coverage. It is too simple to reasonably mock.
*/
export function itLeaks(expectation: string, cb?: AsyncMochaTest): Mocha.Test {
if (isNil(cb)) {
return it(expectation);
}
return it(expectation, function trackTest(this: Mocha.Context) {
return new Promise<unknown>((res, rej) => {
cb.call(this).then((value: unknown) => {
res(value);
}, (err: Error) => {
rej(err);
});
});
});
}

3
tsconfig.json Executable file
View File

@ -0,0 +1,3 @@
{
"extends": "./config/tsconfig.json"
}

9
vendor/rollup-resources/index.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module "*.json" {
export const data: unknown;
export default data;
}
declare module "*.yml" {
export const data: unknown;
export default data;
}

3660
yarn.lock Normal file

File diff suppressed because it is too large Load Diff