diff --git a/.github/contributing.md b/.github/contributing.md
index ee421c4f..0bcb2c32 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -11,8 +11,9 @@
## Prerequisites
-- [Node.js](https://nodejs.org) (>= 14.17)
-- [npm](https://www.npmjs.com) (>= 6.14)
+- [Node.js](https://nodejs.org) (>= 16.13)
+- [npm](https://www.npmjs.com) (>= 7.x)
+- [pnpm](https://pnpm.io) (>= 6.x)
- [yarn](https://yarnpkg.com) (>= 1.22)
- [Git](https://git-scm.com) (>= 2.20)
@@ -22,6 +23,7 @@
$ git clone https://github.com/zce/caz.git
$ cd caz
$ npm install
+$ npm run build
$ npm link
```
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c0783ab3..d37c07bc 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -5,30 +5,21 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
-
strategy:
matrix:
- node-version: [12, 14, 16]
-
+ node-version: [14, 16, 17]
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
+ - run: npm i pnpm yarn -g
+ - run: git config --global user.name "GitHub Actions"
+ - run: git config --global user.email "bots@github.com"
- run: npm install
- run: npm run lint
- run: npm run build
- run: npm run test
-
- codecov:
- if: github.ref == 'refs/heads/main'
- needs: build
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
- - run: npm install
- - run: npm run test
- run: npx codecov
publish:
@@ -36,8 +27,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
with:
registry-url: https://registry.npmjs.org
- run: npm install
diff --git a/.gitignore b/.gitignore
index fe01137e..96a9b15b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,10 +2,11 @@
node_modules
-/lib
+/dist
/coverage
/test/.temp
package-lock.json
+pnpm-lock.yaml
yarn.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 954b665a..b751d6a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [1.0.0] - 2022-03-24
+
+- zero dependency by tsup
+- node v14.14.0 required
+- auto install template dependencies
+- support native esm
+- upgrade all dependencies
+- refactor test
+- chore: minify output dist
+- docs: chinese docs
+- fix: dts output
+
## [0.8.2] - 2022-01-20
- fix: output types.d.ts
@@ -84,6 +96,7 @@
+[1.0.0]: https://github.com/zce/caz/compare/v0.8.2...v1.0.0
[0.8.2]: https://github.com/zce/caz/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/zce/caz/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/zce/caz/compare/v0.7.0...v0.8.0
diff --git a/README.md b/README.md
index 55a529e2..df59b5a2 100644
--- a/README.md
+++ b/README.md
@@ -7,18 +7,19 @@
-
+
-
-
-
+
+
+**English** | [简体中文](README.zh-CN.md)
+
## Introduction
CAZ (**C**reate **A**pp **Z**en)
@@ -36,7 +37,7 @@ _For more introduction, please refer to the [How it works](#how-it-works)._
- Light-weight
- Still powerful
- High efficiency
-- Less dependencies
+- Zero dependencies
- Template-based
- Configurable
- Extensible
@@ -58,11 +59,13 @@ _For more introduction, please refer to the [How it works](#how-it-works)._
- [Local Templates](#local-templates)
- [Remote ZIP Templates](#remote-zip-templates)
- [Offline Mode](#offline-mode)
+ - [Prompts Override](#prompts-override)
+ - [Debug Mode](#debug-mode)
- [List Available Templates](#list-available-templates)
- - [Official Templates](#official-templates)
+- [Official Templates](#official-templates)
- [Advanced](#advanced)
- - [Create Your Template](#create-your-template)
- [Configuration](#configuration)
+ - [Create Your Template](#create-your-template)
- [Create Your Scaffold](#create-your-scaffold)
- [References](#references)
- [Motivation](#motivation)
@@ -77,8 +80,8 @@ _For more introduction, please refer to the [How it works](#how-it-works)._
### Prerequisites
-- [Node.js](https://nodejs.org) (>= 12.10 required, >= 14.17 preferred)
-- [npm](https://www.npmjs.com) (>= 6.x) or [yarn](https://yarnpkg.com) (>= 1.22)
+- [Node.js](https://nodejs.org) (>= 14.14 required, >= 16.13 preferred)
+- [npm](https://www.npmjs.com) (>= 7.x) or [pnpm](https://pnpm.io) (>= 6.x) or [yarn](https://yarnpkg.com) (>= 1.22)
- [Git](https://git-scm.com) (>= 2.0)
### Installation
@@ -182,17 +185,21 @@ CAZ allows you to specify prompt response answers through cli parameters.
$ caz minima my-project --name my-proj
```
+By running this command, you don't have to answer the next `name` prompts.
+
### Debug Mode
```shell
$ caz nm my-project --debug
```
-`--debug` parameter will open the debug mode, In debug mode, once an exception occurs, the exception details will be automatically output. This is very helpful in finding errors in the template.
+`--debug` parameter will open the debug mode.
+
+In debug mode, once an exception occurs, the exception details will be automatically output. This is very helpful in finding errors in the template.
### List Available Templates
-Show all available templates
+Show all available templates:
```shell
$ caz list [owner] [-j|--json] [-s|--short]
@@ -207,16 +214,17 @@ $ caz list [owner] [-j|--json] [-s|--short]
- `-j, --json`: Output with json format
- `-s, --short`: Output with short format
-### Official Templates
+## Official Templates
Current available templates list:
- [template](https://github.com/caz-templates/template) - for creating [caz](https://github.com/zce/caz) templates.
- [nm](https://github.com/caz-templates/nm) - for creating [node](https://nodejs.org) modules.
-- [react](https://github.com/caz-templates/react) - for creating modern [react](https://reactjs.org) app.
-- [vue](https://github.com/caz-templates/vue) - for creating modern [vue.js](https://vuejs.org) app.
-- [vite](https://github.com/caz-templates/vite) - for creating vue.js app powered by [vite](https://github.com/vitejs/vite).
-- [electron](https://github.com/caz-templates/electron) - :construction: for creating [electron](https://electronjs.org) app.
+- [vercel](https://github.com/caz-templates/vercel) - for creating [vercel](https://vercel.com) apps.
+- [react](https://github.com/caz-templates/react) - for creating modern [react](https://reactjs.org) apps.
+- [vue](https://github.com/caz-templates/vue) - for creating modern [vue.js](https://vuejs.org) apps.
+- [vite](https://github.com/caz-templates/vite) - for creating vue.js apps powered by [vite](https://github.com/vitejs/vite).
+- [electron](https://github.com/caz-templates/electron) - :construction: for creating [electron](https://electronjs.org) apps.
- [mp](https://github.com/caz-templates/mp) - :construction: for creating wechat [mini-programs](https://developers.weixin.qq.com/miniprogram/dev/framework).
- [jekyll](https://github.com/caz-templates/jekyll) - :construction: for creating [jekyll](https://jekyllrb.com) site.
- [x-pages](https://github.com/caz-templates/x-pages) - for creating [x-pages](https://github.com/zce/x-pages) static site.
@@ -225,26 +233,16 @@ Maybe more: https://github.com/caz-templates
> You can also run `$ caz list` to see all available official templates in real time.
-## Advanced
-
-### Create Your Template
-
-```shell
-$ caz template my-template
-```
+**All templates are currently hosted on GitHub, Chinese users can [use the mirror](#mirror-for-chinese) on coding.net.**
-The above command will pulls the template from [caz-templates/template](https://github.com/caz-templates/template), and help you create your own CAZ template.
-
-To create and distribute your own template, please refer to the [How to create template](docs/create-template.md).
-
-> Maybe fork an official template is also a good decision.
+## Advanced
### Configuration
CAZ will read the configuration file in `~/.cazrc`, default config:
```ini
-; template download registry,
+; template download registry
; {owner} & {name} & {branch} will eventually be replaced by the corresponding value.
registry = https://github.com/{owner}/{name}/archive/{branch}.zip
; template offlicial organization name
@@ -271,6 +269,29 @@ $ caz nm my-project
The above command will download & extract template from `https://gitlab.com/faker/nm/archive/main.zip`.
+#### Mirror for Chinese
+
+Due to network limitations, the template download may time out, you can consider using the mirror repository I configured on [coding.net](https://coding.net).
+
+`~/.cazrc`:
+
+```ini
+registry = https://zce.coding.net/p/{owner}/d/{name}/git/archive/{branch}
+official = caz
+```
+
+### Create Your Template
+
+```shell
+$ caz template my-template
+```
+
+The above command will pulls the template from [caz-templates/template](https://github.com/caz-templates/template), and help you create your own CAZ template.
+
+To create and distribute your own template, please refer to the [How to create template](docs/create-template.md).
+
+> Maybe fork an official template is also a good decision.
+
### Create Your Scaffold
```shell
@@ -286,20 +307,18 @@ with ESM and async/await:
```javascript
import caz from 'caz'
-;(async () => {
- try {
- const template = 'nm'
- // project path (relative cwd or full path)
- const project = 'my-project'
- const options = { force: false, offline: false }
- // scaffolding by caz...
- await caz(template, project, options)
- // success created my-project by nm template
- } catch (e) {
- // error handling
- console.error(e)
- }
-})()
+try {
+ const template = 'nm'
+ // project path (relative cwd or full path)
+ const project = 'my-project'
+ const options = { force: false, offline: false }
+ // scaffolding by caz...
+ await caz(template, project, options)
+ // success created my-project by nm template
+} catch (e) {
+ // error handling
+ console.error(e)
+}
```
or with CommonJS and Promise:
@@ -326,8 +345,6 @@ This means that you can develop your own scaffolding module based on it.
To create and distribute your own scaffolding tools, please refer to the [How to create scaffolding tools based on CAZ](docs/create-scaffold.md).
-
-
## References
@@ -339,12 +356,12 @@ Create new project from a template
#### template
- Type: `string`
-- Details: template name
+- Details: template name, it can also be a template folder path
#### project
- Type: `string`
-- Details: project name
+- Details: project name, it can also be a project folder path
- Default: `'.'`
#### options
@@ -355,20 +372,20 @@ Create new project from a template
##### force
-Type: `boolean`
-Details: overwrite if the target exists
-Default: `false`
+- Type: `boolean`
+- Details: overwrite if the target exists
+- Default: `false`
##### offline
-Type: `boolean`
-Details: try to use an offline template
-Default: `false`
+- Type: `boolean`
+- Details: try to use an offline template
+- Default: `false`
##### [key: string]
-Type: `any`
-Details: cli options to override prompts
+- Type: `any`
+- Details: cli options to override prompts
## Motivation
@@ -376,13 +393,11 @@ Details: cli options to override prompts
Joking: I want to make wheels ;P
-The real reason is that I think I need a scaffolding tool that is more suitable for my personal productivity.
+The real reason is that I think I need a scaffolding tool that is more suitable for my personal productivity. The existing tools have more or less certain limitations because of their different starting points.
Nothing else.
-
-
-## About
+## Concepts
### How It Works
@@ -392,22 +407,22 @@ Nothing else.
#### Main Workflow
-The [core code](src/init/index.ts) is based on the middleware mechanism provided by [zce/mwa](https://github.com/zce/mwa).
+The [core code](src/index.ts) is based on the middleware mechanism provided by [zce/mwa](https://github.com/zce/mwa).
The following middleware will be executed sequentially.
-1. [confirm](src/init/confirm.ts) - Confirm destination by [prompts](https://github.com/terkelg/prompts).
-2. [resolve](src/init/resolve.ts) - Resolve template from remote or local.
-3. [load](src/init/load.ts) - Load template config by require.
-4. [inquire](src/init/inquire.ts) - Inquire template prompts by [prompts](https://github.com/terkelg/prompts).
-5. [setup](src/init/setup.ts) - Apply template setup hook.
-6. [prepare](src/init/prepare.ts) - Prepare all template files.
-7. [rename](src/init/rename.ts) - Rename file if necessary.
-8. [render](src/init/render.ts) - Render file if template.
-9. [emit](src/init/emit.ts) - Emit files to destination.
-10. [install](src/init/install.ts) - Execute `npm | yarn | pnpm install` command.
-11. [init](src/init/init.ts) - Execute `git init && git add && git commit` command.
-12. [complete](src/init/complete.ts) - Apply template complete hook.
+1. [confirm](src/confirm.ts) - Confirm destination by [prompts](https://github.com/terkelg/prompts).
+2. [resolve](src/resolve.ts) - Resolve template from remote or local filesystem.
+3. [load](src/load.ts) - Install template dependencies, load template config by `require`.
+4. [inquire](src/inquire.ts) - Inquire template prompts by [prompts](https://github.com/terkelg/prompts).
+5. [setup](src/setup.ts) - Only apply template setup hook function.
+6. [prepare](src/prepare.ts) - Filter out unnecessary files and prepare all files to be generated.
+7. [rename](src/rename.ts) - Rename each file if the filename contains interpolations.
+8. [render](src/render.ts) - Render the contents of each file if template.
+9. [emit](src/emit.ts) - Emit files to destination.
+10. [install](src/install.ts) - Execute `npm | yarn | pnpm install` command if necessary.
+11. [init](src/init.ts) - Execute `git init && git add && git commit` command if necessary.
+12. [complete](src/complete.ts) - Only apply template complete hook function.
### Built With
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 00000000..244dde04
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,468 @@
+
+
+
+
+**简体中文** | [English](README.md)
+
+## 简介
+
+CAZ (**C**reate **A**pp **Z**en)
+
+这是一个基于模板机制、简单而强大的脚手架工具,用于提升我个人生产力,受启发于 [Yeoman](https://yeoman.io)、[Vue CLI 2](https://npm.im/vue-cli) 等项目。
+
+- 读作:[[kæz]](http://dict.youdao.com/dictvoice?audio=caz) 📷 ✌
+- 写作:CAZ / caz
+
+_更多介绍,请阅读[它如何工作](#如何工作)。_
+
+### 特性
+
+- 简单易用
+- 轻量化
+- 依然强大
+- 高工作效率
+- 零生产依赖
+- 基于模板
+- 可配置
+- 可扩展
+- 使用 TypeScript
+- 使用现代化的 API
+
+> 稍后我会给出具体的理由。
+
+## 目录
+
+- [简介](#简介)
+ - [特性](#特性)
+- [起步](#起步)
+ - [环境准备](#环境准备)
+ - [安装](#安装)
+ - [快速起步](#快速起步)
+- [配方](#配方)
+ - [GitHub 仓库模板](#gitHub-仓库模板)
+ - [本地模板](#本地模板)
+ - [远程压缩包模板](#远程压缩包模板)
+ - [离线模式](#离线模式)
+ - [命令行参数](#命令行参数)
+ - [调试模式](#调试模式)
+ - [列出可用模板](#列出可用模板)
+- [官方模板](#官方模板)
+- [高级](#高级)
+ - [配置选项](#配置选项)
+ - [创建你的模板](#创建你的模板)
+ - [创建你的脚手架](#创建你的脚手架)
+- [参考资料](#参考资料)
+- [开发动机](#开发动机)
+- [概念](#概念)
+ - [如何工作](#如何工作)
+ - [用到什么](#用到什么)
+- [路线图](#路线图)
+- [参与贡献](#参与贡献)
+- [许可证](#许可证)
+
+## 起步
+
+### 环境准备
+
+- [Node.js](https://nodejs.org) (必须 >= 14.14, >= 16.13 更佳)
+- [npm](https://www.npmjs.com) (>= 7.x) 或 [pnpm](https://pnpm.io) (>= 6.x) 或 [yarn](https://yarnpkg.com) (>= 1.22)
+- [Git](https://git-scm.com) (>= 2.0)
+
+### 安装
+
+```shell
+# 全局安装
+$ npm install -g caz
+
+# 或者使用 yarn 安装
+$ yarn global add caz
+```
+
+### 快速起步
+
+使用模板创建一个新项目。
+
+```shell
+$ caz [project] [-f|--force] [-o|--offline]
+
+# 使用官方模板
+$ caz [project]
+
+# 使用 GitHub 仓库(自定义模板)
+$ caz / [project]
+```
+
+如果您只是偶尔使用它,我建议您使用 `npx` 直接运行 `caz`。
+
+```shell
+$ npx caz [project] [-f|--force] [-o|--offline]
+```
+
+#### 选项
+
+- `-f, --force`: 如果目标存在就覆盖掉
+- `-o, --offline`: 尝试使用本地离线缓存模板
+
+## 配方
+
+### GitHub 仓库模板
+
+```shell
+$ caz nm my-project
+```
+
+此命令会从 [caz-templates/nm](https://github.com/caz-templates/nm) 拉取模板,然后根据模板的配置,询问你一些问题,最后生成项目在 `./my-project`。
+
+```shell
+$ caz nm#typescript my-project
+```
+
+运行此命令,CAZ 将从 [caz-templates/nm](https://github.com/caz-templates/nm) 的 `typescript` 分支拉取模板。
+
+#### 使用自定义模板
+
+```shell
+$ caz zce/nm my-project
+```
+
+此命令会从 [zce/nm](https://github.com/zce/nm) 拉取模板。这意味着你也可以从你的公开 GitHub 仓库拉取模板。
+
+**注意:模板必须使用公开的仓库。**
+
+### 本地模板
+
+你也可以使用本地文件系统的模板。
+
+例如:
+
+```shell
+$ caz ~/local/template my-project
+```
+
+以上命令将使用 `~/local/template` 文件夹作为模板。
+
+### 远程压缩包模板
+
+你也可以使用 zip 压缩包的模板。
+
+例如:
+
+```shell
+$ caz https://cdn.zce.me/boilerplate.zip my-project
+```
+
+以上命令将从 `https://cdn.zce.me/boilerplate.zip` 下载并解压模板。
+
+### 离线模式
+
+```shell
+$ caz nm my-project --offline
+```
+
+运行以上命令,CAZ 将尝试从缓存中找到 `nm` 模板,如果找不到该模板的缓存,它仍将自动从 GitHub 下载。
+
+### 命令行参数
+
+CAZ 允许你通过命令行参数来指定提示问题的答案。
+
+```shell
+$ caz minima my-project --name my-proj
+```
+
+运行以上命令,你就不用再回答接下来 `name` 的问题了。
+
+### 调试模式
+
+```shell
+$ caz nm my-project --debug
+```
+
+`--debug` 参数将打开调试模式。
+
+在调试模式下,一旦发生异常,命令行将自动输出异常详细信息。这对于查找模板中的错误非常有帮助。
+
+### 列出可用模版
+
+显示全部可用的模板:
+
+```shell
+$ caz list [owner] [-j|--json] [-s|--short]
+```
+
+#### 参数
+
+- `[owner]`: GitHub 组织或用户的别名, 默认值:`'caz-templates'`
+
+#### 选项
+
+- `-j, --json`: 以 JSON 格式输出
+- `-s, --short`: 以精简格式输出
+
+## 官方模板
+
+目前 CAZ 可用的官方模板有:
+
+- [template](https://github.com/caz-templates/template) - 用来创建 [CAZ](https://github.com/zce/caz) 的模板
+- [nm](https://github.com/caz-templates/nm) - 用来创建 [Node](https://nodejs.org) 模块
+- [vercel](https://github.com/caz-templates/vercel) - 用来创建 [Vercel](https://vercel.com) 应用
+- [react](https://github.com/caz-templates/react) - 用来创建现代化 [React](https://reactjs.org) 应用
+- [vue](https://github.com/caz-templates/vue) - 用来创建现代化 [Vue.js](https://vuejs.org) 应用
+- [vite](https://github.com/caz-templates/vite) - 用来创建基于 [Vite](https://github.com/vitejs/vite) 的 Vue.js 应用
+- [electron](https://github.com/caz-templates/electron) - :construction: 用来创建 [Electron](https://electronjs.org) 应用
+- [mp](https://github.com/caz-templates/mp) - :construction: 用来创建[微信小程序](https://developers.weixin.qq.com/miniprogram/dev/framework)
+- [jekyll](https://github.com/caz-templates/jekyll) - :construction: 用来创建 [Jekyll](https://jekyllrb.com) 站点
+- [x-pages](https://github.com/caz-templates/x-pages) - 用来创建 [X-Pages](https://github.com/zce/x-pages) 静态站点
+
+可能还有更多:https://github.com/caz-templates
+
+> 你也可以通过运行 `$ caz list` 命令来实时列出所有官方模板。
+
+**目前所有模板都托管在 GitHub 上,中国用户可以[使用在 coding.net 上镜像](#中国用户镜像)。**
+
+## 高级
+
+### 配置选项
+
+CAZ 将会读取 `~/.cazrc` 配置文件,默认配置:
+
+```ini
+; 模板下载地址
+; {owner} & {name} & {branch} 最终将被相应的值替换。
+registry = https://github.com/{owner}/{name}/archive/{branch}.zip
+; 模板缺省 owner 的值,可以理解为官方名称
+official = caz-templates
+; 缺省的模板分支名称
+branch = master
+```
+
+这就意味着你可以通过修改配置文件来自定义配置。
+
+例如,你的 `~/.cazrc`:
+
+```ini
+registry = https://gitlab.com/{owner}/{name}/archive/{branch}.zip
+official = faker
+branch = main
+```
+
+然后运行以下命令:
+
+```shell
+$ caz nm my-project
+```
+
+这样就会从 `https://gitlab.com/faker/nm/archive/main.zip` 下载模板。
+
+#### 中国用户镜像
+
+由于网络限制,很多时候下载 GitHub 上的模板都会超时,你可以考虑使用我在 [coding.net](https://coding.net) 上镜像的模板。
+
+`~/.cazrc`:
+
+```ini
+registry = https://zce.coding.net/p/{owner}/d/{name}/git/archive/{branch}
+official = caz
+```
+
+### 创建你的模板
+
+```shell
+$ caz template my-template
+```
+
+以上命令会从 [caz-templates/template](https://github.com/caz-templates/template) 下载模板,并帮你创建你自己的 CAZ 模板。
+
+创建并发布模板,详细可以请参考 [如何创建模板](docs/create-template.zh-CN.md)。
+
+> 也许 fork 一个官方模板是一个更好的决定。
+
+### 创建你的脚手架
+
+```shell
+# 本地安装 caz 模块
+$ npm install caz
+
+# 或者使用 yarn 安装
+$ yarn add caz
+```
+
+以 ESM 和 async/await 的方式使用:
+
+```javascript
+import caz from 'caz'
+
+try {
+ const template = 'nm'
+ // project path (relative cwd or full path)
+ const project = 'my-project'
+ const options = { force: false, offline: false }
+ // scaffolding by caz...
+ await caz(template, project, options)
+ // success created my-project by nm template
+} catch (e) {
+ // error handling
+ console.error(e)
+}
+```
+
+或者使用 CommonJS 和 Promise 的方式:
+
+```javascript
+const { default: caz } = require('caz')
+
+const template = 'nm'
+// project path (relative cwd or full path)
+const project = 'my-project'
+const options = { force: false, offline: false }
+// scaffolding by caz...
+caz(template, project, options)
+ .then(() => {
+ // success created my-project by nm template
+ })
+ .catch(e => {
+ // error handling
+ console.error(e)
+ })
+```
+
+这也就意味着你可以基于 CAZ 模块开发自己的脚手架工具。
+
+创建并发布脚手架工具,详细可以请参考 [如何创建脚手架工具](docs/create-scaffold.zh-CN.md)。
+
+## 参考资料
+
+
+
+### caz(template, project?, options?)
+
+使用指定模板创建一个新项目
+
+#### template
+
+- 类型:`string`
+- 描述:模板名称,也可以是模板文件夹路径
+
+#### project
+
+- 类型:`string`
+- 描述:项目名称,也可以是项目文件夹路径
+- 默认值:`'.'`
+
+#### options
+
+- 类型:`object`
+- 描述:选项参数 & 预设询问结果
+- 默认值:`{}`
+
+##### force
+
+- 类型:`boolean`
+- 描述:如果目标路径已存在就强制覆盖
+- 默认值:`false`
+
+##### offline
+
+- 类型:`boolean`
+- 描述:尝试使用离线模板
+- 默认值:`false`
+
+##### [key: string]
+
+- 类型:`any`
+- 描述:命令行参数覆盖问题答案
+
+## 开发动机
+
+👉 🛠 ⚙
+
+开个玩笑:我就是想造个轮子 ;P
+
+真实的原因是因为我觉得我需要一个更适合我的个人生产力的脚手架工具:简洁、强大、高效。现存的工具因为出发点的不同,都或多或少有一定的局限性。
+
+再无其他
+
+## 概念
+
+### 如何工作
+
+![脚手架工作流程](https://user-images.githubusercontent.com/6166576/88473012-d4ecb180-cf4b-11ea-968a-5508c6f84502.png)
+
+> P.S. 图片来自互联网,但是我没有记住具体来源,这里对愿作者说声抱歉。
+
+#### 主要的工作流程
+
+[核心代码](src/index.ts) 是基于 [zce/mwa](https://github.com/zce/mwa) 项目提供的中间件机制。
+
+以下中间件将按顺序依次执行:
+
+1. [confirm](src/confirm.ts) - 使用 [prompts](https://github.com/terkelg/prompts) 确认目标路径可用。
+2. [resolve](src/resolve.ts) - 从远程或者本地磁盘中找到模板。
+3. [load](src/load.ts) - 自动安装模板依赖项,使用 `require` 加载模板的配置文件。
+4. [inquire](src/inquire.ts) - 使用 [prompts](https://github.com/terkelg/prompts) 询问用户模板所需要的问题。
+5. [setup](src/setup.ts) - 只是调用模板中定义的 `setup` 钩子函数。
+6. [prepare](src/prepare.ts) - 过滤掉不需要的文件,并读取全部将要输出的文件内容。
+7. [rename](src/rename.ts) - 如果文件名中包含插值表达式就重命名文件(替换文件名中的变量)。
+8. [render](src/render.ts) - 如果文件是一个模板文件就渲染文件内容(替换文件内容中的变量)。
+9. [emit](src/emit.ts) - 将每一个文件内容输出写入到目标路径。
+10. [install](src/install.ts) - 如果需要的话,执行 `npm | yarn | pnpm install`。
+11. [init](src/init.ts) - 如果需要的话,执行 `git init && git add && git commit`。
+12. [complete](src/complete.ts) - 只是调用模板中定义的 `complete` 钩子函数。
+
+### 用到什么
+
+- [adm-zip](https://github.com/cthackers/adm-zip) - 一个 JavaScript 实现的 zip 文件压缩解压缩的库,支持内存和磁盘上的压缩解压缩。
+- [cac](https://github.com/cacjs/cac) - 简单而强大的命令行工具框架。
+- [env-paths](https://github.com/sindresorhus/env-paths) - 获取系统存储路径,例如数据、配置、缓存等。
+- [fast-glob](https://github.com/mrmlnc/fast-glob) - 非常快的和非常高效的 glob 库,用于 Node.js
+- [ini](https://github.com/npm/ini) - 一个 Node.js 的 ini 文件解析器。
+- [lodash](https://github.com/lodash/lodash) - Lodash 工具库。
+- [node-fetch](https://github.com/node-fetch/node-fetch) - 一个 Node.js 的 fetch API 的封装。
+- [ora](https://github.com/sindresorhus/ora) - 强大的终端加载动画。
+- [prompts](https://github.com/terkelg/promptss) - 轻量级,美观的和用户友好的提示。
+- [semver](https://github.com/npm/node-semver) - 一个 Node.js 的 semver 库。
+- [validate-npm-package-name](https://github.com/npm/validate-npm-package-name) - 一个 Node.js 的 npm 包名验证器。
+
+## 路线图
+
+以下是我想要实现或者正在开发的功能:
+
+- [ ] config 命令
+- [ ] cache 命令
+- [ ] 全部生命周期钩子
+- [ ] 静默控制台输出 & 彩色控制台输出
+- [ ] 越来越多的官方模板
+
+也可以查看 [打开的 Issues](https://github.com/zce/caz/issues) 中有关建议功能(和已知问题)的列表。
+
+## 参与贡献
+
+1. **Fork** 一个仓库在 GitHub 上!
+2. **Clone** 你复刻的仓库到你本地机器上
+3. **Checkout** 一个特性分支:`git checkout -b my-awesome-feature`
+4. **Commit** 你的修改到你自己的分支上:`git commit -am 'Add some feature'`
+5. **Push** 你的修改到你复刻的仓库中:`git push -u origin my-awesome-feature`
+6. 提交一个 **Pull Request** 让我可以看到你的修改
+
+> **提示**: 请确保尝试合并你的修改之前已经拉取了上游最新的代码。
+
+## 许可证
+
+基于 MIT 协议开源,有关详细信息请查看 [LICENSE](LICENSE) 文件。© [汪磊](https://zce.me)
+
+
diff --git a/bin/caz.js b/bin/caz.js
deleted file mode 100755
index fa67b84f..00000000
--- a/bin/caz.js
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env node
-require('../lib/cli')
diff --git a/docs/create-scaffold.md b/docs/create-scaffold.md
index bba44579..ba10b2f0 100644
--- a/docs/create-scaffold.md
+++ b/docs/create-scaffold.md
@@ -1,5 +1,7 @@
# Writing Custom Scaffolding tools from Scratch
+**English** | [简体中文](create-scaffold.zh-CN.md)
+
In reading this section, you'll learn how to create and distribute your own Scaffolding tools based on CAZ.
TODO: tutorial...
diff --git a/docs/create-scaffold.zh-CN.md b/docs/create-scaffold.zh-CN.md
new file mode 100644
index 00000000..c6d698eb
--- /dev/null
+++ b/docs/create-scaffold.zh-CN.md
@@ -0,0 +1,9 @@
+# 从头开始编写定制脚手架工具
+
+**简体中文** | [English](create-scaffold.md)
+
+通过阅读这部分,你将学习如何基于 CAZ 创建和发布你自己的脚手架工具。
+
+教程尚未完善!
+
+现在,请参考 https://github.com/zce/create-nm 项目。
diff --git a/docs/create-template.md b/docs/create-template.md
index d949854a..857a305f 100644
--- a/docs/create-template.md
+++ b/docs/create-template.md
@@ -1,16 +1,18 @@
# Writing Custom Templates from Scratch
+**English** | [简体中文](create-template.zh-CN.md)
+
In reading this section, you'll learn how to create and distribute your own template.
## Template structure
```
└── my-template
- ├── template ··················· Template source files directory (Required)
+ ├── template ··················· Template source files directory (Required, Can be configured with other names)
│ ├── lib ···················· Any directory (Recurse all subdirectories)
│ │ ├── {name}.js ·········· Any file name with interpolate (Auto rename by answers)
│ │ └── logo.png ··········· Any file without interpolate (Auto skip binary file)
- │ └── package.json ··········· Any file with interpolate (Auto render interpolate by answers)
+ │ └── package.json ··········· Any file contents with interpolate (Auto render interpolate by answers)
├── index.js ··················· Entry point (Optional, Template configuration file)
├── package.json ··············· Package info (Optional)
└── README.md ·················· README (Optional)
@@ -18,12 +20,14 @@ In reading this section, you'll learn how to create and distribute your own temp
## Generate a template from a template
-We built a [template](https://github.com/caz-templates/template) to help users get started with their own template. Feel free to use it to bootstrap your own template once you understand the below concepts.
+We built a [template](https://github.com/caz-templates/template) to help users get started with their own template.
```shell
$ caz template my-template
```
+Feel free to use it to bootstrap your own template once you understand the below concepts.
+
## Configuration
A template repo may have a configuration file for the template which can be either a `index.js` or `main` field defined in `package.json`.
@@ -98,7 +102,7 @@ Upon metadata definition, they can be used in template files as follows:
<%= bio %>
// => 'my template generated'
<%= year %>
-// => 2020 (current year)
+// => 2022 (current year)
```
### prompts
@@ -120,12 +124,11 @@ module.exports = {
The following keys automatically assign initial values (from other config or system info):
-- `name` - destination path basename, fallback: path.basename(dest)
-- `version` - npm init config, fallback: `0.1.0`
+- `name` - destination path basename, fallback: `path.basename(dest)`
+- `version` - npm init config, fallback: `'0.1.0'`
- `author` - npm or git name config
- `email` - npm or git email config
- `url` - npm or git url config
-
The following keys automatically assign default validater:
@@ -355,6 +358,7 @@ Template emit hook, execute after all files emit to the destination.
```javascript
module.exports = {
+ // You can get the following data in context
emit: async ctx => {
const {
template,
@@ -533,40 +537,30 @@ export interface File {
## Dependencies
-Because the template does not automatically install its own dependencies before it works, it is not possible to load third-party modules in the template configuration file at this time.
-
-e.g. template `index.js`:
+Because the template will automatically install its production dependencies before it works, so you can normally use the third-party NPM module in the template configuration file.
-```javascript
-const chalk = require('chalk')
-// => Cannot find module 'chalk'
-```
+e.g.
-The reason why we don't install template dependencies automatically is to ensure that the templates are simple and take less space.
+Install `chalk` as production dependencies:
-To solve this problem, you can also host `node_modules` to template repository, these modules will work properly.
-
-But I don't recommend it because it's extremely inefficient.
-
-I personally prefer to modify `index.js` module paths to sharing the [dependencies of caz](https://npm.im/caz).
+```shell
+$ npm install chalk --save
+```
-e.g. template `index.js`:
+`index.js`:
```javascript
-// Sharing the dependencies of caz
-// Make sure the following statement is executed before all code
-module.paths = require.main.paths
-
const chalk = require('chalk')
-// => require chalk module from caz dependencies
```
+> **NOTE:** Only production dependencies are automatically installed.
+
## Type Annotation
Install `caz` as devDependencies:
```shell
-$ npm i caz --save-dev
+$ npm install caz --save-dev
```
Then in your template configuration file:
diff --git a/docs/create-template.zh-CN.md b/docs/create-template.zh-CN.md
new file mode 100644
index 00000000..f8a0bb5b
--- /dev/null
+++ b/docs/create-template.zh-CN.md
@@ -0,0 +1,583 @@
+# 从头开始编写自定义模板
+
+**简体中文** | [English](create-template.md)
+
+通过阅读这部分,你将学习如何创建和发布你自己的 CAZ 模版。
+
+## 模板结构
+
+```
+└── my-template
+ ├── template ··················· 模板源文件目录(必须的,但可以配置为其他名称)
+ │ ├── lib ···················· 任何文件夹(可以递归包含任意层级的子目录)
+ │ │ ├── {name}.js ·········· 文件名包含插值表达式(自动替换插值重命名)
+ │ │ └── logo.png ··········· 其他文件(二进制文件不经过加工,自动拷贝输出)
+ │ └── package.json ··········· 文件内容包含插值表达式(自动经过模板引擎加工)
+ ├── index.js ··················· 入口文件(可选的,作为模板配置文件)
+ ├── package.json ··············· 模板包信息文件(可选的)
+ └── README.md ·················· 模板自述文件(可选的)
+```
+
+## 使用模板创建一个自定义模板
+
+我们开发了一个 [template](https://github.com/caz-templates/template) 用于帮助用户快速创建自己的自定义模板。
+
+```shell
+$ caz template my-template
+```
+
+一旦你理解了以下的基本概念,就可以使用它来引导你创建自己的模板。
+
+## 配置选项
+
+一个模板仓库可以有一个配置文件,这个配置文件可以是 `index.js` 或者是在 `package.json` 中 `main` 字段定义的文件。
+
+这个文件必须导出一个对象:
+
+```javascript
+module.exports = {
+ // 你的自定义配置...
+}
+```
+
+对象类型:[Template](#template)
+
+这个配置文件可以包含以下字段:
+
+### name
+
+模板名称
+
+- 类型:`string`
+
+```javascript
+module.exports = {
+ name: 'my-template'
+}
+```
+
+### version
+
+模板版本
+
+- 类型:`string`
+
+```javascript
+module.exports = {
+ version: '0.1.0'
+}
+```
+
+### source
+
+模板源文件目录
+
+- 类型:`string`
+- 默认值:`'template'`
+
+```javascript
+module.exports = {
+ source: 'template'
+}
+```
+
+### metadata
+
+可以在模板文件中使用的预设元数据
+
+- 类型:`Record`
+
+```javascript
+module.exports = {
+ metadata: {
+ bio: 'my template generated',
+ year: new Date().getFullYear()
+ }
+}
+```
+
+一旦定义了以上数据,可以在模板文件中使用以下方式:
+
+```ejs
+<%= bio %>
+// => 'my template generated'
+<%= year %>
+// => 2022 (current year)
+```
+
+### prompts
+
+交互式询问,使用 [prompts](https://github.com/terkelg/prompts),请参考 [prompts 文档](https://github.com/terkelg/prompts#-prompt-objects)
+
+- 类型:`PromptObject | PromptObject[]`
+- 默认值:`'{ name: 'name', type: 'text', message: 'Project name' }'`
+
+```javascript
+module.exports = {
+ prompts: [
+ { name: 'name', type: 'text', message: 'Project name' },
+ { name: 'version', type: 'text', message: 'Project version' },
+ { name: 'sass', type: 'confirm', message: 'Use sass preprocessor?', initial: true }
+ ]
+}
+```
+
+使用以下键名将自动分配初始值(来自其他配置或系统信息):
+
+- `name` - 目标文件夹名称,默认值:`path.basename(dest)`
+- `version` - npm 配置的版本号,默认值:`'0.1.0'`
+- `author` - npm 或 git 配置的作者
+- `email` - npm 或 git 配置的邮箱
+- `url` - npm 或 git 配置的网址
+
+使用以下键名将自动指定默认验证器:
+
+- `name` - 使用 [validate-npm-package-name](https://github.com/npm/validate-npm-package-name)
+- `version` - 使用 [semver](https://github.com/npm/node-semver)
+- `email` - 使用正则 `/[^\s]+@[^\s]+\.[^\s]+/`
+- `url` - 使用正则 `/https?:\/\/[^\s]*/`
+
+Upon prompts answers, they can be used in template files as follows:
+经过上述问题询问,最后可以在模板文件中使用以下变量:
+
+```ejs
+<%= name %>
+// => User input text
+
+<%= version %>
+// => User input text
+
+<% if (sass) { %>
+// use sass preprocessor
+<% } %>
+```
+
+### filters
+
+过滤哪些文件你希望被输出
+
+- 类型:`Record boolean>`
+
+```javascript
+module.exports = {
+ prompts: [
+ { name: 'sass', type: 'confirm', message: 'Use sass preprocessor?', initial: true }
+ ],
+ filters: {
+ '*/*.scss': answers => answers.sass,
+ '*/*.css': answers => !answers.sass
+ }
+}
+```
+
+### helpers
+
+自定义模板引擎的助手函数
+
+- 类型:`Record`
+- 默认值:`{ _: require('lodash') }`
+
+```javascript
+module.exports = {
+ helpers: {
+ upper: input => input.toUpperCase()
+ }
+}
+```
+
+一旦注册,你可以在模板中使用以下函数:
+
+```ejs
+<%= upper('zce') %>
+// => 'ZCE'
+
+// lodash is always
+<%= _.camelCase('wow caz') %>
+// => 'wowCaz'
+```
+
+### install
+
+在生成文件完成过后自动安装依赖项
+
+- 类型:`false | 'npm' | 'yarn' | 'pnpm'`
+- 默认值:根据所生成的文件中是否包含 `package.json` 文件决定
+
+```javascript
+module.exports = {
+ // 生成文件后运行 `yarn install`
+ install: 'yarn'
+}
+```
+
+### init
+
+在生成文件完成过后自动初始化仓库
+
+- 类型:`boolean`
+- 默认值:根据所生成的文件中是否包含 `.gitignore` 文件决定
+
+```javascript
+module.exports = {
+ // 生成文件后运行 `git init && git add && git commit`
+ init: true
+}
+```
+
+### setup
+
+模板初始化钩子,在模板加载完成且问题询问完成后执行
+
+- 类型:`(ctx: Context) => Promise`
+- 引用:[Context](#context)
+
+```javascript
+module.exports = {
+ setup: async ctx => {
+ // 此时你可以在上下文中安全的访问以下成员
+ const {
+ template,
+ project,
+ options,
+ dest,
+ src,
+ config,
+ answers // 用户回答
+ } = ctx
+ console.log('template setup', ctx)
+ }
+}
+```
+
+#### 示例:
+
+选择包管理工具
+
+```javascript
+module.exports = {
+ // ...
+ prompts: [
+ {
+ name: 'install',
+ type: 'confirm',
+ message: 'Install dependencies',
+ initial: true
+ },
+ {
+ name: 'pm',
+ type: prev => prev ? 'select' : null,
+ message: 'Package manager',
+ hint: ' ',
+ choices: [
+ { title: 'npm', value: 'npm' },
+ { title: 'yarn', value: 'yarn' }
+ ]
+ }
+ ],
+ setup: async ctx => {
+ // 根据用户选择决定如何安装依赖
+ ctx.config.install = ctx.answers.install && ctx.answers.pm
+ }
+}
+```
+
+动态设置模板源文件目录
+
+```javascript
+module.exports = {
+ // ...
+ prompts: [
+ {
+ name: 'features',
+ type: 'multiselect',
+ message: 'Project features',
+ instructions: false,
+ choices: [
+ { title: 'TypeScript', value: 'typescript', selected: true }
+ // ....
+ ]
+ }
+ ],
+ setup: async ctx => {
+ // 动态设置模板源文件目录
+ ctx.config.source = ctx.answers.features.includes('typescript')
+ ? 'template/typescript'
+ : 'template/javascript'
+ }
+}
+```
+
+其他设置,尽可能发挥你的创造力...
+
+### prepare
+
+Template prepare hook, execute after template files prepare, before rename & render.
+模板准备完成钩子,在模板文件准备完成后、重命名和渲染之前执行
+
+- 类型:`(ctx: Context) => Promise`
+- 引用:[Context](#context)
+
+```javascript
+module.exports = {
+ prepare: async ctx => {
+ // 此时你可以在上下文中安全的访问以下成员
+ const {
+ template,
+ project,
+ options,
+ dest,
+ src,
+ config,
+ answers,
+ files // 尚未重命名和渲染的文件列表
+ } = ctx
+ console.log('template prepare', ctx)
+ }
+}
+```
+
+#### 示例:
+
+动态添加需要生成的文件
+
+```javascript
+module.exports = {
+ prepare: async ctx => {
+ ctx.files.push({
+ path: 'additional.txt',
+ contents: Buffer.from('<%= name %> additional contents')
+ })
+ }
+}
+```
+
+### emit
+
+Template emit hook, execute after all files emit to the destination.
+模板文件生成钩子,在所有文件生成到目标目录后执行
+
+- 类型:`(ctx: Context) => Promise`
+- 引用:[Context](#context)
+
+```javascript
+module.exports = {
+ emit: async ctx => {
+ // 此时你可以在上下文中安全的访问以下成员
+ const {
+ template,
+ project,
+ options,
+ dest,
+ src,
+ config,
+ answers,
+ files // 已经重命名和渲染的文件列表
+ } = ctx
+ console.log('template emit')
+ }
+}
+```
+
+### complete
+
+生成完成回调,如果设置为一个字符串,则将其打印到控制台
+
+- 类型:`string` or `(ctx: Context) => string | Promise`
+- 默认值:打印全部生成的文件列表
+- 引用:[Context](#context)
+
+回掉函数
+
+```javascript
+module.exports = {
+ complete: async ctx => {
+ // ctx => all context
+ console.log(' Happy hacking ;)')
+ }
+}
+```
+
+或者字符串
+
+```javascript
+module.exports = {
+ complete: ' Happy hacking ;)'
+}
+```
+
+_更多示例,可以参考 [fixtures](../test/fixtures/features/index.js)。_
+
+## 核心类型
+
+### Context
+
+```typescript
+/**
+ * Creator context.
+ */
+interface Context {
+ /**
+ * Template name.
+ * e.g.
+ * - offlical short name: `nm`
+ * - offlical short name with branch: `nm#master`
+ * - custom full name: `zce/nm`
+ * - custom full name with branch: `zce/nm#master`
+ * - local directory path: `~/templates/nm`
+ * - full url: `https://github.com/zce/nm/archive/master.zip`
+ */
+ readonly template: string
+ /**
+ * Project name, which is also the project directory.
+ */
+ readonly project: string
+ /**
+ * More options.
+ */
+ readonly options: Options & Record
+ /**
+ * The source directory where the template (absolute).
+ */
+ src: string
+ /**
+ * Generated result output destination directory (absolute).
+ */
+ dest: string
+ /**
+ * Template config.
+ */
+ readonly config: Template
+ /**
+ * Template prompts answers.
+ */
+ readonly answers: Answers
+ /**
+ * Template files.
+ */
+ readonly files: File[]
+}
+```
+
+### Template
+
+```typescript
+/**
+ * Template config.
+ */
+export interface Template {
+ /**
+ * Template name.
+ */
+ name: string
+ /**
+ * Template version.
+ */
+ version?: string
+ /**
+ * Template source dirname.
+ */
+ source?: string
+ /**
+ * Template metadata.
+ */
+ metadata?: Record
+ /**
+ * Template prompts.
+ */
+ prompts?: PromptObject | PromptObject[]
+ /**
+ * Template file filters.
+ */
+ filters?: Record) => boolean>
+ /**
+ * Template engine helpers.
+ */
+ helpers?: Record
+ /**
+ * Auto install dependencies.
+ */
+ install?: false | 'npm' | 'yarn' | 'pnpm'
+ /**
+ * Auto init git repository.
+ */
+ init?: boolean
+ /**
+ * Template setup hook, execute after template loaded & inquire completed.
+ */
+ setup?: (ctx: Context) => Promise
+ /**
+ * Template prepare hook, execute after template files prepare, before rename & render.
+ */
+ prepare?: (ctx: Context) => Promise
+ /**
+ * Template emit hook, execute after all files emit to the destination.
+ */
+ emit?: (ctx: Context) => Promise
+ /**
+ * Template all completed.
+ */
+ complete?: ((ctx: Context) => string | Promise | Promise) | string
+}
+```
+
+### File
+
+```typescript
+/**
+ * File info.
+ */
+export interface File {
+ /**
+ * File full path
+ */
+ path: string
+ /**
+ * File contents (buffer)
+ */
+ contents: Buffer
+}
+```
+
+## 依赖项
+
+因为模板在工作前会自动安装生产依赖,所以你可以在模板配置文件中正常的使用第三方的 NPM 模块。
+
+e.g.
+
+将 `chalk` 模块作为生产依赖安装:
+
+```shell
+$ npm install chalk --save
+```
+
+`index.js`:
+
+```javascript
+const chalk = require('chalk')
+```
+
+> **注意:** 只有生产依赖才会被自动安装。
+
+## 类型注解
+
+将 `caz` 模块作为开发依赖安装:
+
+```shell
+$ npm install caz --save-dev
+```
+
+然后在你的模板配置文件中:
+
+```javascript
+/** @type {import('caz').Template} */
+module.exports = {
+ // 拥有智能提示和类型感知(VSCode)
+}
+```
+
+## 模板转译
+
+如果你想直接输出模板插值表达式字符,你可以这样:
+
+- `<%= '\<%= name %\>' %>` => `<%= name %>`
+- `<%= '${name}' %>` => `${name}`
diff --git a/package.json b/package.json
index a3d64809..fc4f1094 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "caz",
- "version": "0.8.2",
+ "version": "1.0.0",
"description": "A simple yet powerful template-based Scaffolding tools.",
"keywords": [
"productivity",
@@ -28,21 +28,33 @@
"email": "w@zce.me",
"url": "https://zce.me"
},
- "main": "lib/index.js",
- "types": "lib/index.d.ts",
- "bin": "bin/caz.js",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "bin": "dist/cli.js",
"files": [
- "lib",
+ "dist",
"types.d.ts"
],
"scripts": {
+ "build": "tsup",
"lint": "ts-standard",
- "clean": "rimraf lib",
- "compile": "tsc",
- "build": "npm run clean && npm run compile",
"test": "jest --coverage",
"prepare": "husky install .github/husky"
},
+ "tsup": {
+ "entry": [
+ "src/index.ts",
+ "src/cli.ts"
+ ],
+ "clean": true,
+ "minify": true,
+ "splitting": true,
+ "dts": {
+ "resolve": true,
+ "entry": "src/index.ts",
+ "banner": "/// "
+ }
+ },
"commitlint": {
"extends": [
"@commitlint/config-conventional"
@@ -52,10 +64,17 @@
"*.{ts,js}": "ts-standard --fix"
},
"jest": {
- "preset": "ts-jest",
- "testTimeout": 30000,
+ "preset": "ts-jest/presets/js-with-ts",
+ "testTimeout": 20000,
+ "testEnvironment": "node",
"collectCoverageFrom": [
"src/**"
+ ],
+ "moduleNameMapper": {
+ "#(.*)": "/node_modules/$1"
+ },
+ "transformIgnorePatterns": [
+ "node_modules/(?!(node-fetch|fetch-blob|formdata-polyfill|data-uri-to-buffer|env-paths|ora|cli-cursor|restore-cursor|chalk|log-symbols|is-unicode-supported|is-interactive|strip-ansi|ansi-regex))"
]
},
"renovate": {
@@ -63,41 +82,36 @@
"zce"
]
},
- "dependencies": {
- "adm-zip": "0.5.5",
- "cac": "^6.7.3",
- "env-paths": "^2.2.1",
- "fast-glob": "^3.2.7",
- "ini": "^2.0.0",
- "lodash": "^4.17.21",
- "node-fetch": "^2.6.1",
- "ora": "^5.4.1",
- "prompts": "^2.4.1",
- "semver": "^7.3.5",
- "validate-npm-package-name": "^3.0.0"
- },
"devDependencies": {
- "@commitlint/cli": "16.1.0",
- "@commitlint/config-conventional": "16.0.0",
- "@types/adm-zip": "0.4.34",
+ "@commitlint/cli": "16.2.3",
+ "@commitlint/config-conventional": "16.2.1",
"@types/ini": "1.3.31",
- "@types/jest": "27.4.0",
- "@types/lodash": "4.14.178",
- "@types/node": "17.0.10",
- "@types/node-fetch": "2.5.12",
- "@types/prompts": "2.0.14",
+ "@types/jest": "27.4.1",
+ "@types/lodash": "4.14.180",
+ "@types/node": "17.0.23",
"@types/semver": "7.3.9",
"@types/validate-npm-package-name": "3.0.3",
+ "adm-zip": "0.5.9",
+ "cac": "6.7.12",
+ "env-paths": "3.0.0",
+ "fast-glob": "3.2.11",
"husky": "7.0.4",
- "jest": "27.4.7",
- "lint-staged": "12.2.2",
- "rimraf": "3.0.2",
+ "ini": "2.0.0",
+ "jest": "27.5.1",
+ "lint-staged": "12.3.7",
+ "lodash": "4.17.21",
+ "node-fetch": "3.2.3",
+ "ora": "6.1.0",
+ "prompts": "2.4.2",
+ "semver": "7.3.5",
"ts-jest": "27.1.3",
"ts-standard": "11.0.0",
- "typescript": "4.3.5"
+ "tsup": "5.12.1",
+ "typescript": "4.6.2",
+ "validate-npm-package-name": "3.0.0"
},
"engines": {
- "node": ">=12.10"
+ "node": ">=14.14"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
diff --git a/test/cli.spec.ts b/src/cli.spec.ts
similarity index 50%
rename from test/cli.spec.ts
rename to src/cli.spec.ts
index 0b8a518b..854ef6f5 100644
--- a/test/cli.spec.ts
+++ b/src/cli.spec.ts
@@ -1,23 +1,16 @@
-let mockedInit: jest.SpyInstance
-let mockedList: jest.SpyInstance
+const mockedInit = jest.fn().mockImplementation()
+const mockedList = jest.fn().mockImplementation()
-const mockArgv = (args: string[]): () => void => {
+const mockArgv = (...args: string[]): () => void => {
const original = process.argv
- process.argv = [original[0], require.resolve('../bin/caz'), ...args]
- return () => {
- process.argv = original
- }
+ process.argv = original.slice(0, 2).concat(...args)
+ return () => { process.argv = original }
}
beforeEach(async () => {
+ jest.resetAllMocks()
jest.resetModules()
- mockedInit = jest.fn().mockImplementation()
- mockedList = jest.fn().mockImplementation()
- jest.mock('../src', () => ({
- __esModule: true,
- default: mockedInit,
- list: mockedList
- }))
+ jest.mock('.', () => ({ __esModule: true, default: mockedInit, list: mockedList }))
})
afterAll(async () => {
@@ -25,42 +18,31 @@ afterAll(async () => {
})
test('unit:cli:init', async () => {
- const restore = mockArgv(['template', 'project', '--force', '--offline'])
- await import('../src/cli')
+ const restore = mockArgv('template', 'project', '--force', '--offline', '--foo', 'bar')
+ await import('./cli')
expect(mockedInit).toHaveBeenCalled()
expect(mockedInit.mock.calls[0][0]).toBe('template')
expect(mockedInit.mock.calls[0][1]).toBe('project')
- expect(mockedInit.mock.calls[0][2]).toEqual({
- '--': [],
- f: true,
- force: true,
- o: true,
- offline: true
- })
+ expect(mockedInit.mock.calls[0][2]).toEqual({ '--': [], f: true, force: true, o: true, offline: true, foo: 'bar' })
restore()
})
test('unit:cli:list', async () => {
- const restore = mockArgv(['list', 'zce', '--json', '--short'])
- await import('../src/cli')
+ const restore = mockArgv('list', 'zce', '--json', '--short')
+ await import('./cli')
expect(mockedList).toHaveBeenCalled()
expect(mockedList.mock.calls[0][0]).toBe('zce')
- expect(mockedList.mock.calls[0][1]).toEqual({
- '--': [],
- j: true,
- json: true,
- s: true,
- short: true
- })
+ expect(mockedList.mock.calls[0][1]).toEqual({ '--': [], j: true, json: true, s: true, short: true })
restore()
})
test('unit:cli:help', async () => {
- const restore = mockArgv(['--help'])
+ const restore = mockArgv('--help')
const log = jest.spyOn(console, 'log').mockImplementation()
- await import('../src/cli')
+ await import('./cli')
expect(log).toHaveBeenCalledTimes(1)
expect(log.mock.calls[0][0]).toContain('$ caz [project]')
+ log.mockRestore()
restore()
})
@@ -69,8 +51,8 @@ test('unit:cli:help', async () => {
// test('unit:cli:error', async () => {
// const error = jest.spyOn(console, 'error').mockImplementation()
// const exit = jest.spyOn(process, 'exit').mockImplementation()
-// const restore = mockArgv([])
-// await import('../src/cli')
+// const restore = mockArgv()
+// await import('./cli')
// expect(error).toHaveBeenCalled()
// expect(exit).toHaveBeenCalled()
// restore()
diff --git a/src/cli.ts b/src/cli.ts
old mode 100644
new mode 100755
index be049649..54f34a4d
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,5 +1,7 @@
+#!/usr/bin/env node
+
import cac from 'cac'
-import init, { list } from '.'
+import main, { list } from '.'
import { name, version } from '../package.json'
const cli = cac(name)
@@ -14,7 +16,7 @@ cli
.example(` $ ${name} [project]`)
.example(' # with a custom github repo')
.example(` $ ${name} / [project]`)
- .action(init)
+ .action(main)
cli
.command('list [owner]', 'Show all available templates')
diff --git a/test/init/complete.spec.ts b/src/complete.spec.ts
similarity index 64%
rename from test/init/complete.spec.ts
rename to src/complete.spec.ts
index 149fc668..874ec3b1 100644
--- a/test/init/complete.spec.ts
+++ b/src/complete.spec.ts
@@ -1,5 +1,5 @@
-import { createContext } from './util'
-import complete from '../../src/init/complete'
+import { context } from '../test/helpers'
+import complete from './complete'
let log: jest.SpyInstance
@@ -11,12 +11,8 @@ afterEach(async () => {
log.mockRestore()
})
-test('unit:init:complete', async () => {
- expect(typeof complete).toBe('function')
-})
-
-test('unit:init:complete:fallback', async () => {
- const ctx = createContext({
+test('unit:complete:fallback', async () => {
+ const ctx = context({
template: 'fallback',
project: 'fallback-app',
files: [
@@ -33,30 +29,30 @@ test('unit:init:complete:fallback', async () => {
expect(log.mock.calls[4][0]).toBe('\nHappy hacking :)')
})
-test('unit:init:complete:string', async () => {
- const ctx = createContext({}, { complete: 'completed' })
+test('unit:complete:string', async () => {
+ const ctx = context({}, { complete: 'completed' })
await complete(ctx)
expect(log.mock.calls[0][0]).toBe('completed')
})
-test('unit:init:complete:callback', async () => {
+test('unit:complete:callback', async () => {
const callback = jest.fn()
- const ctx = createContext({}, { complete: callback })
+ const ctx = context({}, { complete: callback })
await complete(ctx)
expect(callback.mock.calls[0][0]).toBe(ctx)
})
-test('unit:init:complete:callback-return', async () => {
+test('unit:complete:callback-return', async () => {
const callback = jest.fn().mockReturnValue('completed')
- const ctx = createContext({}, { complete: callback })
+ const ctx = context({}, { complete: callback })
await complete(ctx)
expect(callback).toHaveBeenCalled()
expect(log.mock.calls[0][0]).toBe('completed')
})
-test('unit:init:complete:callback-promise', async () => {
+test('unit:complete:callback-promise', async () => {
const callback = jest.fn().mockReturnValue(Promise.resolve('completed'))
- const ctx = createContext({}, { complete: callback })
+ const ctx = context({}, { complete: callback })
await complete(ctx)
expect(callback).toHaveBeenCalled()
expect(log.mock.calls[0][0]).toBe('completed')
diff --git a/src/init/complete.ts b/src/complete.ts
similarity index 100%
rename from src/init/complete.ts
rename to src/complete.ts
diff --git a/test/init/confirm.spec.ts b/src/confirm.spec.ts
similarity index 60%
rename from test/init/confirm.spec.ts
rename to src/confirm.spec.ts
index 31cba605..a556d633 100644
--- a/test/init/confirm.spec.ts
+++ b/src/confirm.spec.ts
@@ -1,37 +1,32 @@
import fs from 'fs'
import path from 'path'
import prompts from 'prompts'
-import { createContext, createTempDir } from './util'
-import confirm from '../../src/init/confirm'
+import { context, destory, mktmpdir } from '../test/helpers'
+import confirm from './confirm'
-let cwd: string
+const cwd = process.cwd()
beforeAll(async () => {
- cwd = process.cwd()
- process.chdir(await createTempDir())
+ process.chdir(await mktmpdir())
})
afterAll(async () => {
const temp = process.cwd()
process.chdir(cwd)
- await fs.promises.rmdir(temp, { recursive: true })
+ await destory(temp)
})
-test('unit:init:confirm', async () => {
- expect(typeof confirm).toBe('function')
-})
-
-test('unit:init:confirm:not-exists', async () => {
- const ctx = createContext({
+test('unit:confirm:not-exists', async () => {
+ const ctx = context({
project: 'not-exists'
})
await confirm(ctx)
expect(ctx.dest).toBe(path.resolve('not-exists'))
})
-test('unit:init:confirm:force', async () => {
+test('unit:confirm:force', async () => {
await fs.promises.writeFile('force', '')
- const ctx = createContext({
+ const ctx = context({
project: 'force',
options: { force: true }
})
@@ -39,62 +34,62 @@ test('unit:init:confirm:force', async () => {
expect(ctx.dest).toBe(path.resolve('force'))
})
-test('unit:init:confirm:file', async () => {
+test('unit:confirm:file', async () => {
await fs.promises.writeFile('file', '')
- const ctx = createContext({
+ const ctx = context({
project: 'file'
})
expect.hasAssertions()
try {
await confirm(ctx)
} catch (e) {
- expect(e.message).toBe('Cannot create file: File exists.')
+ expect((e as Error).message).toBe('Cannot create file: File exists.')
}
})
-test('unit:init:confirm:empty', async () => {
+test('unit:confirm:empty', async () => {
await fs.promises.mkdir('empty')
- const ctx = createContext({
+ const ctx = context({
project: 'empty'
})
await confirm(ctx)
expect(ctx.dest).toBe(path.resolve('empty'))
})
-test('unit:init:confirm:sure', async () => {
+test('unit:confirm:sure', async () => {
await fs.promises.mkdir('sure')
await fs.promises.writeFile('sure/file', '')
prompts.inject([false])
- const ctx = createContext({
+ const ctx = context({
project: 'sure'
})
expect.hasAssertions()
try {
await confirm(ctx)
} catch (e) {
- expect(e.message).toBe('You have cancelled this task.')
+ expect((e as Error).message).toBe('You have cancelled this task.')
}
})
-test('unit:init:confirm:sure-cwd', async () => {
+test('unit:confirm:sure-cwd', async () => {
prompts.inject([false])
await fs.promises.writeFile('file', '')
- const ctx = createContext({
+ const ctx = context({
project: '.'
})
expect.hasAssertions()
try {
await confirm(ctx)
} catch (e) {
- expect(e.message).toBe('You have cancelled this task.')
+ expect((e as Error).message).toBe('You have cancelled this task.')
}
})
-test('unit:init:confirm:merge', async () => {
+test('unit:confirm:merge', async () => {
await fs.promises.mkdir('merge')
await fs.promises.writeFile('merge/file', '')
prompts.inject([true, 'merge'])
- const ctx = createContext({
+ const ctx = context({
project: 'merge'
})
await confirm(ctx)
@@ -102,11 +97,11 @@ test('unit:init:confirm:merge', async () => {
expect(fs.existsSync('merge/file')).toBe(true)
})
-test('unit:init:confirm:overwrite', async () => {
+test('unit:confirm:overwrite', async () => {
await fs.promises.mkdir('overwrite')
await fs.promises.writeFile('overwrite/file', '')
prompts.inject([true, 'overwrite'])
- const ctx = createContext({
+ const ctx = context({
project: 'overwrite'
})
await confirm(ctx)
@@ -114,18 +109,18 @@ test('unit:init:confirm:overwrite', async () => {
expect(fs.existsSync('overwrite')).toBe(false)
})
-test('unit:init:confirm:cancel', async () => {
+test('unit:confirm:cancel', async () => {
await fs.promises.mkdir('cancel')
await fs.promises.writeFile('cancel/file', '')
prompts.inject([true, 'cancel'])
- const ctx = createContext({
+ const ctx = context({
project: 'cancel'
})
expect.hasAssertions()
try {
await confirm(ctx)
} catch (e) {
- expect(e.message).toBe('You have cancelled this task.')
+ expect((e as Error).message).toBe('You have cancelled this task.')
expect(fs.existsSync('cancel/file')).toBe(true)
}
})
diff --git a/src/init/confirm.ts b/src/confirm.ts
similarity index 98%
rename from src/init/confirm.ts
rename to src/confirm.ts
index ae8687e7..77ca3c95 100644
--- a/src/init/confirm.ts
+++ b/src/confirm.ts
@@ -1,6 +1,6 @@
import path from 'path'
import prompts from 'prompts'
-import { file } from '../core'
+import { file } from './core'
import { Context } from './types'
/**
diff --git a/test/core/config.spec.ts b/src/core/config.spec.ts
similarity index 82%
rename from test/core/config.spec.ts
rename to src/core/config.spec.ts
index 89f3d94e..bcfd88e0 100644
--- a/test/core/config.spec.ts
+++ b/src/core/config.spec.ts
@@ -1,9 +1,9 @@
import os from 'os'
-import path from 'path'
-import config from '../../src/core/config'
+import { fixture } from '../../test/helpers'
+import config from './config'
const mockHomedir = (): jest.SpyInstance => {
- return jest.spyOn(os, 'homedir').mockImplementation(() => path.join(__dirname, '../../test/fixtures'))
+ return jest.spyOn(os, 'homedir').mockImplementation(() => fixture(''))
}
test('unit:core:config', async () => {
@@ -16,7 +16,7 @@ test('unit:core:config', async () => {
test('unit:core:config:custom', async () => {
const homedir = mockHomedir()
jest.resetModules()
- const { default: conf } = await import('../../src/core/config')
+ const { default: conf } = await import('./config')
expect(conf.registry).toBe('https://gitlab.com/{owner}/{name}/archive/refs/heads/{branch}.zip')
expect(conf.official).toBe('faker')
expect(conf.branch).toBe('dev')
@@ -45,7 +45,7 @@ test('unit:core:config:paths', async () => {
})
test('unit:core:config:ini', async () => {
- const result1 = config.ini(path.join(__dirname, '../fixtures/.npmrc'))
+ const result1 = config.ini(fixture('.npmrc'))
expect(result1?.['init-author-name']).toBe('zce')
const result2 = config.ini('fakkkkkkker.ini')
expect(result2).toBe(undefined)
diff --git a/src/core/config.ts b/src/core/config.ts
index 32819e0d..64458755 100644
--- a/src/core/config.ts
+++ b/src/core/config.ts
@@ -36,7 +36,7 @@ export default {
},
get paths () {
// TODO: cache version
- return envPaths(name, { suffix: undefined })
+ return envPaths(name, { suffix: '' })
},
ini: parseIni
}
diff --git a/test/core/exec.spec.ts b/src/core/exec.spec.ts
similarity index 66%
rename from test/core/exec.spec.ts
rename to src/core/exec.spec.ts
index ae24018b..c2f09bac 100644
--- a/test/core/exec.spec.ts
+++ b/src/core/exec.spec.ts
@@ -1,4 +1,4 @@
-import exec from '../../src/core/exec'
+import exec from './exec'
test('unit:core:exec:normal', async () => {
await exec('node', ['--version'], { stdio: 'ignore' })
@@ -8,7 +8,7 @@ test('unit:core:exec:error', async () => {
try {
await exec('zceeczzce', [], {})
} catch (e) {
- expect(e.message).toBe('spawn zceeczzce ENOENT')
+ expect((e as Error).message).toBe('spawn zceeczzce ENOENT')
}
})
@@ -16,6 +16,6 @@ test('unit:core:exec:fail', async () => {
try {
await exec('node', ['-zce'], {})
} catch (e) {
- expect(e.message).toBe('Failed to execute node command.')
+ expect((e as Error).message).toBe('Failed to execute node command.')
}
})
diff --git a/test/core/file.spec.ts b/src/core/file.spec.ts
similarity index 61%
rename from test/core/file.spec.ts
rename to src/core/file.spec.ts
index 96c47275..4c49d7a5 100644
--- a/test/core/file.spec.ts
+++ b/src/core/file.spec.ts
@@ -1,8 +1,8 @@
import os from 'os'
import fs from 'fs'
import path from 'path'
-import { createTempDir } from '../init/util'
-import * as file from '../../src/core/file'
+import { fixture, exists, mktmpdir, destory } from '../../test/helpers'
+import * as file from './file'
test('unit:core:file:exists', async () => {
const result1 = await file.exists(__dirname)
@@ -38,90 +38,88 @@ test('unit:core:file:isEmpty', async () => {
const empty1 = await file.isEmpty(__dirname)
expect(empty1).toBe(false)
- const temp2 = await createTempDir()
+ const temp2 = await mktmpdir()
const empty2 = await file.isEmpty(temp2)
expect(empty2).toBe(true)
- await fs.promises.rmdir(temp2)
+
+ await destory(temp2)
})
test('unit:core:file:mkdir', async () => {
+ const temp = 'test/.temp'
+
// relative (cwd) path recursive
- const root1 = 'test/.temp/1'
- const target1 = `${root1}/${Date.now()}/caz/mkdir/1`
+ const target1 = `${temp}/1/${Date.now()}/caz/mkdir/1`
await file.mkdir(target1)
- expect(fs.existsSync(target1)).toBe(true)
+ expect(await exists(target1)).toBe(true)
// absolute path recursive
- const root2 = await createTempDir()
+ const root2 = await mktmpdir()
const target2 = `${root2}/caz/mkdir/2`
await file.mkdir(target2)
- expect(fs.existsSync(target2)).toBe(true)
-
- // mode options
- const root3 = 'test/.temp/3'
- await file.mkdir(root3, { mode: 0o755, recursive: false })
- const stat3 = await fs.promises.stat(root3)
- expect(stat3.mode).toBe(process.platform === 'win32' ? 16822 : 16877)
-
- // cleanup require node >= v12.10
- await fs.promises.rmdir(root1, { recursive: true })
- await fs.promises.rmdir(root2, { recursive: true })
- await fs.promises.rmdir(root3, { recursive: true })
+ expect(await exists(target2)).toBe(true)
+
+ // mode options (recursive false dependency case 1)
+ const target3 = temp + '/3'
+ await file.mkdir(target3, { mode: 0o755, recursive: false })
+ const stat3 = await fs.promises.stat(target3)
+ if (process.platform !== 'win32') {
+ expect(stat3.mode & 0o777).toBe(0o755)
+ }
+
+ await destory(temp, root2)
})
test('unit:core:file:remove', async () => {
- const temp = await createTempDir()
+ const temp = await mktmpdir()
// remove not exists
const target1 = path.join(temp, 'caz-remove-1')
await file.remove(target1)
- const exists1 = fs.existsSync(target1)
- expect(exists1).toBe(false)
+ expect(await exists(target1)).toBe(false)
// remove a file
const target2 = path.join(temp, 'caz-remove-2')
await fs.promises.writeFile(target2, '')
await file.remove(target2)
- const exists2 = fs.existsSync(target2)
- expect(exists2).toBe(false)
+ expect(await exists(target2)).toBe(false)
// remove a dir
const target3 = path.join(temp, 'caz-remove-3')
await fs.promises.mkdir(target3)
await file.remove(target3)
- const exists3 = fs.existsSync(target3)
- expect(exists3).toBe(false)
+ expect(await exists(target3)).toBe(false)
// remove a dir recursive
const target4 = path.join(temp, 'caz-remove-4')
await fs.promises.mkdir(target4 + '/subdir/foo/bar', { recursive: true })
await file.remove(target4)
- const exists4 = fs.existsSync(target4)
- expect(exists4).toBe(false)
+ expect(await exists(target4)).toBe(false)
- await fs.promises.rmdir(temp)
+ await destory(temp)
})
test('unit:core:file:read', async () => {
- const filename = path.join(path.join(__dirname, '../fixtures/.npmrc'))
+ const filename = path.join(fixture('.npmrc'))
const buffer = await file.read(filename)
const contents = buffer.toString().trim()
expect(contents).toBe('init-author-name = zce')
})
test('unit:core:file:write', async () => {
- const dirname = path.join(__dirname, '../.temp')
- const filename = path.join(dirname, 'temp.txt')
+ const temp = await mktmpdir()
+ const filename = path.join(temp, 'temp.txt')
await file.write(filename, 'hello zce')
const contents = await fs.promises.readFile(filename, 'utf8')
expect(contents).toBe('hello zce')
- await fs.promises.rmdir(dirname, { recursive: true })
+
+ await destory(temp)
})
test('unit:core:file:isBinary', async () => {
- const buffer1 = await fs.promises.readFile(path.join(__dirname, '../fixtures/archive.zip'))
+ const buffer1 = await fs.promises.readFile(fixture('archive.zip'))
expect(file.isBinary(buffer1)).toBe(true)
- const buffer2 = await fs.promises.readFile(path.join(__dirname, '../fixtures/.cazrc'))
+ const buffer2 = await fs.promises.readFile(fixture('.cazrc'))
expect(file.isBinary(buffer2)).toBe(false)
})
@@ -182,45 +180,60 @@ test('unit:core:file:untildify', async () => {
})
test('unit:core:file:extract:zip', async () => {
- const temp = await createTempDir()
+ const temp = await mktmpdir()
- await file.extract(path.join(__dirname, '../fixtures/archive.zip'), temp)
+ await file.extract(fixture('archive.zip'), temp)
- expect(fs.existsSync(path.join(temp, 'archive'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'archive/LICENSE'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'archive/README.md'))).toBe(true)
+ const dir = path.join(temp, 'archive')
+ expect(await exists(dir)).toBe(true)
- await fs.promises.rmdir(temp, { recursive: true })
+ const file1 = path.join(dir, 'LICENSE')
+ expect(await exists(file1)).toBe(true)
+ const stat1 = await fs.promises.stat(file1)
+ if (process.platform !== 'win32') {
+ expect(stat1.mode & 0o777).toBe(0o644)
+ }
+
+ const file2 = path.join(dir, 'README.md')
+ expect(await exists(file2)).toBe(true)
+ const stat2 = await fs.promises.stat(file2)
+ if (process.platform !== 'win32') {
+ expect(stat2.mode & 0o777).toBe(0o755)
+ }
+
+ await destory(temp)
})
test('unit:core:file:extract:error', async () => {
- const temp = await createTempDir()
+ const temp = await mktmpdir()
expect.hasAssertions()
try {
- await file.extract(path.join(__dirname, '../fixtures/error.zip'), temp)
+ await file.extract(fixture('error.zip'), temp)
} catch (e) {
- expect(e.message).toBe('Invalid or unsupported zip format. No END header found')
+ expect((e as Error).message).toBe('Invalid or unsupported zip format. No END header found')
}
+
+ await destory(temp)
})
test('unit:core:file:extract:strip', async () => {
- const temp = await createTempDir()
+ const temp = await mktmpdir()
- await file.extract(path.join(__dirname, '../fixtures/archive.zip'), temp, 1)
+ await file.extract(fixture('archive.zip'), temp, 1)
- expect(fs.existsSync(path.join(temp, 'LICENSE'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'README.md'))).toBe(true)
+ expect(await exists(path.join(temp, 'LICENSE'))).toBe(true)
+ expect(await exists(path.join(temp, 'README.md'))).toBe(true)
- await fs.promises.rmdir(temp, { recursive: true })
+ await destory(temp)
})
test('unit:core:file:extract:strip-max', async () => {
- const temp = await createTempDir()
+ const temp = await mktmpdir()
- await file.extract(path.join(__dirname, '../fixtures/archive.zip'), temp, 10)
+ await file.extract(fixture('archive.zip'), temp, 10)
- expect(fs.existsSync(path.join(temp, 'LICENSE'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'README.md'))).toBe(true)
+ expect(await exists(path.join(temp, 'LICENSE'))).toBe(true)
+ expect(await exists(path.join(temp, 'README.md'))).toBe(true)
- await fs.promises.rmdir(temp, { recursive: true })
+ await destory(temp)
})
diff --git a/src/core/file.ts b/src/core/file.ts
index c4c7246a..cb23e181 100644
--- a/src/core/file.ts
+++ b/src/core/file.ts
@@ -20,7 +20,7 @@ export const exists = async (input: string): Promise= v12.10
+ * require node >= v14.14.0
* @param input input path
+ * @param options recursive & force by default
* @todo https://github.com/sindresorhus/trash
*/
-export const remove = async (input: string, options?: fs.RmDirOptions): Promise => {
- const result = await exists(input)
- if (result === false) return
-
- // file or other
- if (result !== 'dir') return await fs.promises.unlink(input)
-
- // dir
- await fs.promises.rmdir(input, { recursive: true, ...options })
+export const remove = async (input: string, options?: fs.RmOptions): Promise => {
+ await fs.promises.rm(input, { recursive: true, force: true, ...options })
}
/**
@@ -158,7 +152,10 @@ export const extract = async (input: string, output: string, strip = 0): Promise
entry.entryName = stripped === '' ? entry.entryName : stripped
})
- zip.extractAllToAsync(output, true, err => {
+ // https://github.com/cthackers/adm-zip/issues/389
+ // https://github.com/cthackers/adm-zip/issues/407#issuecomment-990086783
+ // keep original file permissions
+ zip.extractAllToAsync(output, true, true, err => {
/* istanbul ignore if */
if (err != null) throw err
resolve()
diff --git a/test/core/http.spec.ts b/src/core/http.spec.ts
similarity index 74%
rename from test/core/http.spec.ts
rename to src/core/http.spec.ts
index a4561db7..faa6bc9f 100644
--- a/test/core/http.spec.ts
+++ b/src/core/http.spec.ts
@@ -1,19 +1,15 @@
import fs from 'fs'
-import * as http from '../../src/core/http'
+import * as http from './http'
+import { destory } from '../../test/helpers'
const registry = 'https://registry.npmjs.org'
const tarball = `${registry}/caz/-/caz-0.0.0.tgz`
-// // npm.taobao.org mirror
-// const registry = 'https://registry.npm.taobao.org'
-// const tarball = `${registry}/caz/download/caz-0.0.0.tgz`
-
test('unit:core:http:request', async () => {
const response = await http.request(registry)
expect(response.ok).toBe(true)
const data = await response.json() as Record
-
expect(data).toBeTruthy()
expect(data.db_name).toBe('registry')
})
@@ -23,7 +19,7 @@ test('unit:core:http:request:error', async () => {
try {
await http.request(`${registry}/faaaaaaaaaker-${Date.now()}`)
} catch (e) {
- expect(e.message).toBe('Unexpected response: Not Found')
+ expect((e as Error).message).toBe('Unexpected response: Not Found')
}
})
@@ -32,14 +28,14 @@ test('unit:core:http:download', async () => {
const stats = await fs.promises.stat(filename)
expect(stats.isFile()).toBe(true)
expect(stats.size).toBe(367)
- await fs.promises.unlink(filename)
+ await destory(filename)
})
test('unit:core:http:download:text', async () => {
const filename = await http.download(registry)
const stats = await fs.promises.stat(filename)
expect(stats.isFile()).toBe(true)
- await fs.promises.unlink(filename)
+ await destory(filename)
})
test('unit:core:http:download:error', async () => {
@@ -47,6 +43,6 @@ test('unit:core:http:download:error', async () => {
try {
await http.download(`${registry}/faaaaaaaaaker-${Date.now()}.tgz`)
} catch (e) {
- expect(e.message).toBe('Unexpected response: Not Found')
+ expect((e as Error).message).toBe('Unexpected response: Not Found')
}
})
diff --git a/src/core/http.ts b/src/core/http.ts
index 4fb86c2b..e5298b8a 100644
--- a/src/core/http.ts
+++ b/src/core/http.ts
@@ -26,6 +26,8 @@ export const request = async (url: RequestInfo, init?: RequestInit): Promise => {
const response = await request(url)
+ /* istanbul ignore if */
+ if (response.body == null) throw Error('Unexpected response: Response body is empty')
// ensure temp dirname
await fs.mkdir(config.paths.temp, { recursive: true })
const filename = join(config.paths.temp, Date.now().toString() + '.tmp')
diff --git a/test/core/index.spec.ts b/src/core/index.spec.ts
similarity index 74%
rename from test/core/index.spec.ts
rename to src/core/index.spec.ts
index df8dbe4f..d959d964 100644
--- a/test/core/index.spec.ts
+++ b/src/core/index.spec.ts
@@ -1,8 +1,9 @@
-import * as core from '../../src/core'
+import * as core from '.'
test('unit:core', async () => {
expect(typeof core.file).toBe('object')
expect(typeof core.http).toBe('object')
expect(typeof core.config).toBe('object')
+ expect(typeof core.exec).toBe('function')
expect(typeof core.Ware).toBe('function')
})
diff --git a/src/core/index.ts b/src/core/index.ts
index 9e78238e..167ad112 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,5 +1,5 @@
export * as file from './file'
export * as http from './http'
-export { default as exec } from './exec'
export { default as config } from './config'
+export { default as exec } from './exec'
export { Ware, Middleware } from './ware'
diff --git a/test/core/ware.spec.ts b/src/core/ware.spec.ts
similarity index 86%
rename from test/core/ware.spec.ts
rename to src/core/ware.spec.ts
index e4864aba..8afb62d4 100644
--- a/test/core/ware.spec.ts
+++ b/src/core/ware.spec.ts
@@ -1,4 +1,4 @@
-import { Ware } from '../../src/core/ware'
+import { Ware } from './ware'
test('unit:core:ware', async () => {
type State = Record
@@ -27,7 +27,7 @@ test('unit:core:ware', async () => {
try {
await app.run({ a: 1 })
} catch (e) {
- expect(e.message).toEqual('break')
+ expect((e as Error).message).toEqual('break')
}
expect(order).toEqual([1, 2])
diff --git a/test/init/emit.spec.ts b/src/emit.spec.ts
similarity index 55%
rename from test/init/emit.spec.ts
rename to src/emit.spec.ts
index c4730ddf..98ad98b9 100644
--- a/test/init/emit.spec.ts
+++ b/src/emit.spec.ts
@@ -1,15 +1,11 @@
import fs from 'fs'
import path from 'path'
-import { createContext, createTempDir } from './util'
-import emit from '../../src/init/emit'
+import { context, destory, mktmpdir } from '../test/helpers'
+import emit from './emit'
-test('unit:init:emit', async () => {
- expect(typeof emit).toBe('function')
-})
-
-test('unit:init:emit:normal', async () => {
- const temp = await createTempDir()
- const ctx = createContext({
+test('unit:emit:normal', async () => {
+ const temp = await mktmpdir()
+ const ctx = context({
dest: temp,
files: [
{ path: 'hello.txt', contents: Buffer.from('hello') },
@@ -21,12 +17,12 @@ test('unit:init:emit:normal', async () => {
expect(hello).toBe('hello')
const bar = await fs.promises.readFile(path.join(temp, 'foo/bar.txt'), 'utf8')
expect(bar).toBe('bar')
- await fs.promises.rmdir(temp, { recursive: true })
+ await destory(temp)
})
-test('unit:init:emit:hook', async () => {
+test('unit:emit:hook', async () => {
const callback = jest.fn()
- const ctx = createContext({}, { emit: callback })
+ const ctx = context({}, { emit: callback })
await emit(ctx)
expect(callback.mock.calls[0][0]).toBe(ctx)
})
diff --git a/src/init/emit.ts b/src/emit.ts
similarity index 93%
rename from src/init/emit.ts
rename to src/emit.ts
index 3f674272..d5b7e9e3 100644
--- a/src/init/emit.ts
+++ b/src/emit.ts
@@ -1,5 +1,5 @@
import path from 'path'
-import { file } from '../core'
+import { file } from './core'
import { Context } from './types'
/**
diff --git a/src/index.spec.ts b/src/index.spec.ts
new file mode 100644
index 00000000..0c96a0fb
--- /dev/null
+++ b/src/index.spec.ts
@@ -0,0 +1,56 @@
+import fs from 'fs'
+import path from 'path'
+import prompts from 'prompts'
+import { http } from './core'
+import { destory, exists, fixture, mktmpdir } from '../test/helpers'
+import * as caz from '.'
+
+test('unit:exports', async () => {
+ expect(typeof caz.inject).toBe('function')
+ expect(typeof caz.file).toBe('object')
+ expect(typeof caz.http).toBe('object')
+ expect(typeof caz.config).toBe('object')
+ expect(typeof caz.exec).toBe('function')
+ expect(typeof caz.Ware).toBe('function')
+ expect(typeof caz.list).toBe('function')
+ expect(typeof caz.default).toBe('function')
+})
+
+test('unit:default', async () => {
+ const log = jest.spyOn(console, 'log').mockImplementation()
+ const clear = jest.spyOn(console, 'clear').mockImplementation()
+ const downloadtmpdir = await mktmpdir()
+ const download = jest.spyOn(http, 'download').mockImplementation(async () => {
+ const file = fixture('minima.zip')
+ const target = path.join(downloadtmpdir, 'minima.zip')
+ await fs.promises.copyFile(file, target)
+ return target
+ })
+ const temp = await mktmpdir()
+ const original = process.cwd()
+ process.chdir(temp)
+ prompts.inject(['caz'])
+ await caz.default('minima', 'minima-app', { force: true, offline: false })
+ expect(await exists('minima-app')).toBe(true)
+ const contents = await fs.promises.readFile('minima-app/caz.txt', 'utf8')
+ expect(contents.trim()).toBe('hey caz.')
+ process.chdir(original)
+ log.mockRestore()
+ clear.mockRestore()
+ download.mockRestore()
+ await destory(temp, downloadtmpdir)
+})
+
+test('unit:error', async () => {
+ expect.assertions(2)
+ try {
+ await caz.default(null as unknown as string)
+ } catch (e) {
+ expect((e as Error).message).toBe('Missing required argument: `template`.')
+ }
+ try {
+ await caz.default('')
+ } catch (e) {
+ expect((e as Error).message).toBe('Missing required argument: `template`.')
+ }
+})
diff --git a/src/index.ts b/src/index.ts
index 5b471dc5..cfa50151 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,58 @@
-export { inject } from 'prompts'
-export { file, http, config, Ware, Middleware } from './core'
-export { default as list, ListOptions } from './list'
-export { default, Options, Context, Template } from './init'
+import { inject } from 'prompts'
+
+import list, { ListOptions } from './list'
+
+import { file, http, config, exec, Ware, Middleware } from './core'
+import { Options, Context, Template } from './types'
+
+import confirm from './confirm'
+import resolve from './resolve'
+import load from './load'
+import inquire from './inquire'
+import setup from './setup'
+import prepare from './prepare'
+import rename from './rename'
+import render from './render'
+import emit from './emit'
+import install from './install'
+import init from './init'
+import complete from './complete'
+
+const creator = new Ware()
+
+creator.use(confirm)
+creator.use(resolve)
+creator.use(load)
+creator.use(inquire)
+creator.use(setup)
+creator.use(prepare)
+creator.use(rename)
+creator.use(render)
+creator.use(emit)
+creator.use(install)
+creator.use(init)
+creator.use(complete)
+
+export default async (template: string, project: string = '.', options: Options = {}): Promise => {
+ // required arguments
+ if (template == null || template === '') {
+ throw new Error('Missing required argument: `template`.')
+ }
+
+ // create context
+ const context = {
+ template,
+ project,
+ options,
+ src: '',
+ dest: '',
+ config: Object.create(null),
+ answers: Object.create(null),
+ files: []
+ }
+
+ // running creator
+ await creator.run(context)
+}
+
+export { inject, file, http, config, exec, Ware, list, Middleware, Options, Context, Template, ListOptions }
diff --git a/src/init.spec.ts b/src/init.spec.ts
new file mode 100644
index 00000000..8824deb1
--- /dev/null
+++ b/src/init.spec.ts
@@ -0,0 +1,59 @@
+import fs from 'fs'
+import path from 'path'
+import { context, destory, exists, mktmpdir } from '../test/helpers'
+import init from './init'
+
+test('unit:init:null', async () => {
+ const ctx = context()
+ const result = await init(ctx)
+ expect(result).toBe(undefined)
+})
+
+test('unit:init:false', async () => {
+ const ctx = context({}, { init: false })
+ const result = await init(ctx)
+ expect(result).toBe(undefined)
+})
+
+test('unit:init:default', async () => {
+ const temp = await mktmpdir()
+ await fs.promises.writeFile(path.join(temp, 'caz.txt'), 'hello')
+ const ctx = context({
+ dest: temp,
+ files: [{ path: '.gitignore', contents: Buffer.from('') }]
+ })
+ await init(ctx)
+ expect(await exists(path.join(temp, '.git'))).toBe(true)
+ const stats = await fs.promises.stat(path.join(temp, '.git'))
+ expect(stats.isDirectory()).toBe(true)
+ expect(await exists(path.join(temp, '.git', 'COMMIT_EDITMSG'))).toBe(true)
+ const msg = await fs.promises.readFile(path.join(temp, '.git', 'COMMIT_EDITMSG'), 'utf8')
+ expect(msg.trim()).toBe('feat: initial commit')
+ await destory(temp)
+})
+
+test('unit:init:manual', async () => {
+ const temp = await mktmpdir()
+ await fs.promises.writeFile(path.join(temp, 'caz.txt'), 'hello')
+ const ctx = context({ dest: temp }, { init: true })
+ await init(ctx)
+ expect(await exists(path.join(temp, '.git'))).toBe(true)
+ const stats = await fs.promises.stat(path.join(temp, '.git'))
+ expect(stats.isDirectory()).toBe(true)
+ expect(await exists(path.join(temp, '.git', 'COMMIT_EDITMSG'))).toBe(true)
+ const msg = await fs.promises.readFile(path.join(temp, '.git', 'COMMIT_EDITMSG'), 'utf8')
+ expect(msg.trim()).toBe('feat: initial commit')
+ await destory(temp)
+})
+
+test('unit:init:error', async () => {
+ const temp = await mktmpdir()
+ const ctx = context({ dest: temp }, { init: true })
+ expect.hasAssertions()
+ try {
+ await init(ctx)
+ } catch (e) {
+ expect((e as Error).message).toBe('Initial repository failed.')
+ }
+ await destory(temp)
+})
diff --git a/src/init/init.ts b/src/init.ts
similarity index 94%
rename from src/init/init.ts
rename to src/init.ts
index 39a0549c..d1485109 100644
--- a/src/init/init.ts
+++ b/src/init.ts
@@ -1,4 +1,4 @@
-import { exec, config } from '../core'
+import { exec, config } from './core'
import { Context } from './types'
/**
diff --git a/src/init/index.ts b/src/init/index.ts
deleted file mode 100644
index 3e99bf55..00000000
--- a/src/init/index.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Ware } from '../core'
-import { Options, Context, Template } from './types'
-
-import confirm from './confirm'
-import resolve from './resolve'
-import load from './load'
-import inquire from './inquire'
-import setup from './setup'
-import prepare from './prepare'
-import rename from './rename'
-import render from './render'
-import emit from './emit'
-import install from './install'
-import init from './init'
-import complete from './complete'
-
-const creator = new Ware()
-
-creator.use(confirm)
-creator.use(resolve)
-creator.use(load)
-creator.use(inquire)
-creator.use(setup)
-creator.use(prepare)
-creator.use(rename)
-creator.use(render)
-creator.use(emit)
-creator.use(install)
-creator.use(init)
-creator.use(complete)
-
-export default async (template: string, project: string = '.', options: Options = {}): Promise => {
- // required arguments
- if (template == null || template === '') {
- throw new Error('Missing required argument: `template`.')
- }
-
- // create context
- const context = {
- template,
- project,
- options,
- src: '',
- dest: '',
- config: Object.create(null),
- answers: Object.create(null),
- files: []
- }
-
- // running creator
- await creator.run(context)
-}
-
-export { Options, Context, Template }
diff --git a/test/init/inquire.spec.ts b/src/inquire.spec.ts
similarity index 87%
rename from test/init/inquire.spec.ts
rename to src/inquire.spec.ts
index 5cf29b39..1dcca78a 100644
--- a/test/init/inquire.spec.ts
+++ b/src/inquire.spec.ts
@@ -1,14 +1,10 @@
import path from 'path'
import prompts, { PromptObject } from 'prompts'
-import { config } from '../../src'
-import { createContext } from './util'
-import inquire, { validater, processor } from '../../src/init/inquire'
+import { config } from './core'
+import { context } from '../test/helpers'
+import inquire, { validater, processor } from './inquire'
-test('unit:init:inquire', async () => {
- expect(typeof inquire).toBe('function')
-})
-
-test('unit:init:inquire:validater:name', async () => {
+test('unit:inquire:validater:name', async () => {
expect(validater.name('foo')).toBe(true)
expect(validater.name('foo-bar')).toBe(true)
expect(validater.name('foo_bar')).toBe(true)
@@ -18,27 +14,27 @@ test('unit:init:inquire:validater:name', async () => {
expect(validater.name('Caz')).toBe('name can no longer contain capital letters')
})
-test('unit:init:inquire:validater:version', async () => {
+test('unit:inquire:validater:version', async () => {
expect(validater.version('0.1.0')).toBe(true)
expect(validater.version('0.1')).toBe('The `0.1` is not a semantic version.')
expect(validater.version('0')).toBe('The `0` is not a semantic version.')
})
-test('unit:init:inquire:validater:email', async () => {
+test('unit:inquire:validater:email', async () => {
expect(validater.email('w@zce.me')).toBe(true)
expect(validater.email('foo')).toBe('The `foo` is not a email address.')
expect(validater.email('w@zce')).toBe('The `w@zce` is not a email address.')
})
-test('unit:init:inquire:validater:url', async () => {
+test('unit:inquire:validater:url', async () => {
expect(validater.url('http://zce.me')).toBe(true)
expect(validater.url('https://zce.me')).toBe(true)
expect(validater.url('foo')).toBe('The `foo` is not a url address.')
expect(validater.url('ftp://zce.me')).toBe('The `ftp://zce.me` is not a url address.')
})
-test('unit:init:inquire:processor:name', async () => {
- const ctx = createContext({ dest: __dirname })
+test('unit:inquire:processor:name', async () => {
+ const ctx = context({ dest: __dirname })
const fn = processor(ctx)
const prompt1: PromptObject = { name: 'name', type: 'text' }
@@ -57,8 +53,8 @@ test('unit:init:inquire:processor:name', async () => {
expect(prompt3.validate).toBe(validate3)
})
-test('unit:init:inquire:processor:version', async () => {
- const ctx = createContext({})
+test('unit:inquire:processor:version', async () => {
+ const ctx = context({})
const fn = processor(ctx)
const prompt0: PromptObject = { name: 'version', type: 'text', initial: '3.2.1' }
@@ -67,6 +63,7 @@ test('unit:init:inquire:processor:version', async () => {
expect(prompt0.initial).toBe('3.2.1')
const mockConfig = jest.spyOn(config, 'npm', 'get').mockReturnValue({ 'init-version': '1.2.3' })
+
const prompt1: PromptObject = { name: 'version', type: 'text' }
fn(prompt1)
expect(prompt1.initial).toBe('1.2.3')
@@ -89,8 +86,8 @@ test('unit:init:inquire:processor:version', async () => {
mockConfig.mockRestore()
})
-test('unit:init:inquire:processor:author', async () => {
- const ctx = createContext({})
+test('unit:inquire:processor:author', async () => {
+ const ctx = context({})
const fn = processor(ctx)
const prompt0: PromptObject = { name: 'author', type: 'text', initial: 'zce' }
@@ -98,6 +95,7 @@ test('unit:init:inquire:processor:author', async () => {
expect(prompt0.initial).toBe('zce')
const mockConfig = jest.spyOn(config, 'npm', 'get').mockReturnValue({ 'init-author-name': 'faker-npm' })
+
const prompt1: PromptObject = { name: 'author', type: 'text' }
fn(prompt1)
expect(prompt1.initial).toBe('faker-npm')
@@ -127,8 +125,8 @@ test('unit:init:inquire:processor:author', async () => {
mockConfig.mockRestore()
})
-test('unit:init:inquire:processor:email', async () => {
- const ctx = createContext({})
+test('unit:inquire:processor:email', async () => {
+ const ctx = context({})
const fn = processor(ctx)
const prompt0: PromptObject = { name: 'email', type: 'text', initial: 'w@zce.me' }
@@ -136,6 +134,7 @@ test('unit:init:inquire:processor:email', async () => {
expect(prompt0.initial).toBe('w@zce.me')
const mockConfig = jest.spyOn(config, 'npm', 'get').mockReturnValue({ 'init-author-email': 'npm@faker.com' })
+
const prompt1: PromptObject = { name: 'email', type: 'text' }
fn(prompt1)
expect(prompt1.validate).toBe(validater.email)
@@ -171,8 +170,8 @@ test('unit:init:inquire:processor:email', async () => {
mockConfig.mockRestore()
})
-test('unit:init:inquire:processor:url', async () => {
- const ctx = createContext({})
+test('unit:inquire:processor:url', async () => {
+ const ctx = context({})
const fn = processor(ctx)
const prompt0: PromptObject = { name: 'url', type: 'text', initial: 'https://zce.me' }
@@ -180,6 +179,7 @@ test('unit:init:inquire:processor:url', async () => {
expect(prompt0.initial).toBe('https://zce.me')
const mockConfig = jest.spyOn(config, 'npm', 'get').mockReturnValue({ 'init-author-url': 'https://npm.faker.com' })
+
const prompt1: PromptObject = { name: 'url', type: 'text' }
fn(prompt1)
expect(prompt1.validate).toBe(validater.url)
@@ -215,9 +215,9 @@ test('unit:init:inquire:processor:url', async () => {
mockConfig.mockRestore()
})
-test('unit:init:inquire:default', async () => {
+test('unit:inquire:default', async () => {
const clear = jest.spyOn(console, 'clear').mockImplementation()
- const ctx = createContext({ dest: __dirname })
+ const ctx = context({ dest: __dirname })
prompts.inject(['foo'])
await inquire(ctx)
expect(clear).toBeCalledTimes(1)
@@ -226,9 +226,9 @@ test('unit:init:inquire:default', async () => {
clear.mockRestore()
})
-test('unit:init:inquire:custom', async () => {
+test('unit:inquire:custom', async () => {
const clear = jest.spyOn(console, 'clear').mockImplementation()
- const ctx = createContext({}, {
+ const ctx = context({}, {
prompts: [
{ name: 'foo', type: 'text', message: 'foo' },
{ name: 'bar', type: 'text', message: 'bar' }
@@ -241,9 +241,9 @@ test('unit:init:inquire:custom', async () => {
clear.mockRestore()
})
-test('unit:init:inquire:override', async () => {
+test('unit:inquire:override', async () => {
const clear = jest.spyOn(console, 'clear').mockImplementation()
- const ctx = createContext({}, {
+ const ctx = context({}, {
prompts: [
{ name: 'foo', type: 'text', message: 'foo' },
{ name: 'bar', type: 'text', message: 'bar' }
diff --git a/src/init/inquire.ts b/src/inquire.ts
similarity index 98%
rename from src/init/inquire.ts
rename to src/inquire.ts
index 11805a66..d84e558b 100644
--- a/src/init/inquire.ts
+++ b/src/inquire.ts
@@ -2,7 +2,7 @@ import path from 'path'
import semver from 'semver'
import prompts, { PromptObject } from 'prompts'
import validateName from 'validate-npm-package-name'
-import { config } from '../core'
+import { config } from './core'
import { Context } from './types'
/**
diff --git a/src/install.spec.ts b/src/install.spec.ts
new file mode 100644
index 00000000..2fad603e
--- /dev/null
+++ b/src/install.spec.ts
@@ -0,0 +1,73 @@
+import fs from 'fs'
+import path from 'path'
+import { context, destory, exists, mktmpdir } from '../test/helpers'
+import install from './install'
+
+test('unit:install:false', async () => {
+ const ctx = context({}, { install: false })
+ const result = await install(ctx)
+ expect(result).toBe(undefined)
+})
+
+test('unit:install:null', async () => {
+ const ctx = context()
+ const result = await install(ctx)
+ expect(result).toBe(undefined)
+})
+
+test('unit:install:default', async () => {
+ const temp = await mktmpdir()
+ const pkg = { dependencies: { caz: '0.0.0' } }
+ await fs.promises.writeFile(path.join(temp, 'package.json'), JSON.stringify(pkg))
+ const ctx = context({
+ dest: temp,
+ files: [{ path: 'package.json', contents: Buffer.from('') }]
+ })
+ await install(ctx)
+ expect(ctx.config.install).toBe('npm')
+ expect(await exists(path.join(temp, 'node_modules'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules', 'caz'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules', 'caz', 'package.json'))).toBe(true)
+ await destory(temp)
+})
+
+// required yarn env
+test('unit:install:manual:yarn', async () => {
+ const temp = await mktmpdir()
+ const pkg = { dependencies: { caz: '0.0.0' } }
+ await fs.promises.writeFile(path.join(temp, 'package.json'), JSON.stringify(pkg))
+ const ctx = context({ dest: temp }, { install: 'yarn' })
+ await install(ctx)
+ expect(await exists(path.join(temp, 'yarn.lock'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules', 'caz'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules', 'caz', 'package.json'))).toBe(true)
+ await destory(temp)
+})
+
+// required pnpm env
+test('unit:install:manual:pnpm', async () => {
+ const temp = await mktmpdir()
+ const pkg = { dependencies: { caz: '0.0.0' } }
+ await fs.promises.writeFile(path.join(temp, 'package.json'), JSON.stringify(pkg))
+ const ctx = context({ dest: temp }, { install: 'pnpm' })
+ await install(ctx)
+ expect(await exists(path.join(temp, 'pnpm-lock.yaml'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules', 'caz'))).toBe(true)
+ expect(await exists(path.join(temp, 'node_modules', 'caz', 'package.json'))).toBe(true)
+ await destory(temp)
+})
+
+test('unit:install:manual:error', async () => {
+ const temp = await mktmpdir()
+ await fs.promises.writeFile(path.join(temp, 'package.json'), 'error package.json')
+ const ctx = context({ dest: temp }, { install: 'npm' })
+ expect.hasAssertions()
+ try {
+ await install(ctx)
+ } catch (e) {
+ expect((e as Error).message).toBe('Install dependencies failed.')
+ }
+ await destory(temp)
+})
diff --git a/src/init/install.ts b/src/install.ts
similarity index 96%
rename from src/init/install.ts
rename to src/install.ts
index a1f8afa9..0d3bdf30 100644
--- a/src/init/install.ts
+++ b/src/install.ts
@@ -1,4 +1,4 @@
-import { exec } from '../core'
+import { exec } from './core'
import { Context } from './types'
/**
diff --git a/test/list/fetch.spec.ts b/src/list/fetch.spec.ts
similarity index 63%
rename from test/list/fetch.spec.ts
rename to src/list/fetch.spec.ts
index 0a47b947..60c3f2cb 100644
--- a/test/list/fetch.spec.ts
+++ b/src/list/fetch.spec.ts
@@ -1,8 +1,4 @@
-import fetch from '../../src/list/fetch'
-
-test('unit:fetch', async () => {
- expect(typeof fetch).toBe('function')
-})
+import fetch from './fetch'
test('unit:fetch:empty', async () => {
const results = await fetch('ghost')
@@ -19,6 +15,6 @@ test('unit:fetch:error', async () => {
try {
await fetch('fakkkkkkkkkkker')
} catch (e) {
- expect(e.message).toBe('Failed to fetch list from remote: Unexpected response: Not Found.')
+ expect((e as Error).message).toBe('Failed to fetch list from remote: Unexpected response: Not Found.')
}
})
diff --git a/src/list/fetch.ts b/src/list/fetch.ts
index 7fa9aa85..5f4eb298 100644
--- a/src/list/fetch.ts
+++ b/src/list/fetch.ts
@@ -24,6 +24,6 @@ export default async (owner: string): Promise => {
return results
} catch (e) {
spinner.stop()
- throw new Error(`Failed to fetch list from remote: ${e.message as string}.`)
+ throw new Error(`Failed to fetch list from remote: ${(e as Error).message}.`)
}
}
diff --git a/test/list/index.spec.ts b/src/list/index.spec.ts
similarity index 92%
rename from test/list/index.spec.ts
rename to src/list/index.spec.ts
index 8f11153d..86809b81 100644
--- a/test/list/index.spec.ts
+++ b/src/list/index.spec.ts
@@ -1,8 +1,4 @@
-import list from '../../src/list'
-
-test('unit:list', async () => {
- expect(typeof list).toBe('function')
-})
+import list from '.'
test('unit:list:default', async () => {
const log = jest.spyOn(console, 'log').mockImplementation()
diff --git a/src/load.spec.ts b/src/load.spec.ts
new file mode 100644
index 00000000..7a2fed83
--- /dev/null
+++ b/src/load.spec.ts
@@ -0,0 +1,57 @@
+import fs from 'fs'
+import path from 'path'
+import { context, destory, exists, fixture, mktmpdir } from '../test/helpers'
+import load from './load'
+
+test('unit:load:normal', async () => {
+ const ctx = context({ src: fixture('features') })
+ await load(ctx)
+ expect(ctx.config.name).toBe('features')
+ expect(ctx.config.version).toBe('0.1.0')
+ expect(ctx.config.source).toBe('template')
+ expect(ctx.config.metadata?.date).toBeTruthy()
+ expect(ctx.config.prompts).toBeInstanceOf(Array)
+ expect(ctx.config.filters).toBeTruthy()
+ expect(ctx.config.helpers).toBeTruthy()
+ expect(ctx.config.install).toBe('npm')
+ expect(ctx.config.init).toBe(true)
+ expect(typeof ctx.config.setup).toBe('function')
+ expect(typeof ctx.config.prepare).toBe('function')
+ expect(typeof ctx.config.emit).toBe('function')
+ expect(typeof ctx.config.complete).toBe('function')
+})
+
+test('unit:load:default', async () => {
+ const ctx = context({ template: 'fake-load', src: fixture('minima') })
+ await load(ctx)
+ expect(ctx.config.name).toBe('fake-load')
+})
+
+test('unit:load:error', async () => {
+ const ctx = context({ src: fixture('error') })
+ expect.hasAssertions()
+ try {
+ await load(ctx)
+ } catch (e) {
+ expect((e as Error).message).toBe('Invalid template: template needs to expose an object.')
+ }
+})
+
+test('unit:load:install-deps', async () => {
+ const temp = await mktmpdir()
+
+ await fs.promises.writeFile(path.join(temp, 'package.json'), JSON.stringify({
+ dependencies: { caz: '0.0.0' },
+ devDependencies: { zce: '0.0.0' }
+ }))
+
+ const ctx = context({ src: temp })
+
+ await load(ctx)
+
+ expect(await exists(path.join(ctx.src, 'node_modules'))).toBe(true)
+ expect(await exists(path.join(ctx.src, 'node_modules/caz'))).toBe(true)
+ expect(await exists(path.join(ctx.src, 'node_modules/zce'))).toBe(false)
+
+ await destory(temp)
+})
diff --git a/src/init/load.ts b/src/load.ts
similarity index 50%
rename from src/init/load.ts
rename to src/load.ts
index cbd47969..0a43cc91 100644
--- a/src/init/load.ts
+++ b/src/load.ts
@@ -1,9 +1,12 @@
+import ora from 'ora'
+import { exec } from './core'
import { Context } from './types'
/**
* Load template config.
* @todo
* - Adapt to any repository?
+ * - Automatic install template dependencies.
* - Template dependencies not found.
* - Check template is available.
*/
@@ -11,6 +14,17 @@ export default async (ctx: Context): Promise => {
// default template name
ctx.config.name = ctx.template
+ // Automatic install template dependencies.
+ const spinner = ora('Installing template dependencies...').start()
+ try {
+ /* istanbul ignore next */
+ const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
+ await exec(cmd, ['install', '--production'], { cwd: ctx.src })
+ spinner.succeed('Installing template dependencies complete.')
+ } catch {
+ spinner.fail('Install template dependencies failed.')
+ }
+
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require(ctx.src)
@@ -20,9 +34,10 @@ export default async (ctx: Context): Promise => {
}
Object.assign(ctx.config, mod)
- } catch (e) {
+ } catch (err) {
+ const e = err as NodeJS.ErrnoException
if (e.code === 'MODULE_NOT_FOUND') return
- e.message = `Invalid template: ${e.message as string}`
+ e.message = `Invalid template: ${e.message}`
throw e
}
}
diff --git a/test/init/prepare.spec.ts b/src/prepare.spec.ts
similarity index 67%
rename from test/init/prepare.spec.ts
rename to src/prepare.spec.ts
index a01156b9..5e282b9b 100644
--- a/test/init/prepare.spec.ts
+++ b/src/prepare.spec.ts
@@ -1,15 +1,10 @@
-import path from 'path'
-import { createContext } from './util'
-import prepare from '../../src/init/prepare'
+import { context, fixture } from '../test/helpers'
+import prepare from './prepare'
-const src = path.join(__dirname, '../fixtures/features')
+const src = fixture('features')
-test('unit:init:prepare', async () => {
- expect(typeof prepare).toBe('function')
-})
-
-test('unit:init:prepare:default', async () => {
- const ctx = createContext({ src })
+test('unit:prepare:default', async () => {
+ const ctx = context({ src })
await prepare(ctx)
expect(ctx.files).toHaveLength(6)
const names = ctx.files.map(i => i.path)
@@ -21,8 +16,8 @@ test('unit:init:prepare:default', async () => {
expect(names).toContain('src/index.ts')
})
-test('unit:init:prepare:custom', async () => {
- const ctx = createContext({
+test('unit:prepare:custom', async () => {
+ const ctx = context({
src,
answers: {
features: ['cli', 'typescript']
@@ -45,9 +40,9 @@ test('unit:init:prepare:custom', async () => {
expect(names).toContain('src/index.ts')
})
-test('unit:init:prepare:hook', async () => {
+test('unit:prepare:hook', async () => {
const callback = jest.fn()
- const ctx = createContext({}, { prepare: callback })
+ const ctx = context({}, { prepare: callback })
await prepare(ctx)
expect(callback.mock.calls[0][0]).toBe(ctx)
})
diff --git a/src/init/prepare.ts b/src/prepare.ts
similarity index 97%
rename from src/init/prepare.ts
rename to src/prepare.ts
index e0dec1a0..daa11ac1 100644
--- a/src/init/prepare.ts
+++ b/src/prepare.ts
@@ -1,6 +1,6 @@
import path from 'path'
import glob from 'fast-glob'
-import { file } from '../core'
+import { file } from './core'
import { Context } from './types'
/**
diff --git a/test/init/rename.spec.ts b/src/rename.spec.ts
similarity index 64%
rename from test/init/rename.spec.ts
rename to src/rename.spec.ts
index 1150df78..2d08369c 100644
--- a/test/init/rename.spec.ts
+++ b/src/rename.spec.ts
@@ -1,12 +1,8 @@
-import { createContext } from './util'
-import rename from '../../src/init/rename'
+import { context } from '../test/helpers'
+import rename from './rename'
-test('unit:init:rename', async () => {
- expect(typeof rename).toBe('function')
-})
-
-test('unit:init:rename:normal', async () => {
- const ctx = createContext({
+test('unit:rename:normal', async () => {
+ const ctx = context({
answers: { foo: 'caz' },
files: [
{ path: 'original', contents: Buffer.from('') },
diff --git a/src/init/rename.ts b/src/rename.ts
similarity index 100%
rename from src/init/rename.ts
rename to src/rename.ts
diff --git a/test/init/render.spec.ts b/src/render.spec.ts
similarity index 66%
rename from test/init/render.spec.ts
rename to src/render.spec.ts
index 7ccc1be5..28cb7c7d 100644
--- a/test/init/render.spec.ts
+++ b/src/render.spec.ts
@@ -1,23 +1,18 @@
import fs from 'fs'
-import path from 'path'
-import { createContext } from './util'
-import render from '../../src/init/render'
+import { context, fixture } from '../test/helpers'
+import render from './render'
-test('unit:init:render', async () => {
- expect(typeof render).toBe('function')
-})
-
-test('unit:init:render:normal', async () => {
+test('unit:render:normal', async () => {
const template = `
<%= title %><% if (enable) { %>
hahaha
<% } %>
`
// binary files
- const img = fs.readFileSync(path.join(__dirname, '../fixtures/caz.png'))
- const zip = fs.readFileSync(path.join(__dirname, '../fixtures/archive.zip'))
+ const img = fs.readFileSync(fixture('caz.png'))
+ const zip = fs.readFileSync(fixture('archive.zip'))
- const ctx = createContext({
+ const ctx = context({
answers: {
title: 'caz test',
enable: false
@@ -40,10 +35,10 @@ test('unit:init:render:normal', async () => {
expect(ctx.files[3].contents).toBe(zip)
})
-test('unit:init:render:metadata', async () => {
+test('unit:render:metadata', async () => {
const now = Date.now()
- const ctx = createContext({
+ const ctx = context({
files: [
{ path: 'a.txt', contents: Buffer.from('<%= now %>') }
]
@@ -56,8 +51,8 @@ test('unit:init:render:metadata', async () => {
expect(ctx.files[0].contents.toString()).toBe(now.toString())
})
-test('unit:init:render:helpers', async () => {
- const ctx = createContext({
+test('unit:render:helpers', async () => {
+ const ctx = context({
files: [
{ path: 'a.txt', contents: Buffer.from('<%= upper(\'caz\') %>') }
]
diff --git a/src/init/render.ts b/src/render.ts
similarity index 95%
rename from src/init/render.ts
rename to src/render.ts
index 7be624c7..c09883fd 100644
--- a/src/init/render.ts
+++ b/src/render.ts
@@ -1,5 +1,5 @@
import _ from 'lodash'
-import { file } from '../core'
+import { file } from './core'
import { Context } from './types'
/**
diff --git a/src/resolve.spec.ts b/src/resolve.spec.ts
new file mode 100644
index 00000000..d62967bb
--- /dev/null
+++ b/src/resolve.spec.ts
@@ -0,0 +1,143 @@
+import fs from 'fs'
+import path from 'path'
+import { file, http, config } from './core'
+import { context, exists, destory, fixture, mktmpdir } from '../test/helpers'
+import resolve, { getTemplatePath, getTemplateUrl } from './resolve'
+
+let log: jest.SpyInstance
+let download: jest.SpyInstance
+let tmpdir: string | undefined
+
+const src = path.join(config.paths.cache, 'f8327697301af2fa')
+
+beforeEach(async () => {
+ log = jest.spyOn(console, 'log').mockImplementation()
+ download = jest.spyOn(http, 'download').mockImplementation(async () => {
+ tmpdir = await mktmpdir()
+ const file = fixture('archive.zip')
+ const target = path.join(tmpdir, 'archive.zip')
+ await fs.promises.copyFile(file, target)
+ return target
+ })
+})
+
+afterEach(async () => {
+ log.mockRestore()
+ download.mockRestore()
+ if (tmpdir != null) {
+ await destory(tmpdir)
+ tmpdir = undefined
+ }
+})
+
+test('unit:resolve:getTemplatePath', async () => {
+ const notdir = await getTemplatePath('caz-faker')
+ expect(notdir).toBe(false)
+
+ const dir1 = await getTemplatePath(__dirname)
+ expect(dir1).toBe(__dirname)
+
+ try {
+ await getTemplatePath('./caz-faker')
+ } catch (e) {
+ expect((e as Error).message).toBe('Local template not found: `./caz-faker` is not a directory')
+ }
+
+ try {
+ await getTemplatePath('~/caz-faker')
+ } catch (e) {
+ expect((e as Error).message).toBe('Local template not found: `~/caz-faker` is not a directory')
+ }
+})
+
+test('unit:resolve:getTemplateUrl', async () => {
+ const url1 = await getTemplateUrl('tpl1')
+ expect(url1).toBe('https://github.com/caz-templates/tpl1/archive/refs/heads/master.zip')
+
+ const url2 = await getTemplateUrl('zce/tpl2')
+ expect(url2).toBe('https://github.com/zce/tpl2/archive/refs/heads/master.zip')
+
+ const url3 = await getTemplateUrl('zce/tpl3#dev')
+ expect(url3).toBe('https://github.com/zce/tpl3/archive/refs/heads/dev.zip')
+
+ const url4 = await getTemplateUrl('tpl4#dev')
+ expect(url4).toBe('https://github.com/caz-templates/tpl4/archive/refs/heads/dev.zip')
+
+ const url5 = await getTemplateUrl('https://github.com/zce/tpl5/archive/refs/heads/dev.zip')
+ expect(url5).toBe('https://github.com/zce/tpl5/archive/refs/heads/dev.zip')
+
+ const url6 = await getTemplateUrl('zce/tpl3#dev/cli')
+ expect(url6).toBe('https://github.com/zce/tpl3/archive/refs/heads/dev/cli.zip')
+
+ const url7 = await getTemplateUrl('tpl7#topic/xyz')
+ expect(url7).toBe('https://github.com/caz-templates/tpl7/archive/refs/heads/topic/xyz.zip')
+})
+
+test('unit:resolve:local-relative', async () => {
+ const ctx = context({ template: './caz-faker' })
+ try {
+ await resolve(ctx)
+ } catch (e) {
+ expect((e as Error).message).toBe('Local template not found: `./caz-faker` is not a directory')
+ }
+})
+
+test('unit:resolve:local-absolute', async () => {
+ const ctx = context({ template: __dirname })
+ await resolve(ctx)
+ expect(ctx.src).toBe(__dirname)
+})
+
+test('unit:resolve:local-tildify', async () => {
+ const ctx = context({ template: '~/caz-faker' })
+ try {
+ await resolve(ctx)
+ } catch (e) {
+ expect((e as Error).message).toBe('Local template not found: `~/caz-faker` is not a directory')
+ }
+})
+
+test('unit:resolve:fetch-remote', async () => {
+ await fs.promises.mkdir(src, { recursive: true })
+
+ const ctx = context({ template: 'minima' })
+ await resolve(ctx)
+ expect(ctx.src).toBe(src)
+ expect(await exists(src)).toBe(true)
+ expect(await exists(path.join(src, 'LICENSE'))).toBe(true)
+ expect(await exists(path.join(src, 'README.md'))).toBe(true)
+})
+
+test('unit:resolve:fetch-cache-success', async () => {
+ await fs.promises.mkdir(src, { recursive: true })
+
+ const ctx = context({ template: 'minima', options: { offline: true } })
+ await resolve(ctx)
+ expect(log.mock.calls[0][0]).toBe(`Using cached template: \`${file.tildify(src)}\`.`)
+})
+
+test('unit:resolve:fetch-cache-failed', async () => {
+ await destory(src)
+
+ const ctx = context({ template: 'minima', options: { offline: true } })
+ await resolve(ctx)
+ expect(log.mock.calls[0][0]).toBe(`Cache not found: \`${file.tildify(src)}\`.`)
+ expect(ctx.src).toBe(src)
+ expect(await exists(src)).toBe(true)
+ expect(await exists(path.join(src, 'LICENSE'))).toBe(true)
+ expect(await exists(path.join(src, 'README.md'))).toBe(true)
+})
+
+test('unit:resolve:fetch-error', async () => {
+ download.mockImplementation(async () => {
+ throw new Error('download error')
+ })
+
+ const ctx = context({ template: 'not-found' })
+ expect.hasAssertions()
+ try {
+ await resolve(ctx)
+ } catch (e) {
+ expect((e as Error).message).toBe('Failed to pull `not-found` template: download error.')
+ }
+})
diff --git a/src/init/resolve.ts b/src/resolve.ts
similarity index 97%
rename from src/init/resolve.ts
rename to src/resolve.ts
index c01c8ca9..473f7018 100644
--- a/src/init/resolve.ts
+++ b/src/resolve.ts
@@ -1,7 +1,7 @@
import path from 'path'
import crypto from 'crypto'
import ora from 'ora'
-import { file, http, config } from '../core'
+import { file, http, config } from './core'
import { Context } from './types'
/**
@@ -97,6 +97,6 @@ export default async (ctx: Context): Promise => {
spinner.succeed('Download template complete.')
} catch (e) {
spinner.stop()
- throw new Error(`Failed to pull \`${ctx.template}\` template: ${e.message as string}.`)
+ throw new Error(`Failed to pull \`${ctx.template}\` template: ${(e as Error).message}.`)
}
}
diff --git a/src/setup.spec.ts b/src/setup.spec.ts
new file mode 100644
index 00000000..9376c013
--- /dev/null
+++ b/src/setup.spec.ts
@@ -0,0 +1,15 @@
+import { context } from '../test/helpers'
+import setup from './setup'
+
+test('unit:setup:null', async () => {
+ const ctx = context()
+ const result = await setup(ctx)
+ expect(result).toBe(undefined)
+})
+
+test('unit:setup:callback', async () => {
+ const callback = jest.fn()
+ const ctx = context({}, { setup: callback })
+ await setup(ctx)
+ expect(callback.mock.calls[0][0]).toBe(ctx)
+})
diff --git a/src/init/setup.ts b/src/setup.ts
similarity index 100%
rename from src/init/setup.ts
rename to src/setup.ts
diff --git a/src/init/types.ts b/src/types.ts
similarity index 100%
rename from src/init/types.ts
rename to src/types.ts
diff --git a/test/fixtures/archive.zip b/test/fixtures/archive.zip
index 39054bd8..d9df861e 100644
Binary files a/test/fixtures/archive.zip and b/test/fixtures/archive.zip differ
diff --git a/test/fixtures/error/package.json b/test/fixtures/error/package.json
new file mode 100644
index 00000000..2f4091f4
--- /dev/null
+++ b/test/fixtures/error/package.json
@@ -0,0 +1,3 @@
+{
+ "dependencies": {"fodosdofosdofodoofofoofof": "0.0.0"}
+}
diff --git a/test/fixtures/features/index.js b/test/fixtures/features/index.js
index 4343c129..1ff54840 100644
--- a/test/fixtures/features/index.js
+++ b/test/fixtures/features/index.js
@@ -1,11 +1,7 @@
// full features template
-
-// !!! Sharing the dependencies of caz
-// Make sure the following statement is executed before all code
-module.paths = require.main.paths
+// @ts-check
const path = require('path')
-const chalk = require('chalk')
/** @type {import('../../../src').Template} */
module.exports = {
@@ -95,16 +91,15 @@ module.exports = {
},
complete: async ctx => {
console.clear()
- console.log(chalk`Created a new project in {cyan ${ctx.project}} by the {blue ${ctx.template}} template.\n`)
+ console.log(`Created a new project in ${ctx.project} by the ${ctx.template} template.\n`)
console.log('Getting Started:')
if (ctx.dest !== process.cwd()) {
- console.log(chalk` $ {cyan cd ${path.relative(process.cwd(), ctx.dest)}}`)
+ console.log(` $ cd ${path.relative(process.cwd(), ctx.dest)}`)
}
if (ctx.config.install === false) {
- console.log(chalk` $ {cyan npm install} {gray # or yarn}`)
+ console.log(' $ npm install # or yarn')
}
- console.log(chalk` $ {cyan ${ctx.config.install ? ctx.config.install : 'npm'} test}`)
- // console.log('Good luck :)')
+ console.log(` $ ${ctx.config.install ? ctx.config.install : 'npm'} test`)
console.log('\nHappy hacking :)\n')
}
}
diff --git a/test/fixtures/minima.zip b/test/fixtures/minima.zip
new file mode 100644
index 00000000..3889b26d
Binary files /dev/null and b/test/fixtures/minima.zip differ
diff --git a/test/fixtures/minima/package.json b/test/fixtures/minima/package.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/test/fixtures/minima/package.json
@@ -0,0 +1 @@
+{}
diff --git a/test/helpers.ts b/test/helpers.ts
new file mode 100644
index 00000000..1752f189
--- /dev/null
+++ b/test/helpers.ts
@@ -0,0 +1,70 @@
+import os from 'os'
+import fs from 'fs'
+import path from 'path'
+import { Context, Template } from '../src'
+
+/**
+ * Get a fixture file path.
+ * @param target relative path
+ * @returns absolute path
+ */
+export const fixture = (target: string): string => {
+ return path.join(__dirname, 'fixtures', target)
+}
+
+/**
+ * Check input path exists.
+ * @param input input path
+ * @returns true if input is exists
+ */
+export const exists = async (input: string): Promise => {
+ return await fs.promises.access(input).then(() => true).catch(() => false)
+}
+
+/**
+ * Create a temporary directory.
+ * @returns temp directory path
+ */
+export const mktmpdir = async (): Promise => {
+ return await fs.promises.mkdtemp(path.join(os.tmpdir(), 'caz-test-'))
+}
+
+/**
+ * Force deletion of specified directory.
+ * @param target destory target
+ */
+export const destory = async (...target: string[]): Promise => {
+ for (const item of target) {
+ // cleanup require node >= v14.14.0
+ await fs.promises.rm(item, { recursive: true, force: true })
+ }
+}
+
+// /**
+// * Local mock zip download function
+// * @returns mock download function
+// */
+// export const download = async () => {
+// const file = fixture('archive.zip')
+// const target = path.join(await mktmpdir(), 'archive.zip')
+// await fs.promises.copyFile(file, target)
+// return target
+// }
+
+/**
+ * Create a context.
+ * @param context additional context options
+ * @param config additional config options
+ * @returns a context object
+ */
+export const context = (context?: Partial, config?: Partial): Context => ({
+ template: 'faker',
+ project: 'faker',
+ options: {},
+ src: path.join(__dirname, 'fixtures'),
+ dest: path.join(__dirname, '.temp'),
+ config: { name: 'faker', ...config },
+ answers: {},
+ files: [],
+ ...context
+})
diff --git a/test/index.spec.ts b/test/index.spec.ts
deleted file mode 100644
index 2751a161..00000000
--- a/test/index.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as caz from '../src'
-
-test('unit', async () => {
- expect(typeof caz.inject).toBe('function')
- expect(typeof caz.file).toBe('object')
- expect(typeof caz.http).toBe('object')
- expect(typeof caz.config).toBe('object')
- expect(typeof caz.Ware).toBe('function')
- expect(typeof caz.list).toBe('function')
- expect(typeof caz.default).toBe('function')
-})
diff --git a/test/init/index.spec.ts b/test/init/index.spec.ts
deleted file mode 100644
index 63971d5c..00000000
--- a/test/init/index.spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import fs from 'fs'
-import { createTempDir } from './util'
-import init from '../../src/init'
-import prompts from 'prompts'
-
-test('unit:init', async () => {
- expect(typeof init).toBe('function')
-})
-
-test('unit:init:error', async () => {
- expect.assertions(2)
- try {
- await init(null as unknown as string)
- } catch (e) {
- expect(e.message).toBe('Missing required argument: `template`.')
- }
- try {
- await init('')
- } catch (e) {
- expect(e.message).toBe('Missing required argument: `template`.')
- }
-})
-
-test('unit:init:default', async () => {
- const log = jest.spyOn(console, 'log').mockImplementation()
- const clear = jest.spyOn(console, 'clear').mockImplementation()
- const temp = await createTempDir()
- const original = process.cwd()
- process.chdir(temp)
- prompts.inject(['caz'])
- await init('minima', 'minima-app', { force: true, offline: true })
- expect(fs.existsSync('minima-app')).toBe(true)
- const contents = await fs.promises.readFile('minima-app/caz.txt', 'utf8')
- expect(contents.trim()).toBe('hey caz.')
- process.chdir(original)
- await fs.promises.rmdir(temp, { recursive: true })
- log.mockRestore()
- clear.mockRestore()
-})
diff --git a/test/init/init.spec.ts b/test/init/init.spec.ts
deleted file mode 100644
index d3fbce1b..00000000
--- a/test/init/init.spec.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import os from 'os'
-import fs from 'fs'
-import path from 'path'
-import { createContext, createTempDir } from './util'
-import init from '../../src/init/init'
-
-const gitconfig = path.join(os.homedir(), '.gitconfig')
-let autoGitConfig = false
-
-beforeAll(async () => {
- if (fs.existsSync(gitconfig)) return
- fs.writeFileSync(gitconfig, '[user]\n name = bot\n email = bot@zce.me')
- autoGitConfig = true
-})
-
-afterAll(async () => {
- if (!autoGitConfig) return
- fs.unlinkSync(gitconfig)
-})
-
-test('unit:init:init', async () => {
- expect(typeof init).toBe('function')
-})
-
-test('unit:init:init:null', async () => {
- const ctx = createContext()
- const result = await init(ctx)
- expect(result).toBe(undefined)
-})
-
-test('unit:init:init:false', async () => {
- const ctx = createContext({}, { init: false })
- const result = await init(ctx)
- expect(result).toBe(undefined)
-})
-
-test('unit:init:init:default', async () => {
- const temp = await createTempDir()
- await fs.promises.writeFile(path.join(temp, 'caz.txt'), 'hello')
- const ctx = createContext({
- dest: temp,
- files: [{ path: '.gitignore', contents: Buffer.from('') }]
- })
- await init(ctx)
- expect(fs.existsSync(path.join(temp, '.git'))).toBe(true)
- const stats = await fs.promises.stat(path.join(temp, '.git'))
- expect(stats.isDirectory()).toBe(true)
- expect(fs.existsSync(path.join(temp, '.git', 'COMMIT_EDITMSG'))).toBe(true)
- const msg = await fs.promises.readFile(path.join(temp, '.git', 'COMMIT_EDITMSG'), 'utf8')
- expect(msg.trim()).toBe('feat: initial commit')
- await fs.promises.rmdir(temp, { recursive: true })
-})
-
-test('unit:init:init:manual', async () => {
- const temp = await createTempDir()
- await fs.promises.writeFile(path.join(temp, 'caz.txt'), 'hello')
- const ctx = createContext({ dest: temp }, { init: true })
- await init(ctx)
- expect(fs.existsSync(path.join(temp, '.git'))).toBe(true)
- const stats = await fs.promises.stat(path.join(temp, '.git'))
- expect(stats.isDirectory()).toBe(true)
- expect(fs.existsSync(path.join(temp, '.git', 'COMMIT_EDITMSG'))).toBe(true)
- const msg = await fs.promises.readFile(path.join(temp, '.git', 'COMMIT_EDITMSG'), 'utf8')
- expect(msg.trim()).toBe('feat: initial commit')
- await fs.promises.rmdir(temp, { recursive: true })
-})
-
-test('unit:init:init:error', async () => {
- const temp = await createTempDir()
- const ctx = createContext({ dest: temp }, { init: true })
- expect.hasAssertions()
- try {
- await init(ctx)
- } catch (e) {
- expect(e.message).toBe('Initial repository failed.')
- }
- await fs.promises.rmdir(temp, { recursive: true })
-})
diff --git a/test/init/install.spec.ts b/test/init/install.spec.ts
deleted file mode 100644
index 7ee55a99..00000000
--- a/test/init/install.spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-import { createContext, createTempDir } from './util'
-import install from '../../src/init/install'
-
-// let stdoutWrite: jest.SpyInstance
-// let stderrWrite: jest.SpyInstance
-
-// beforeAll(async () => {
-// stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation()
-// stderrWrite = jest.spyOn(process.stderr, 'write').mockImplementation()
-// })
-
-// afterAll(async () => {
-// stdoutWrite.mockRestore()
-// stderrWrite.mockRestore()
-// })
-
-test('unit:init:install', async () => {
- expect(typeof install).toBe('function')
-})
-
-test('unit:init:init:false', async () => {
- const ctx = createContext({}, { install: false })
- const result = await install(ctx)
- expect(result).toBe(undefined)
-})
-
-test('unit:init:init:null', async () => {
- const ctx = createContext()
- const result = await install(ctx)
- expect(result).toBe(undefined)
-})
-
-test('unit:init:init:default', async () => {
- const temp = await createTempDir()
- const pkg = { dependencies: { caz: '0.0.0' } }
- await fs.promises.writeFile(path.join(temp, 'package.json'), JSON.stringify(pkg))
- const ctx = createContext({
- dest: temp,
- files: [{ path: 'package.json', contents: Buffer.from('') }]
- })
- await install(ctx)
- expect(ctx.config.install).toBe('npm')
- expect(fs.existsSync(path.join(temp, 'node_modules'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'node_modules', 'caz'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'node_modules', 'caz', 'package.json'))).toBe(true)
- await fs.promises.rmdir(temp, { recursive: true })
-})
-
-test('unit:init:init:manual:yarn', async () => {
- const temp = await createTempDir()
- const pkg = { dependencies: { caz: '0.0.0' } }
- await fs.promises.writeFile(path.join(temp, 'package.json'), JSON.stringify(pkg))
- const ctx = createContext({ dest: temp }, { install: 'yarn' })
- await install(ctx)
- expect(fs.existsSync(path.join(temp, 'yarn.lock'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'node_modules'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'node_modules', 'caz'))).toBe(true)
- expect(fs.existsSync(path.join(temp, 'node_modules', 'caz', 'package.json'))).toBe(true)
- await fs.promises.rmdir(temp, { recursive: true })
-})
-
-test('unit:init:init:manual:error', async () => {
- const temp = await createTempDir()
- await fs.promises.writeFile(path.join(temp, 'package.json'), 'error package.json')
- const ctx = createContext({ dest: temp }, { install: 'npm' })
- expect.hasAssertions()
- try {
- await install(ctx)
- } catch (e) {
- expect(e.message).toBe('Install dependencies failed.')
- }
- await fs.promises.rmdir(temp, { recursive: true })
-})
diff --git a/test/init/load.spec.ts b/test/init/load.spec.ts
deleted file mode 100644
index f3ed3207..00000000
--- a/test/init/load.spec.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import path from 'path'
-import { createContext } from './util'
-import load from '../../src/init/load'
-
-test('unit:init:load', async () => {
- expect(typeof load).toBe('function')
-})
-
-test('unit:init:load:normal', async () => {
- const ctx = createContext({
- src: path.join(__dirname, '../fixtures/features')
- })
- await load(ctx)
- expect(ctx.config.name).toBe('features')
- expect(ctx.config.version).toBe('0.1.0')
- expect(ctx.config.source).toBe('template')
- expect(ctx.config.metadata?.date).toBeTruthy()
- expect(ctx.config.prompts).toBeInstanceOf(Array)
- expect(ctx.config.filters).toBeTruthy()
- expect(ctx.config.helpers).toBeTruthy()
- expect(ctx.config.install).toBe('npm')
- expect(ctx.config.init).toBe(true)
- expect(typeof ctx.config.setup).toBe('function')
- expect(typeof ctx.config.prepare).toBe('function')
- expect(typeof ctx.config.emit).toBe('function')
- expect(typeof ctx.config.complete).toBe('function')
-})
-
-test('unit:init:load:default', async () => {
- const ctx = createContext({
- template: 'fake-load',
- src: path.join(__dirname, '../fixtures/minima')
- })
- await load(ctx)
- expect(ctx.config.name).toBe('fake-load')
-})
-
-test('unit:init:load:error', async () => {
- const ctx = createContext({
- src: path.join(__dirname, '../fixtures/error')
- })
- expect.hasAssertions()
- try {
- await load(ctx)
- } catch (e) {
- expect(e.message).toBe('Invalid template: template needs to expose an object.')
- }
-})
diff --git a/test/init/resolve.spec.ts b/test/init/resolve.spec.ts
deleted file mode 100644
index 03c50823..00000000
--- a/test/init/resolve.spec.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-import { createContext } from './util'
-import { file, config } from '../../src'
-import resolve, { getTemplatePath, getTemplateUrl } from '../../src/init/resolve'
-
-let log: jest.SpyInstance
-
-beforeEach(async () => {
- log = jest.spyOn(console, 'log').mockImplementation()
-})
-
-afterEach(async () => {
- log.mockRestore()
-})
-
-test('unit:init:resolve', async () => {
- expect(typeof resolve).toBe('function')
- expect(typeof getTemplatePath).toBe('function')
- expect(typeof getTemplateUrl).toBe('function')
-})
-
-test('unit:init:resolve:getTemplatePath', async () => {
- const notdir = await getTemplatePath('caz-faker')
- expect(notdir).toBe(false)
-
- const dir1 = await getTemplatePath(__dirname)
- expect(dir1).toBe(__dirname)
-
- try {
- await getTemplatePath('./caz-faker')
- } catch (e) {
- expect(e.message).toBe('Local template not found: `./caz-faker` is not a directory')
- }
-
- try {
- await getTemplatePath('~/caz-faker')
- } catch (e) {
- expect(e.message).toBe('Local template not found: `~/caz-faker` is not a directory')
- }
-})
-
-test('unit:init:resolve:getTemplateUrl', async () => {
- const url1 = await getTemplateUrl('tpl1')
- expect(url1).toBe('https://github.com/caz-templates/tpl1/archive/refs/heads/master.zip')
-
- const url2 = await getTemplateUrl('zce/tpl2')
- expect(url2).toBe('https://github.com/zce/tpl2/archive/refs/heads/master.zip')
-
- const url3 = await getTemplateUrl('zce/tpl3#dev')
- expect(url3).toBe('https://github.com/zce/tpl3/archive/refs/heads/dev.zip')
-
- const url4 = await getTemplateUrl('tpl4#dev')
- expect(url4).toBe('https://github.com/caz-templates/tpl4/archive/refs/heads/dev.zip')
-
- const url5 = await getTemplateUrl('https://github.com/zce/tpl5/archive/refs/heads/dev.zip')
- expect(url5).toBe('https://github.com/zce/tpl5/archive/refs/heads/dev.zip')
-
- const url6 = await getTemplateUrl('zce/tpl3#dev/cli')
- expect(url6).toBe('https://github.com/zce/tpl3/archive/refs/heads/dev/cli.zip')
-
- const url7 = await getTemplateUrl('tpl7#topic/xyz')
- expect(url7).toBe('https://github.com/caz-templates/tpl7/archive/refs/heads/topic/xyz.zip')
-})
-
-test('unit:init:resolve:local-relative', async () => {
- const ctx = createContext({ template: './caz-faker' })
- try {
- await resolve(ctx)
- } catch (e) {
- expect(e.message).toBe('Local template not found: `./caz-faker` is not a directory')
- }
-})
-
-test('unit:init:resolve:local-absolute', async () => {
- const ctx = createContext({ template: __dirname })
- await resolve(ctx)
- expect(ctx.src).toBe(__dirname)
-})
-
-test('unit:init:resolve:local-tildify', async () => {
- const ctx = createContext({ template: '~/caz-faker' })
- try {
- await resolve(ctx)
- } catch (e) {
- expect(e.message).toBe('Local template not found: `~/caz-faker` is not a directory')
- }
-})
-
-test('unit:init:resolve:fetch-remote', async () => {
- const src = path.join(config.paths.cache, 'f8327697301af2fa')
- if (!fs.existsSync(src)) {
- await fs.promises.mkdir(src, { recursive: true })
- }
- const ctx = createContext({ template: 'minima' })
- await resolve(ctx)
- expect(ctx.src).toBe(src)
- expect(fs.existsSync(src)).toBe(true)
- expect(fs.existsSync(path.join(src, 'template'))).toBe(true)
- expect(fs.existsSync(path.join(src, 'template', 'caz.txt'))).toBe(true)
-})
-
-test('unit:init:resolve:fetch-cache-success', async () => {
- const src = path.join(config.paths.cache, 'f8327697301af2fa')
- if (!fs.existsSync(src)) {
- await fs.promises.mkdir(src, { recursive: true })
- }
- const ctx = createContext({ template: 'minima', options: { offline: true } })
- await resolve(ctx)
- expect(log.mock.calls[0][0]).toBe(`Using cached template: \`${file.tildify(src)}\`.`)
-})
-
-test('unit:init:resolve:fetch-cache-failed', async () => {
- const src = path.join(config.paths.cache, 'f8327697301af2fa')
- if (fs.existsSync(src)) {
- await fs.promises.rmdir(src, { recursive: true })
- }
- const ctx = createContext({ template: 'minima', options: { offline: true } })
- await resolve(ctx)
- expect(log.mock.calls[0][0]).toBe(`Cache not found: \`${file.tildify(src)}\`.`)
- expect(ctx.src).toBe(src)
- expect(fs.existsSync(src)).toBe(true)
- expect(fs.existsSync(path.join(src, 'template'))).toBe(true)
- expect(fs.existsSync(path.join(src, 'template', 'caz.txt'))).toBe(true)
-})
-
-test('unit:init:resolve:fetch-error', async () => {
- const ctx = createContext({ template: 'not-found' })
- expect.hasAssertions()
- try {
- await resolve(ctx)
- } catch (e) {
- expect(e.message).toBe('Failed to pull `not-found` template: Unexpected response: Not Found.')
- }
-})
diff --git a/test/init/setup.spec.ts b/test/init/setup.spec.ts
deleted file mode 100644
index 5b9db876..00000000
--- a/test/init/setup.spec.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createContext } from './util'
-import setup from '../../src/init/setup'
-
-test('unit:init:setup', async () => {
- expect(typeof setup).toBe('function')
-})
-
-test('unit:init:setup:null', async () => {
- const ctx = createContext()
- const result = await setup(ctx)
- expect(result).toBe(undefined)
-})
-
-test('unit:init:setup:callback', async () => {
- const callback = jest.fn()
- const ctx = createContext({}, { setup: callback })
- await setup(ctx)
- expect(callback.mock.calls[0][0]).toBe(ctx)
-})
diff --git a/test/init/util.ts b/test/init/util.ts
deleted file mode 100644
index 38aeeec2..00000000
--- a/test/init/util.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import os from 'os'
-import fs from 'fs'
-import path from 'path'
-import { Context, Template } from '../../src'
-
-export const createContext = (context?: Partial, config?: Partial): Context => ({
- template: 'faker',
- project: 'faker',
- options: {},
- src: path.join(__dirname, '../fixtures'),
- dest: path.join(__dirname, '../.temp'),
- config: { name: 'faker', ...config },
- answers: {},
- files: [],
- ...context
-})
-
-export const createTempDir = async (): Promise => {
- return await fs.promises.mkdtemp(path.join(os.tmpdir(), 'caz-test-'))
-}
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
deleted file mode 100644
index b81b3b86..00000000
--- a/tsconfig.eslint.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "include": ["**/*.ts"]
-}
diff --git a/tsconfig.json b/tsconfig.json
index 29a4055b..f236578c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,13 +1,11 @@
/* Visit https://aka.ms/tsconfig.json to read more about this file */
{
"compilerOptions": {
- "target": "ES2019", // require node >= 12
- "module": "CommonJS",
- "declaration": true,
- "outDir": "lib",
- "strict": true,
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
"esModuleInterop": true,
- "pretty": true
- },
- "include": ["src", "types.d.ts"]
+ "allowJs": true,
+ "strict": true
+ }
}
diff --git a/types.d.ts b/types.d.ts
index 2625f6e4..108beee1 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -7,3 +7,456 @@ declare module '*/package.json' {
export const name: string
export const version: string
}
+
+// References:
+// https://github.com/egoist/tsup/issues/14
+// https://github.com/egoist/tsup/issues/367
+// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/prompts/index.d.ts
+declare module 'prompts' {
+ import { Readable, Writable } from 'stream'
+
+ namespace prompts {
+ function inject (arr: readonly any[]): void
+
+ function override (obj: { [key: string]: any }): void
+
+ function autocomplete (args: PromptObject): any
+
+ function confirm (args: PromptObject): void
+
+ function date (args: PromptObject): any
+
+ function invisible (args: PromptObject): any
+
+ function list (args: PromptObject): any
+
+ function multiselect (args: PromptObject): any
+
+ function number (args: PromptObject): void
+
+ function password (args: PromptObject): any
+
+ function select (args: PromptObject): void
+
+ function text (args: PromptObject): void
+
+ function toggle (args: PromptObject): void
+ // Based upon: https://github.com/terkelg/prompts/blob/d7d2c37a0009e3235b2e88a7d5cdbb114ac271b2/lib/elements/select.js#L29
+ interface Choice {
+ title: string
+ value?: any
+ disabled?: boolean | undefined
+ selected?: boolean | undefined
+ description?: string | undefined
+ }
+
+ interface Options {
+ onSubmit?:
+ | ((prompt: PromptObject, answer: any, answers: any[]) => void)
+ | undefined
+ onCancel?: ((prompt: PromptObject, answers: any) => void) | undefined
+ }
+
+ interface PromptObject {
+ type: PromptType | Falsy | PrevCaller
+ name: ValueOrFunc
+ message?: ValueOrFunc | undefined
+ initial?:
+ | InitialReturnValue
+ | PrevCaller>
+ | undefined
+ style?: string | PrevCaller | undefined
+ format?: PrevCaller | undefined
+ validate?:
+ | PrevCaller>
+ | undefined
+ onState?: PrevCaller | undefined
+ min?: number | PrevCaller | undefined
+ max?: number | PrevCaller | undefined
+ float?: boolean | PrevCaller | undefined
+ round?: number | PrevCaller | undefined
+ instructions?: string | boolean | undefined
+ increment?: number | PrevCaller | undefined
+ separator?: string | PrevCaller | undefined
+ active?: string | PrevCaller | undefined
+ inactive?: string | PrevCaller | undefined
+ choices?: Choice[] | PrevCaller | undefined
+ hint?: string | PrevCaller | undefined
+ warn?: string | PrevCaller | undefined
+ suggest?: ((input: any, choices: Choice[]) => Promise) | undefined
+ limit?: number | PrevCaller | undefined
+ mask?: string | PrevCaller | undefined
+ stdout?: Writable | undefined
+ stdin?: Readable | undefined
+ }
+
+ type Answers = { [id in T]: any }
+
+ type PrevCaller = (
+ prev: any,
+ values: Answers,
+ prompt: PromptObject
+ ) => R
+
+ type Falsy = false | null | undefined
+
+ type PromptType =
+ | 'text'
+ | 'password'
+ | 'invisible'
+ | 'number'
+ | 'confirm'
+ | 'list'
+ | 'toggle'
+ | 'select'
+ | 'multiselect'
+ | 'autocomplete'
+ | 'date'
+ | 'autocompleteMultiselect'
+
+ type ValueOrFunc = T | PrevCaller
+
+ type InitialReturnValue = string | number | boolean | Date
+ }
+
+ function prompts (
+ questions: prompts.PromptObject | Array>,
+ options?: prompts.Options
+ ): Promise>
+
+ export = prompts
+}
+
+// References:
+// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59369
+// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/adm-zip/index.d.ts
+declare module 'adm-zip' {
+ class AdmZip {
+ /**
+ * @param fileNameOrRawData If provided, reads an existing archive. Otherwise creates a new, empty archive.
+ */
+ constructor (fileNameOrRawData?: string | Buffer);
+ /**
+ * Extracts the given entry from the archive and returns the content.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @return `Buffer` or `null` in case of error.
+ */
+ readFile (entry: string | AdmZip.IZipEntry): Buffer | null;
+ /**
+ * Asynchronous `readFile`.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @param callback Called with a `Buffer` or `null` in case of error.
+ */
+ readFileAsync (
+ entry: string | AdmZip.IZipEntry,
+ callback: (data: Buffer | null, err: string) => any
+ ): void;
+ /**
+ * Extracts the given entry from the archive and returns the content as
+ * plain text in the given encoding.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @param encoding If no encoding is specified `"utf8"` is used.
+ */
+ readAsText (fileName: string | AdmZip.IZipEntry, encoding?: string): string;
+ /**
+ * Asynchronous `readAsText`.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @param callback Called with the resulting string.
+ * @param encoding If no encoding is specified `"utf8"` is used.
+ */
+ readAsTextAsync (
+ fileName: string | AdmZip.IZipEntry,
+ callback: (data: string, err: string) => any,
+ encoding?: string
+ ): void;
+ /**
+ * Remove the entry from the file or the entry and all its nested directories
+ * and files if the given entry is a directory.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ */
+ deleteFile (entry: string | AdmZip.IZipEntry): void;
+ /**
+ * Adds a comment to the zip. The zip must be rewritten after
+ * adding the comment.
+ * @param comment Content of the comment.
+ */
+ addZipComment (comment: string): void;
+ /**
+ * @return The zip comment.
+ */
+ getZipComment (): string;
+ /**
+ * Adds a comment to a specified file or `IZipEntry`. The zip must be rewritten after
+ * adding the comment.
+ * The comment cannot exceed 65535 characters in length.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @param comment The comment to add to the entry.
+ */
+ addZipEntryComment (entry: string | AdmZip.IZipEntry, comment: string): void;
+ /**
+ * Returns the comment of the specified entry.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @return The comment of the specified entry.
+ */
+ getZipEntryComment (entry: string | AdmZip.IZipEntry): string;
+ /**
+ * Updates the content of an existing entry inside the archive. The zip
+ * must be rewritten after updating the content.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @param content The entry's new contents.
+ */
+ updateFile (entry: string | AdmZip.IZipEntry, content: Buffer): void;
+ /**
+ * Adds a file from the disk to the archive.
+ * @param localPath Path to a file on disk.
+ * @param zipPath Path to a directory in the archive. Defaults to the empty
+ * string.
+ * @param zipName Name for the file.
+ */
+ addLocalFile (localPath: string, zipPath?: string, zipName?: string): void;
+ /**
+ * Adds a local directory and all its nested files and directories to the
+ * archive.
+ * @param localPath Path to a folder on disk.
+ * @param zipPath Path to a folder in the archive. Default: `""`.
+ * @param filter RegExp or Function if files match will be included.
+ */
+ addLocalFolder (
+ localPath: string,
+ zipPath?: string,
+ filter?: RegExp | ((filename: string) => boolean)
+ ): void;
+ /**
+ * Allows you to create a entry (file or directory) in the zip file.
+ * If you want to create a directory the `entryName` must end in `"/"` and a `null`
+ * buffer should be provided.
+ * @param entryName Entry path.
+ * @param content Content to add to the entry; must be a 0-length buffer
+ * for a directory.
+ * @param comment Comment to add to the entry.
+ * @param attr Attribute to add to the entry.
+ */
+ addFile (
+ entryName: string,
+ data: Buffer,
+ comment?: string,
+ attr?: number
+ ): void;
+ /**
+ * Returns an array of `IZipEntry` objects representing the files and folders
+ * inside the archive.
+ */
+ getEntries (): AdmZip.IZipEntry[];
+ /**
+ * Returns a `IZipEntry` object representing the file or folder specified by `name`.
+ * @param name Name of the file or folder to retrieve.
+ * @return The entry corresponding to the `name`.
+ */
+ getEntry (name: string): AdmZip.IZipEntry | null;
+ /**
+ * Extracts the given entry to the given `targetPath`.
+ * If the entry is a directory inside the archive, the entire directory and
+ * its subdirectories will be extracted.
+ * @param entry The full path of the entry or a `IZipEntry` object.
+ * @param targetPath Target folder where to write the file.
+ * @param maintainEntryPath If maintainEntryPath is `true` and the entry is
+ * inside a folder, the entry folder will be created in `targetPath` as
+ * well. Default: `true`.
+ * @param overwrite If the file already exists at the target path, the file
+ * will be overwriten if this is `true`. Default: `false`.
+ */
+ extractEntryTo (
+ entryPath: string | AdmZip.IZipEntry,
+ targetPath: string,
+ maintainEntryPath?: boolean,
+ overwrite?: boolean
+ ): boolean;
+ /**
+ * Extracts the entire archive to the given location.
+ * @param targetPath Target location.
+ * @param overwrite If the file already exists at the target path, the file
+ * will be overwriten if this is `true`. Default: `false`.
+ */
+ extractAllTo (targetPath: string, overwrite?: boolean): void;
+ /**
+ * Extracts the entire archive to the given location.
+ * @param targetPath Target location.
+ * @param overwrite If the file already exists at the target path, the file
+ * will be overwriten if this is `true`. Default: `false`.
+ * @param keepOriginalPermission The file will be set as the permission from
+ * the entry if this is true. Default: `false`.
+ */
+ extractAllTo (
+ targetPath: string,
+ overwrite?: boolean,
+ keepOriginalPermission?: boolean
+ ): void;
+ /**
+ * Extracts the entire archive to the given location.
+ * @param targetPath Target location.
+ * @param overwrite If the file already exists at the target path, the file
+ * will be overwriten if this is `true`. Default: `false`.
+ * @param callback The callback function will be called after extraction.
+ */
+ extractAllToAsync (
+ targetPath: string,
+ overwrite?: boolean,
+ callback?: (error: Error) => void
+ ): void;
+ /**
+ * Extracts the entire archive to the given location.
+ * @param targetPath Target location.
+ * @param overwrite If the file already exists at the target path, the file
+ * will be overwriten if this is `true`. Default: `false`.
+ * @param keepOriginalPermission The file will be set as the permission from
+ * the entry if this is true. Default: `false`.
+ * @param callback The callback function will be called after extraction.
+ */
+ extractAllToAsync (
+ targetPath: string,
+ overwrite?: boolean,
+ keepOriginalPermission?: boolean,
+ callback?: (error: Error) => void
+ ): void;
+ /**
+ * Writes the newly created zip file to disk at the specified location or
+ * if a zip was opened and no `targetFileName` is provided, it will
+ * overwrite the opened zip.
+ */
+ writeZip (
+ targetFileName?: string,
+ callback?: (error: Error | null) => void
+ ): void;
+ /**
+ * Returns the content of the entire zip file.
+ */
+ toBuffer (): Buffer;
+ /**
+ * Asynchronously returns the content of the entire zip file.
+ * @param onSuccess called with the content of the zip file, once it has been generated.
+ * @param onFail unused.
+ * @param onItemStart called before an entry is compressed.
+ * @param onItemEnd called after an entry is compressed.
+ */
+ toBuffer (
+ onSuccess: (buffer: Buffer) => void,
+ onFail?: (...args: any[]) => void,
+ onItemStart?: (name: string) => void,
+ onItemEnd?: (name: string) => void
+ ): void;
+ /**
+ * Test the archive.
+ */
+ test (): boolean;
+ }
+
+ namespace AdmZip {
+ /**
+ * The `IZipEntry` is more than a structure representing the entry inside the
+ * zip file. Beside the normal attributes and headers a entry can have, the
+ * class contains a reference to the part of the file where the compressed
+ * data resides and decompresses it when requested. It also compresses the
+ * data and creates the headers required to write in the zip file.
+ */
+ // disable warning about the I-prefix in interface name to prevent breaking stuff for users without a major bump
+ // tslint:disable-next-line:interface-name
+ interface IZipEntry {
+ /**
+ * Represents the full name and path of the file
+ */
+ entryName: string
+ readonly rawEntryName: Buffer
+ /**
+ * Extra data associated with this entry.
+ */
+ extra: Buffer
+ /**
+ * Entry comment.
+ */
+ comment: string
+ readonly name: string
+ /**
+ * Read-Only property that indicates the type of the entry.
+ */
+ readonly isDirectory: boolean
+ /**
+ * Get the header associated with this ZipEntry.
+ */
+ readonly header: EntryHeader
+ attr: number
+ /**
+ * Retrieve the compressed data for this entry. Note that this may trigger
+ * compression if any properties were modified.
+ */
+ getCompressedData: () => Buffer
+ /**
+ * Asynchronously retrieve the compressed data for this entry. Note that
+ * this may trigger compression if any properties were modified.
+ */
+ getCompressedDataAsync: (callback: (data: Buffer) => void) => void
+ /**
+ * Set the (uncompressed) data to be associated with this entry.
+ */
+ setData: (value: string | Buffer) => void
+ /**
+ * Get the decompressed data associated with this entry.
+ */
+ getData: () => Buffer
+ /**
+ * Asynchronously get the decompressed data associated with this entry.
+ */
+ getDataAsync: (callback: (data: Buffer, err: string) => any) => void
+ /**
+ * Returns the CEN Entry Header to be written to the output zip file, plus
+ * the extra data and the entry comment.
+ */
+ packHeader: () => Buffer
+ /**
+ * Returns a nicely formatted string with the most important properties of
+ * the ZipEntry.
+ */
+ toString: () => string
+ }
+
+ interface EntryHeader {
+ made: number
+ version: number
+ flags: number
+ method: number
+ time: Date
+ crc: number
+ compressedSize: number
+ size: number
+ fileNameLength: number
+ extraLength: number
+ commentLength: number
+ diskNumStart: number
+ inAttr: number
+ attr: number
+ offset: number
+ readonly encripted: boolean
+ readonly entryHeaderSize: number
+ readonly realDataOffset: number
+ readonly dataHeader: DataHeader
+ loadDataHeaderFromBinary: (data: Buffer) => void
+ loadFromBinary: (data: Buffer) => void
+ dataHeaderToBinary: () => Buffer
+ entryHeaderToBinary: () => Buffer
+ toString: () => string
+ }
+
+ interface DataHeader {
+ version: number
+ flags: number
+ method: number
+ time: number
+ crc: number
+ compressedSize: number
+ size: number
+ fnameLen: number
+ extraLen: number
+ }
+ }
+
+ export = AdmZip
+}