├── CHANGELOG.md ├── bin ├── run.cmd └── run ├── src ├── index.ts ├── hooks │ └── init │ │ ├── createDirs.ts │ │ └── checkUpdate.ts ├── path.ts ├── typings │ └── index.d.ts ├── getVersion.ts ├── commands │ ├── login.ts │ ├── init.ts │ ├── export.ts │ └── upload.ts ├── Config.ts ├── User.ts ├── base.ts ├── LarkClient.ts └── Document.ts ├── .gitattributes ├── .vscode └── settings.json ├── docs ├── test.md ├── assets │ ├── goku.png │ ├── waque.png │ └── screenshot.gif ├── changelog.md ├── summary.md ├── faq.md ├── extend.md ├── configuration.md ├── tips.md └── getting-started.md ├── commitlint.config.js ├── test ├── helpers │ └── init.js ├── mocha.opts ├── tsconfig.json ├── commands │ ├── init.test.ts │ ├── login.test.ts │ └── export.test.ts └── index.test.ts ├── .gitignore ├── .nycrc ├── .editorconfig ├── tslint.json ├── yuque.yml ├── LEGAL.md ├── tsconfig.json ├── appveyor.yml ├── .github └── workflows │ └── upload.yml ├── README.md └── package.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from '@oclif/command'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.ts text eol=lf 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /docs/test.md: -------------------------------------------------------------------------------- 1 | # test 2 | 3 | ## 代码块 4 | 5 | ```bash 6 | sed 's/t/T/' file 7 | ``` 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /docs/assets/goku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesmeck/waque/HEAD/docs/assets/goku.png -------------------------------------------------------------------------------- /docs/assets/waque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesmeck/waque/HEAD/docs/assets/waque.png -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## 1.0.0 4 | 5 | `2018-11-07` 6 | 7 | 🎉🎉🎉🎉 发布瓦雀。 8 | -------------------------------------------------------------------------------- /docs/assets/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesmeck/waque/HEAD/docs/assets/screenshot.gif -------------------------------------------------------------------------------- /test/helpers/init.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') 3 | -------------------------------------------------------------------------------- /docs/summary.md: -------------------------------------------------------------------------------- 1 | - [开始使用](getting-started) 2 | - [使用技巧](tips) 3 | - [语法扩展](extend) 4 | - [配置说明](configuration) 5 | - [常见问题](faq) 6 | - [更新日志](changelog) 7 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | oclif.manifest.json 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".ts" 4 | ], 5 | "include": [ 6 | "src/**/*.ts" 7 | ], 8 | "exclude": [ 9 | "**/*.d.ts" 10 | ], 11 | "all": true 12 | } 13 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/helpers/init.js 2 | --require ts-node/register 3 | --require source-map-support/register 4 | --watch-extensions ts 5 | --recursive 6 | --reporter spec 7 | --timeout 5000 8 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "declaration": false, 3 | "extends": "../tsconfig", 4 | "compilerOptions": { 5 | "sourceMap": true 6 | }, 7 | "include": [ 8 | "./**/*", 9 | "../src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /src/hooks/init/createDirs.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import * as mkdirp from 'mkdirp'; 3 | 4 | export default function (options: any) { 5 | if (!existsSync(options.config.cacheDir)) { 6 | mkdirp.sync(options.config.cacheDir); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@oclif/tslint", 3 | "rules": { 4 | "ordered-imports": false, 5 | "semicolon": [true, "always"], 6 | "object-curly-spacing": false, 7 | "no-http-string": false, 8 | "no-unused-variable": [true, {"ignore-pattern": "^_"}] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ## 语雀上为什么会出现重复的文档? 4 | 5 | 因为瓦雀是通过文档的 url 作为唯一标示来更新语雀文档的,所以如果你重命名或者修改过文档的 url,那么瓦雀就会新建一篇文档,而旧的文档会被保留。修改 url 建议先在语雀上操作,再修改本地文档。 6 | 7 | ## 抱歉,语雀不允许通过 API 修改非 Markdown 模式文档,请到语雀进行操作。 8 | 9 | 如果你从语雀导出了一篇文档想用瓦雀更新回去,很可能就会碰到这个错误。原因是这篇文档是从非 markdown 模式创建的,你可以先从语雀上删除这篇文档,再尝试用瓦雀更新。 10 | -------------------------------------------------------------------------------- /src/path.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import slash from 'slash2'; 3 | 4 | export function resolve(id: string) { 5 | return slash(path.resolve(id)); 6 | } 7 | 8 | export function join(...segments: string[]) { 9 | return slash(path.join(...segments)); 10 | } 11 | 12 | export const basename = path.basename; 13 | -------------------------------------------------------------------------------- /yuque.yml: -------------------------------------------------------------------------------- 1 | # 配置请参考:https://www.yuque.com/waquehq/docs/configuration 2 | repo: 'waquehq/docs' 3 | pattern: 'docs/**/*.md' 4 | template: 5 | variables: 6 | foo: bar 7 | tags: 8 | blockStart: '<%' 9 | blockEnd: '%>' 10 | variableStart: '<$' 11 | variableEnd: '$>' 12 | commentStart: '<#' 13 | commentEnd: '#>' 14 | -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'signale'; 2 | 3 | declare module 'yaml-front-matter'; 4 | 5 | declare module 'remark'; 6 | 7 | declare module 'unist-util-visit'; 8 | 9 | declare module 'sha1-file'; 10 | 11 | declare module 'yuque-auth'; 12 | 13 | declare module 'slash2' { 14 | function slash(path: string): string; 15 | export = slash; 16 | } 17 | -------------------------------------------------------------------------------- /LEGAL.md: -------------------------------------------------------------------------------- 1 | Legal Disclaimer 2 | 3 | Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail. 4 | 5 | 法律免责声明 6 | 7 | 关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。 -------------------------------------------------------------------------------- /test/commands/init.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('init', () => { 4 | test 5 | .stdout() 6 | .command(['init']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['init', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/login.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('login', () => { 4 | test 5 | .stdout() 6 | .command(['login']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['login', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/export.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('export', () => { 4 | test 5 | .stdout() 6 | .command(['export']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['export', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "importHelpers": true, 6 | "module": "commonjs", 7 | "outDir": "./lib", 8 | "pretty": true, 9 | "esModuleInterop": true, 10 | "rootDirs": [ 11 | "./src" 12 | ], 13 | "strict": true, 14 | "target": "es2017", 15 | "skipLibCheck": true 16 | }, 17 | "include": [ 18 | "./src/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | import cmd = require('../src') 4 | 5 | describe('waque-cli', () => { 6 | test 7 | .stdout() 8 | .do(() => cmd.run([])) 9 | .it('runs hello', ctx => { 10 | expect(ctx.stdout).to.contain('hello world') 11 | }) 12 | 13 | test 14 | .stdout() 15 | .do(() => cmd.run(['--name', 'jeff'])) 16 | .it('runs hello --name jeff', ctx => { 17 | expect(ctx.stdout).to.contain('hello jeff') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "9" 3 | cache: 4 | - '%LOCALAPPDATA%\Yarn -> appveyor.yml' 5 | - node_modules -> yarn.lock 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version x64 9 | - yarn 10 | test_script: 11 | - .\bin\run --version 12 | - .\bin\run --help 13 | - yarn test 14 | after_test: 15 | - .\node_modules\.bin\nyc report --reporter text-lcov > coverage.lcov 16 | - ps: | 17 | $env:PATH = 'C:\msys64\usr\bin;' + $env:PATH 18 | Invoke-WebRequest -Uri 'https://codecov.io/bash' -OutFile codecov.sh 19 | bash codecov.sh 20 | 21 | build: off 22 | 23 | -------------------------------------------------------------------------------- /src/getVersion.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import axios from 'axios'; 3 | 4 | async function run(name: string, file: string, version: string, registry: string) { 5 | await fs.outputJSON(file, {current: version}); // touch file with current version to prevent multiple updates 6 | const { data } = await axios.get(`${registry}/${name.replace('/', '%2f')}`, {timeout: 5000}); 7 | await fs.outputJSON(file, {...data['dist-tags'], current: version}); 8 | process.exit(0); 9 | } 10 | 11 | run(process.argv[2], process.argv[3], process.argv[4], process.argv[5]) 12 | .catch(require('@oclif/errors/handle')); 13 | -------------------------------------------------------------------------------- /src/commands/login.ts: -------------------------------------------------------------------------------- 1 | import { auth } from 'yuque-auth'; 2 | import User from '../User'; 3 | import Base from '../base'; 4 | 5 | export default class Login extends Base { 6 | static description = 'log into lark'; 7 | 8 | static flags = Base.flags; 9 | 10 | async run() { 11 | const res = await auth({ 12 | clientId: '8Jy5oKMnVHpiL1ci1CRV', 13 | clientSecret: 'Erc2ZqUM6WAs8PeujQQbEzNalYmV1My2cQRJA9PO', 14 | scope: 'repo,doc,group', 15 | }); 16 | const user = new User(this.config, res.access_token); 17 | await user.initByToken(); 18 | this.log('登录成功,欢迎使用瓦雀。'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/extend.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: false 3 | --- 4 | 5 | # Markdown 语法扩展 6 | 7 | 瓦雀对 markdown 语法做了一些扩展: 8 | 9 | ## 文档设置 10 | 11 | 通过 markdown 前的 yaml 来设置文档,目前支持三个属性: 12 | 13 | - **url** - 文档 url,如果不设置瓦雀默认会用文件名作为 url。 14 | - **public** - 浏览权限,1 是公开,0 是私密。 15 | - **template** - 是否启用模板功能,优先级高于 `yuque.yml` 中的设置。 16 | 17 | ```markdown 18 | --- 19 | url: hello-world 20 | public: 1 21 | --- 22 | 23 | # Hello world 24 | ``` 25 | 26 | ## 模板功能 27 | 28 | 在 Markdown 里使用 [nunjucks](nunjucks) 模板语法。 29 | 30 | 通过 `yuque.yml` 设置变量: 31 | 32 | ```yaml 33 | template: 34 | variables: 35 | name: Tom 36 | ``` 37 | 38 | 然后可以直接在文档里使用变量: 39 | 40 | ```markdown 41 | Hello {{ name }}! 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/upload.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - name: npm install 21 | run: npm install 22 | - name: upload 23 | run: | 24 | ./bin/run upload -t ${YUQUE_TOKEN} 25 | if: runner.os == 'Linux' 26 | env: 27 | YUQUE_TOKEN: ${{ secrets.YUQUE_TOKEN }} 28 | - name: upload 29 | run: | 30 | ./bin/run upload -t %YUQUE_TOKEN% 31 | if: runner.os == 'Windows' 32 | env: 33 | YUQUE_TOKEN: ${{ secrets.YUQUE_TOKEN }} 34 | -------------------------------------------------------------------------------- /src/Config.ts: -------------------------------------------------------------------------------- 1 | import { safeLoad } from 'js-yaml'; 2 | import { readFileSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | 5 | export interface IConfig { 6 | pattern: string; 7 | ignore: string; 8 | repo: string; 9 | promote: boolean; 10 | summary: string; 11 | template: 12 | | boolean 13 | | { 14 | variables: { [key: string]: any }; 15 | }; 16 | } 17 | 18 | export type UserConfig = IConfig; 19 | 20 | export function load(configFile: string) { 21 | const defaultConfig: Partial = { 22 | pattern: '**/*.md', 23 | ignore: 'node_modules/**/*', 24 | summary: 'summary.md', 25 | promote: true, 26 | template: false, 27 | }; 28 | 29 | let userConfig = safeLoad(readFileSync(resolve(process.cwd(), configFile), 'utf8')) as UserConfig; 30 | 31 | const config = { ...defaultConfig, ...userConfig }; 32 | 33 | return config; 34 | } 35 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # 配置说明 2 | 3 | | 配置 | 说明 | 默认值 | 4 | | -------- | -------------------------------------------------------------------- | ----------------- | 5 | | repo | 语雀文档库名,以本文档为例,repo 就是 waque/docs | - | 6 | | pattern | 要上传的 markdown 文件,语法参考 https://github.com/isaacs/node-glob | `**/*.md` | 7 | | ignore | 要忽略的文件,语法同上 | `node_modules/**/*` | 8 | | summary | 目录文件 | `summary.md` | 9 | | layout | layout 文件,详见:[使用 layout](tips#使用-layout) | `layout.md` | 10 | | template | 启用模板功能 | `false` | 11 | | promote | 支持瓦雀 | `true` | 12 | -------------------------------------------------------------------------------- /docs/tips.md: -------------------------------------------------------------------------------- 1 | # 使用技巧 2 | 3 | ## 设置目录结构 4 | 5 | 瓦雀支持通过 summary.md 文件来设置知识库的目录结构,在文档目录下创建 `summary.md` 即可,以本文档为例: 6 | 7 | ```markdown 8 | - [开始使用](getting-started) 9 | - [配置说明](configuration) 10 | - [更新日志](changelog) 11 | ``` 12 | 13 | ## 使用 layout 14 | 15 | 如果想在每个文档里都插入同样的内容,可以在目录下新建 layout.md 文件,也可以通过配置文件指定。内容如下: 16 | 17 | ```markdown 18 | {{ content | safe }} 19 | 20 | 这里的公共的内容 21 | ``` 22 | 23 | layout 里同样可以使用 nunjucks 模板语法,除了可以访问在配置文件中设置的变量以外,还可以访问下面这些文档相关的变量: 24 | 25 | | 变量名 | 说明 | 26 | | ----- | ---- | 27 | | filename | 文件名 | 28 | | path | 文件路径 | 29 | | slug | 文档 URL | 30 | | title | 文档标题 | 31 | | content | 文档内容 | 32 | | public | 是否私密 | 33 | 34 | ## 使用 git hook 自动同步文档 35 | 36 | 通过 `husk` 和 `lint-staged` 这两个工具的组合,你还可以只同步当前 commit 修改过的文档 37 | 38 | ```bash 39 | $ npm i husky lint-staged --save-dev 40 | ``` 41 | 42 | 然后在 `package.json` 里加入以下配置: 43 | 44 | ```json 45 | "scripts": { 46 | "precommit": "lint-staged" 47 | }, 48 | "lint-staged": { 49 | "**/*.md": [ 50 | "waque upload" 51 | ] 52 | } 53 | ``` 54 | 55 | 配置好后瓦雀会自动更新每次 commit 修改过的文档。 56 | -------------------------------------------------------------------------------- /src/User.ts: -------------------------------------------------------------------------------- 1 | import { join } from './path'; 2 | import { existsSync, readFileSync, writeFileSync } from 'fs'; 3 | import LarkClient from './LarkClient'; 4 | 5 | export default class User { 6 | config: any; 7 | cacheFile: string; 8 | id?: string; 9 | token?: string; 10 | name?: string; 11 | lark: LarkClient; 12 | 13 | constructor(config: any, token?: string) { 14 | this.config = config; 15 | this.token = token; 16 | this.lark = new LarkClient(config, this); 17 | this.cacheFile = join(this.config.cacheDir, 'user'); 18 | } 19 | 20 | async load() { 21 | if (this.token) { 22 | await this.initByToken(false); 23 | } else if (existsSync(this.cacheFile)) { 24 | const { id, token } = JSON.parse(readFileSync(this.cacheFile, 'utf-8')); 25 | this.token = token; 26 | this.id = id; 27 | } 28 | return this; 29 | } 30 | 31 | async initByToken(save = true) { 32 | const user = await this.lark.getUser(); 33 | this.id = user.id; 34 | this.name = user.name; 35 | if (save) { 36 | this.save({ id: this.id, token: this.token }); 37 | } 38 | } 39 | 40 | save(user: any) { 41 | writeFileSync(this.cacheFile, JSON.stringify(user)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # 什么是瓦雀 2 | 3 | > 双双瓦雀行书案,点点杨花入砚池。 —— 元 叶李《暮春即事》 4 | 5 | 瓦雀可以帮你把本地的文档(markdown)目录发布到语雀上。 6 | 7 | 如果你想要... 8 | 9 | - 返璞归真,使用 markdown; 10 | - 选择自己喜欢的编辑器; 11 | - 把文档维护在 GitHub 上; 12 | 13 | 瓦雀是你居家旅行,编写文档的必备工具。 14 | 15 | > 注:文档同步是单向的,同步的文档不能再在语雀上编辑。 16 | 17 | ## 安装瓦雀 18 | 19 | ```bash 20 | $ npm i -g waque 21 | ``` 22 | 23 | ## 登录语雀 24 | 25 | ```bash 26 | $ waque login 27 | ``` 28 | 29 | ## 初始化配置 30 | 31 | 在文档目录下运行下面的命令生成瓦雀的配置文件 `yuque.yml`。 32 | 33 | 这个命令会要求你输入语雀知识库的名字和要上传的文档,可以参考[配置说明](configuration)来设置。 34 | 35 | ```bash 36 | $ waque init 37 | ``` 38 | 39 | ## 上传文档 40 | 41 | 使用下面的命令来上传文档,瓦雀会把文件名作为语雀上文档的 URL,所以文件名只能包含字母、数字、`_`和`-`(除非在文档里指定了 URL)。 42 | 43 | ```bash 44 | $ waque upload 45 | ``` 46 | 47 | 也可以指定文件上传。 48 | 49 | ```bash 50 | $ waque upload foo.md bar.md 51 | ``` 52 | 53 | ## Watch 模式 54 | 55 | `-w` 参数可以开启 watch 模式。 56 | 57 | ```bash 58 | $ waque upload -w 59 | ``` 60 | 61 | ## 指定配置文件 62 | 63 | 除了使用默认的配置文件 `yuque.yml` 外,也可以通过 `-c` 参数指定其他配置文件。 64 | 65 | ```bash 66 | $ waque upload -c config.yml 67 | ``` 68 | 69 | ## 从已有仓库导出文档 70 | 71 | 如果你要把已有的仓库改用瓦雀管理,那么你可以用下面的命令先把文档导出成 markdown。 72 | 73 | 默认导出到当前目录下,如果指定目录,则目录要先存在。 74 | 75 | ```bash 76 | $ waque export [DIR] 77 | ``` 78 | 79 | [导出再上传可能碰到的问题](faq) 80 | 81 | ## 谁在使用 82 | 83 | - [《Ant Design 实战教程》](https://www.yuque.com/ant-design/course) 84 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import {Command, flags} from '@oclif/command'; 2 | import * as signale from 'signale'; 3 | import User from './User'; 4 | import { load } from './Config'; 5 | 6 | export default abstract class extends Command { 7 | static flags = { 8 | token: flags.string({ 9 | char: 't', 10 | env: 'YUQUE_TOKEN', 11 | }), 12 | 13 | config: flags.string({ 14 | char: 'c', 15 | default: 'yuque.yml', 16 | }) 17 | }; 18 | 19 | flags?: { 20 | env: 'yuque'; 21 | token: string; 22 | config: string; 23 | watch?: boolean; 24 | }; 25 | 26 | args?: { 27 | [key: string]: string; 28 | }; 29 | 30 | config: any; 31 | 32 | async init() { 33 | // do some initialization 34 | const { args, flags } = this.parse(); 35 | this.flags = flags as any; 36 | this.args = args; 37 | if (this.id !== 'login') { 38 | await this.loadUser(); 39 | } 40 | if (['export', 'upload'].includes(this.id!)) { 41 | this.loadConfig(); 42 | } 43 | } 44 | 45 | loadConfig() { 46 | this.config.lark = load(this.flags!.config); 47 | } 48 | 49 | async loadUser() { 50 | const user = new User(this.config, this.flags!.token); 51 | await user.load(); 52 | if (!user.token) { 53 | signale.error('请先使用 waque login 登录语雀'); 54 | process.exit(1); 55 | } 56 | this.config.currentUser = user; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 瓦雀 2 | 3 | > 双双瓦雀行书案,点点杨花入砚池。 —— 元 叶李《暮春即事》 4 | 5 | 瓦雀可以帮你把本地的文档(markdown)目录发布到语雀上。 6 | 7 | 如果你想要... 8 | 9 | - 返璞归真,使用 markdown; 10 | - 选择自己喜欢的编辑器; 11 | - 把文档维护在 GitHub 上; 12 | 13 | 瓦雀是你居家旅行,编写文档的必备工具。 14 | 15 | > 注:文档同步是单向的,同步的文档不能再在语雀上编辑 16 | 17 | - [瓦雀](#%e7%93%a6%e9%9b%80) 18 | - [安装瓦雀](#%e5%ae%89%e8%a3%85%e7%93%a6%e9%9b%80) 19 | - [登录语雀](#%e7%99%bb%e5%bd%95%e8%af%ad%e9%9b%80) 20 | - [初始化配置](#%e5%88%9d%e5%a7%8b%e5%8c%96%e9%85%8d%e7%bd%ae) 21 | - [上传文档](#%e4%b8%8a%e4%bc%a0%e6%96%87%e6%a1%a3) 22 | - [从已有仓库导出文档](#%e4%bb%8e%e5%b7%b2%e6%9c%89%e4%bb%93%e5%ba%93%e5%af%bc%e5%87%ba%e6%96%87%e6%a1%a3) 23 | - [谁在使用](#%e8%b0%81%e5%9c%a8%e4%bd%bf%e7%94%a8) 24 | 25 | ## 安装瓦雀 26 | 27 | ```bash 28 | $ npm i -g waque 29 | ``` 30 | 31 | ## 登录语雀 32 | 33 | ```bash 34 | $ waque login 35 | ``` 36 | 37 | ## 初始化配置 38 | 39 | 在文档目录下运行下面的命令生成瓦雀的配置文件 `yuque.yml`。 40 | 41 | 这个命令会要求你输入语雀知识库的名字和要上传的文档,可以参考[配置说明](docs/configuration.md)来设置。 42 | 43 | ```bash 44 | $ waque init 45 | ``` 46 | 47 | ## 上传文档 48 | 49 | 使用下面的命令来上传文档,瓦雀会把文件名作为语雀上文档的 URL,所以文件名只能包含字母、数字、`_`和`-`(除非在文档里指定了 URL)。 50 | 51 | ```bash 52 | $ waque upload 53 | ``` 54 | 55 | 也可以指定文件上传。 56 | 57 | ```bash 58 | $ waque upload foo.md bar.md 59 | ``` 60 | 61 | ## 从已有仓库导出文档 62 | 63 | 如果你要把已有的仓库改用瓦雀管理,那么你可以用下面的命令先把文档导出成 markdown。 64 | 65 | 默认导出到当前目录下,如果指定目录,则目录要先存在。 66 | 67 | ```bash 68 | $ waque export [DIR] 69 | ``` 70 | 71 | [导出再上传可能碰到的问题](docs/faq.md) 72 | 73 | ## 谁在使用 74 | 75 | - [《Ant Design 实战教程》](https://www.yuque.com/ant-design/course) 76 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'fs'; 2 | import { prompt, registerPrompt } from 'inquirer'; 3 | import * as signale from 'signale'; 4 | import LarkClient from '../LarkClient'; 5 | import Base from '../base'; 6 | import { resolve } from '../path'; 7 | 8 | registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); 9 | 10 | export default class Init extends Base { 11 | static description = 'generate yuque.yml'; 12 | 13 | static flags = Base.flags; 14 | 15 | config: any; 16 | 17 | async run() { 18 | const configFile = resolve('./yuque.yml'); 19 | if (existsSync(configFile)) { 20 | signale.error('yuque.yml 已存在.'); 21 | this.exit(1); 22 | } 23 | 24 | const lark = new LarkClient(this.config, this.config.currentUser); 25 | 26 | const repos = await lark.getRepos(); 27 | 28 | const questions = [ 29 | { 30 | name: 'repo', 31 | message: '选择语雀知识库', 32 | type: 'autocomplete', 33 | source: (_answersSoFar: string, input: string) => { 34 | let repoNames = repos 35 | .filter((repo: any) => !!repo.user) 36 | .map((repo: any) => `${repo.user.name}/${repo.name}(${repo.namespace})`); 37 | 38 | if (input) { 39 | repoNames = repoNames.filter((name: string) => name.includes(input)); 40 | } 41 | 42 | return Promise.resolve(repoNames); 43 | }, 44 | filter: (input: string) => input.match(/\((.+\/.+)\)/)![1], 45 | validate: (input: string) => !!input 46 | }, 47 | { 48 | name: 'pattern', 49 | message: '要上传的文件', 50 | default: '**/*.md' 51 | } 52 | ]; 53 | 54 | const { repo, pattern } = (await prompt(questions)) as any; 55 | 56 | writeFileSync( 57 | configFile, 58 | ` 59 | # 配置请参考:https://www.yuque.com/waquehq/docs/configuration 60 | repo: '${repo}' 61 | pattern: '${pattern}' 62 | `.trimLeft()); 63 | 64 | signale.success('Created yuque.yml'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/hooks/init/checkUpdate.ts: -------------------------------------------------------------------------------- 1 | import {Hook} from '@oclif/config'; 2 | import Chalk from 'chalk'; 3 | import {spawn} from 'child_process'; 4 | import * as fs from 'fs-extra'; 5 | import * as semver from 'semver'; 6 | import * as path from '../../path'; 7 | 8 | const debug = require('debug')('update-check'); 9 | 10 | const hook: Hook<'init'> = async function ({config}) { 11 | const file = path.join(config.cacheDir, 'version'); 12 | 13 | const checkVersion = async () => { 14 | try { 15 | const distTags = await fs.readJSON(file); 16 | if (config.version.includes('-')) { 17 | // TODO: handle channels 18 | return; 19 | } 20 | if (distTags && distTags.latest && semver.gt(distTags.latest.split('-')[0], config.version.split('-')[0])) { 21 | const chalk: typeof Chalk = require('chalk'); 22 | this.log(` 23 | 发现新版本,运行 ${chalk.greenBright(`npm i ${config.name} -g`)} 更新瓦雀到最新版。 24 | 更新日志: https://www.yuque.com/waque/docs/changelog\n 25 | `); 26 | } 27 | } catch (err: any) { 28 | if (err.code !== 'ENOENT') throw err; 29 | } 30 | }; 31 | 32 | const refreshNeeded = async () => { 33 | try { 34 | const cfg = (config.pjson.oclif as any)['warn-if-update-available'] || {}; 35 | const timeoutInDays = cfg.timeoutInDays || 0.5; 36 | const {mtime} = await fs.stat(file); 37 | const staleAt = new Date(mtime.valueOf() + 1000 * 60 * 60 * 24 * timeoutInDays); 38 | return staleAt < new Date(); 39 | } catch (err) { 40 | debug(err); 41 | return true; 42 | } 43 | }; 44 | 45 | const spawnRefresh = async () => { 46 | debug('spawning version refresh'); 47 | spawn( 48 | process.execPath, 49 | [path.join(__dirname, '../../../lib/getVersion'), config.name!, file, config.version!, config.npmRegistry!], 50 | { 51 | detached: !config.windows, 52 | stdio: 'ignore', 53 | } 54 | ).unref(); 55 | }; 56 | 57 | await checkVersion(); 58 | if (await refreshNeeded()) await spawnRefresh(); 59 | }; 60 | 61 | export default hook; 62 | -------------------------------------------------------------------------------- /src/commands/export.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import { mkdirpSync } from 'fs-extra'; 3 | import * as signale from 'signale'; 4 | import LarkClient from '../LarkClient'; 5 | import Base from '../base'; 6 | import { resolve, join } from '../path'; 7 | 8 | function times(n: number, s: string) { 9 | return Array(n) 10 | .fill(s) 11 | .join(''); 12 | } 13 | 14 | export default class Export extends Base { 15 | static description = 'export docs'; 16 | 17 | static flags = Base.flags; 18 | 19 | static args = [ 20 | { 21 | name: 'dir', 22 | default: 'exports', 23 | }, 24 | ]; 25 | 26 | config: any; 27 | 28 | async run() { 29 | const { args } = this.parse(); 30 | const lark = new LarkClient(this.config, this.config.currentUser); 31 | const docs = await lark.getDocs(); 32 | const dir = resolve(args.dir); 33 | 34 | mkdirpSync(dir); 35 | 36 | await Promise.all(docs.map(async (doc: any) => { 37 | const docDetail = await lark.getDoc(doc.id); 38 | const filename = docDetail.title.trim(); 39 | const file = `${filename}.md`; 40 | let content = []; 41 | const url = docDetail.slug === filename ? null : docDetail.slug; 42 | const isPublic = docDetail.public === 1 ? null : docDetail.public; 43 | if (url || isPublic) { 44 | content.push('---'); 45 | if (url) { 46 | content.push(`url: ${url}`); 47 | } 48 | if (isPublic) { 49 | content.push(`public: ${isPublic}`); 50 | } 51 | content.push('---\n'); 52 | } 53 | content.push(`# ${docDetail.title.trim()}\n`); 54 | content.push(docDetail.body); 55 | writeFileSync(join(dir, file.replace(/[\/ ]/g, '-')), content.join('\n')); 56 | signale.success(`Exported ${file}`); 57 | })); 58 | const repo = await lark.getRepo(); 59 | if (repo.toc) { 60 | const toc = await lark.getRepoToc(); 61 | const content = toc.map((doc: any) => { 62 | return `${times(doc.depth - 1, ' ')}- [${doc.title}](${doc.slug})`; 63 | }).concat(['\n']).join('\n'); 64 | writeFileSync(join(dir, 'summary.md'), content); 65 | signale.success('Exported summary.md'); 66 | } 67 | signale.success(`Exported to ${resolve(dir)}`); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/LarkClient.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import User from './User'; 3 | import { join } from './path'; 4 | 5 | require('axios-debug-log')({ 6 | request(debug: any, config: any) { 7 | debug('Request ' + config.url); 8 | }, 9 | response(debug: any, response: any) { 10 | debug( 11 | 'Response with ' + response.status, 12 | 'from ' + response.config.url 13 | ); 14 | }, 15 | error(debug: any, error: any) { 16 | debug('Boom', error); 17 | } 18 | }); 19 | 20 | export default class LarkClient { 21 | static async login(user: any) { 22 | const { data } = await axios.post('/authorize', user); 23 | return data.data; 24 | } 25 | 26 | config: any; 27 | user: User; 28 | client: AxiosInstance; 29 | 30 | constructor(config: any, user: User) { 31 | this.config = config; 32 | this.user = user; 33 | 34 | const baseURL = 'https://www.yuque.com/api/v2'; 35 | 36 | this.client = axios.create({ 37 | baseURL, 38 | headers: { 39 | 'X-Auth-Token': user.token 40 | }, 41 | }); 42 | } 43 | 44 | repoPath(path: string) { 45 | return join('/repos', this.config.lark.repo, path); 46 | } 47 | 48 | createDoc(doc: any) { 49 | return this.client.post(this.repoPath('docs'), doc) as Promise; 50 | } 51 | 52 | updateDoc(id: number, doc: any) { 53 | return this.client.put(this.repoPath(`/docs/${id}`), doc) as Promise; 54 | } 55 | 56 | async getDoc(id: number | string) { 57 | const { data } = await this.client.get(this.repoPath(`/docs/${id}?raw=1`)); 58 | return data.data; 59 | } 60 | 61 | async getDocs() { 62 | const { data } = await this.client.get(this.repoPath('/docs')); 63 | return data.data; 64 | } 65 | 66 | updateRepo(repo: any) { 67 | this.client.put(this.repoPath('/'), repo); 68 | } 69 | 70 | async getRepo() { 71 | const { data } = await this.client.get(this.repoPath('/')); 72 | return data.data; 73 | } 74 | 75 | async getRepoToc() { 76 | const { data } = await this.client.get(this.repoPath('/toc')); 77 | return data.data; 78 | } 79 | 80 | async getRepos() { 81 | const { data } = await this.client.get(`/users/${this.user.id}/repos`); 82 | return data.data; 83 | } 84 | 85 | async getUser() { 86 | const { data } = await this.client.get('/user'); 87 | return data.data; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waque", 3 | "version": "1.13.1", 4 | "author": "Wei Zhu ", 5 | "bin": { 6 | "waque": "./bin/run" 7 | }, 8 | "bugs": "https://www.yuque.com/waquehq/topics", 9 | "dependencies": { 10 | "@oclif/command": "^1", 11 | "@oclif/config": "^1", 12 | "@oclif/errors": "^1.1.2", 13 | "@oclif/plugin-help": "^1", 14 | "@types/form-data": "^2.2.1", 15 | "@types/fs-extra": "^5.0.2", 16 | "@types/inquirer": "^0.0.41", 17 | "@types/mkdirp": "^0.5.2", 18 | "@types/nunjucks": "^3.0.0", 19 | "@types/semver": "^5.5.0", 20 | "axios": "^0.18.0", 21 | "axios-debug-log": "^0.4.0", 22 | "chalk": "^2.4.1", 23 | "chokidar": "^3.3.1", 24 | "debug": "^3.1.0", 25 | "form-data": "^2.3.2", 26 | "fs-extra": "^6.0.1", 27 | "glob": "^7.1.2", 28 | "inquirer": "^5.2.0", 29 | "inquirer-autocomplete-prompt": "^0.12.2", 30 | "js-yaml": "^3.11.0", 31 | "lodash": "^4.17.10", 32 | "mkdirp": "^0.5.1", 33 | "nunjucks": "^3.1.3", 34 | "remark": "^9.0.0", 35 | "semver": "^5.5.0", 36 | "sha1-file": "^1.0.1", 37 | "signale": "^1.1.0", 38 | "slash2": "^2.0.0", 39 | "tslib": "^1", 40 | "unist-util-visit": "^1.3.1", 41 | "yaml-front-matter": "^4.0.0", 42 | "yuque-auth": "^1.0.0" 43 | }, 44 | "devDependencies": { 45 | "@commitlint/cli": "^6.2.0", 46 | "@commitlint/config-conventional": "^6.1.3", 47 | "@oclif/dev-cli": "^1.13.22", 48 | "@oclif/test": "^1", 49 | "@oclif/tslint": "^1", 50 | "@types/chai": "^4", 51 | "@types/glob": "^5.0.35", 52 | "@types/js-yaml": "^3.11.1", 53 | "@types/mocha": "^5", 54 | "@types/node": "^10.17.60", 55 | "chai": "^4", 56 | "globby": "^8.0.1", 57 | "husky": "^0.14.3", 58 | "lint-staged": "^7.1.2", 59 | "mocha": "^5", 60 | "nyc": "^11", 61 | "standard-version": "^8.0.1", 62 | "ts-node": "^6", 63 | "tslint": "^5", 64 | "typescript": "^4.5.2" 65 | }, 66 | "engines": { 67 | "node": ">=8.0.0" 68 | }, 69 | "files": [ 70 | "/bin", 71 | "/lib", 72 | "/oclif.manifest.json" 73 | ], 74 | "homepage": "https://www.yuque.com/waque", 75 | "keywords": [ 76 | "oclif" 77 | ], 78 | "license": "MIT", 79 | "main": "lib/index.js", 80 | "oclif": { 81 | "commands": "./lib/commands", 82 | "bin": "waque", 83 | "plugins": [ 84 | "@oclif/plugin-help" 85 | ], 86 | "hooks": { 87 | "init": [ 88 | "./lib/hooks/init/createDirs", 89 | "./lib/hooks/init/checkUpdate" 90 | ] 91 | } 92 | }, 93 | "repository": "https://github.com/yesmeck/waque", 94 | "scripts": { 95 | "precommit": "lint-staged", 96 | "postpack": "rm -f oclif.manifest.json", 97 | "posttest": "tsc -p test --noEmit && tslint -p test -t stylish", 98 | "prepack": "rm -rf lib && tsc && oclif-dev manifest", 99 | "test": "nyc mocha --forbid-only \"test/**/*.test.ts\"", 100 | "commitmsg": "commitlint -e $GIT_PARAMS", 101 | "release": "standard-version" 102 | }, 103 | "standard-version": { 104 | "skip": { 105 | "changelog": true 106 | } 107 | }, 108 | "lint-staged": { 109 | "docs/**/*.md": [ 110 | "./bin/run upload" 111 | ] 112 | }, 113 | "tnpm": { 114 | "mode": "yarn", 115 | "lockfile": "enable" 116 | }, 117 | "types": "lib/index.d.ts" 118 | } 119 | -------------------------------------------------------------------------------- /src/Document.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import * as yamlFront from 'yaml-front-matter'; 3 | import * as nunjucks from 'nunjucks'; 4 | import { IConfig } from './Config'; 5 | import LarkClient from './LarkClient'; 6 | import { basename, resolve } from './path'; 7 | 8 | const ASSETS_BEGIN = '[comment]: <> (waque assets)'; 9 | const ASSETS_END = '[comment]: <> (waque assets end)'; 10 | 11 | export interface DocumentConfig { 12 | url?: string; 13 | public?: number; 14 | template?: boolean; 15 | __content: string; 16 | } 17 | 18 | export default class Document { 19 | filename: string; 20 | config: IConfig; 21 | body!: string; 22 | title!: string; 23 | slug!: string; 24 | public?: number; 25 | template?: boolean; 26 | lark: LarkClient; 27 | raw: string; 28 | rendered?: string; 29 | assets: { 30 | hash: string; 31 | url: string; 32 | }[]; 33 | larkDocs: any; 34 | id?: number; 35 | 36 | constructor(larkDocs: any, lark: LarkClient, config: IConfig, filename: string) { 37 | this.larkDocs = larkDocs; 38 | this.lark = lark; 39 | this.config = config; 40 | this.filename = filename; 41 | this.assets = []; 42 | this.raw = readFileSync(resolve(this.filename)).toString(); 43 | } 44 | 45 | async createDoc(layout?: string) { 46 | // load config 47 | this.loadConfig(); 48 | // set id 49 | const larkDoc: any = this.larkDocs.find((ld: any) => ld.slug === this.slug); 50 | if (larkDoc) { 51 | this.id = larkDoc.id; 52 | } 53 | // render template 54 | this.renderTemplate(); 55 | 56 | let title = ''; 57 | let body: any = []; 58 | let prevLine = ''; 59 | const lines = this.rendered!.split('\n'); 60 | for (const line of lines) { 61 | if (!title) { 62 | if (line.startsWith('# ')) { 63 | title = line.replace('# ', ''); 64 | continue; 65 | } 66 | if (line.startsWith('====')) { 67 | title = prevLine; 68 | body.shift(); 69 | continue; 70 | } 71 | } 72 | prevLine = line; 73 | body.push(line); 74 | } 75 | if (this.assets.length > 0) { 76 | body.push(''); 77 | body.push(ASSETS_BEGIN); 78 | this.assets.forEach(asset => body.push(`[comment]: <> (${asset.hash}: ${asset.url})`)); 79 | body.push(ASSETS_END); 80 | } 81 | 82 | body = body.join('\n').trim(); 83 | 84 | this.title = title.trim(); 85 | this.body = body; 86 | this.applyLayout(layout); 87 | 88 | if (this.config.promote) { 89 | this.body = this.body + this.signature(); 90 | } 91 | 92 | return this; 93 | } 94 | 95 | signature() { 96 | return '\n\n---\n 本文档由[瓦雀](https://www.yuque.com/waquehq)创建'; 97 | } 98 | 99 | applyLayout(layout?: string) { 100 | if (!layout) { 101 | return; 102 | } 103 | let variables = { 104 | slug: this.slug, 105 | title: this.title, 106 | content: this.body, 107 | public: this.public, 108 | filename: basename(this.filename), 109 | path: this.filename.replace(process.cwd(), ''), 110 | }; 111 | let tags; 112 | if (this.config.template) { 113 | variables = { 114 | ...variables, 115 | ...(this.config.template as any).variables 116 | }; 117 | tags = (this.config.template as any).tags; 118 | } 119 | const env = nunjucks.configure({ tags }); 120 | this.body = env.renderString(readFileSync(resolve(layout)).toString(), variables); 121 | } 122 | 123 | getTemplate() { 124 | if (this.template === false) { 125 | return false; 126 | } 127 | if (this.template === true || this.config.template) { 128 | return { 129 | variables: (this.config.template as any).variables || {}, 130 | tags: (this.config.template as any).tags 131 | }; 132 | } 133 | return false; 134 | } 135 | 136 | renderTemplate() { 137 | const template = this.getTemplate(); 138 | if (template) { 139 | const env = nunjucks.configure({ tags: template.tags }); 140 | this.rendered = env.renderString(this.rendered!, template.variables); 141 | } 142 | } 143 | 144 | loadConfig() { 145 | const config: DocumentConfig = yamlFront.loadFront(this.raw); 146 | this.slug = 147 | config.url || 148 | basename(this.filename, '.md') 149 | .toLowerCase() 150 | .replace(/\s/g, '-'); 151 | this.template = config.template; 152 | this.rendered = config.__content; 153 | this.public = config.public; 154 | } 155 | 156 | dump() { 157 | return { 158 | slug: this.slug, 159 | title: this.title, 160 | body: this.body, 161 | public: this.public 162 | }; 163 | } 164 | 165 | validate() { 166 | const result: { 167 | valid: boolean; 168 | messages: string[]; 169 | } = { 170 | valid: true, 171 | messages: [], 172 | }; 173 | if (!this.title) { 174 | result.valid = false; 175 | result.messages.push('缺少文章标题'); 176 | } 177 | if (!/\w+/.test(this.slug)) { 178 | result.valid = false; 179 | result.messages.push('文件名只能是字母、数字、_和-'); 180 | } 181 | return result; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/commands/upload.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import * as signale from 'signale'; 3 | import { flags } from '@oclif/command'; 4 | import chokidar from 'chokidar'; 5 | import { readFileSync, existsSync } from 'fs'; 6 | import { times, compact } from 'lodash'; 7 | import LarkClient from '../LarkClient'; 8 | import Document from '../Document'; 9 | import Base from '../base'; 10 | import { basename, resolve } from '../path'; 11 | 12 | export default class Upload extends Base { 13 | static description = 'upload docs'; 14 | 15 | static flags: any = { 16 | ...Base.flags, 17 | watch: flags.boolean({ 18 | char: 'w', 19 | }), 20 | }; 21 | 22 | static args = times(1000, Number).map((i) => ({ name: `file${i}` })); 23 | 24 | static strict = false; 25 | 26 | config: any; 27 | lark!: LarkClient; 28 | pattern?: string; 29 | 30 | async run() { 31 | this.lark = new LarkClient(this.config, this.config.currentUser); 32 | this.pattern = this.config.lark.pattern; 33 | const args = compact(Object.values(this.args!)); 34 | if (args.length === 1) { 35 | this.pattern = args[0]; 36 | } else if (args.length > 1) { 37 | this.pattern = `{${args.join(',')}}`; 38 | } 39 | 40 | let foundSummary; 41 | let foundLayout = glob.sync(this.config.lark.pattern).find((f) => this.isLayout(f)); 42 | if (foundLayout) { 43 | signale.info('发现 layout.md'); 44 | } 45 | 46 | const larkDocs = await this.lark.getDocs(); 47 | 48 | const docs = await Promise.all( 49 | glob 50 | .sync(this.pattern!, { ignore: this.config.lark.ignore }) 51 | .filter((filename) => { 52 | if (this.isSummary(filename)) { 53 | signale.info(`发现 ${filename}`); 54 | foundSummary = filename; 55 | return false; 56 | } 57 | if (this.isLayout(filename)) { 58 | return false; 59 | } 60 | return true; 61 | }) 62 | .map((filename) => { 63 | const doc = new Document(larkDocs, this.lark, this.config.lark, filename); 64 | return doc.createDoc(foundLayout); 65 | }), 66 | ); 67 | 68 | let hasError = false; 69 | 70 | await Promise.all( 71 | docs.map((doc) => { 72 | const result = doc.validate(); 73 | if (!result.valid) { 74 | signale.error(`${doc.filename} ${result.messages.join('|')}`); 75 | hasError = true; 76 | return; 77 | } 78 | if (doc.id) { 79 | this.debug('Update yuque doc %s', doc.title); 80 | return this.lark 81 | .updateDoc(doc.id, doc.dump()) 82 | .then(() => { 83 | signale.success(`更新 ${doc.title}[${doc.slug}]`); 84 | }) 85 | .catch((error) => { 86 | signale.error(`更新 ${doc.title}[${doc.slug}]`); 87 | hasError = true; 88 | this.log(error.response.data); 89 | }); 90 | } else { 91 | this.debug('Create yuque doc %s', doc.title); 92 | return this.lark 93 | .createDoc(doc.dump()) 94 | .then(() => { 95 | signale.success(`创建 ${doc.title}[${doc.slug}]`); 96 | }) 97 | .catch((error) => { 98 | signale.error(`创建 ${doc.title}[${doc.slug}]`); 99 | hasError = true; 100 | this.log(error.response.data); 101 | }); 102 | } 103 | }), 104 | ); 105 | this.updateToc(foundSummary); 106 | 107 | if (this.flags!.watch) { 108 | this.startWatch(); 109 | signale.success('watch for changes...'); 110 | } else { 111 | if (hasError) { 112 | this.exit(1); 113 | } 114 | } 115 | } 116 | 117 | updateToc(foundSummary?: string) { 118 | const summaryFile = foundSummary || resolve(this.config.lark.summary); 119 | if (existsSync(summaryFile)) { 120 | const summary = readFileSync(summaryFile).toString(); 121 | this.lark.updateRepo({ toc: summary }); 122 | signale.success('更新目录'); 123 | } 124 | } 125 | 126 | isSummary(filename: string) { 127 | return basename(filename).toLowerCase() === 'summary.md'; 128 | } 129 | 130 | isLayout(filename: string) { 131 | return basename(filename).toLowerCase() === 'layout.md'; 132 | } 133 | 134 | startWatch() { 135 | const watcher = chokidar.watch(this.pattern!, { 136 | ignored: this.config.lark.ignore, 137 | ignoreInitial: true, 138 | }); 139 | 140 | watcher 141 | .on('add', (path) => this.handleDoc(path)) 142 | .on('change', (path) => this.handleDoc(path)); 143 | } 144 | 145 | async handleDoc(filename: string) { 146 | if (this.isSummary(filename)) { 147 | this.updateToc(filename); 148 | return; 149 | } 150 | if (this.isLayout(filename)) { 151 | return; 152 | } 153 | 154 | const larkDocs = await this.lark.getDocs(); 155 | const doc = new Document(larkDocs, this.lark, this.config.lark, filename); 156 | 157 | const foundLayout = glob.sync(this.config.lark.pattern).find((f) => this.isLayout(f)); 158 | doc.createDoc(foundLayout); 159 | const result = doc.validate(); 160 | if (!result.valid) { 161 | signale.error(`${doc.filename} ${result.messages.join('|')}`); 162 | return; 163 | } 164 | if (doc.id) { 165 | this.debug('Update yuque doc %s', doc.title); 166 | return this.lark 167 | .updateDoc(doc.id, doc.dump()) 168 | .then(() => { 169 | signale.success(`更新 ${doc.title}[${doc.slug}]`); 170 | }) 171 | .catch((error) => { 172 | signale.error(`更新 ${doc.title}[${doc.slug}]`); 173 | this.log(error.response.data); 174 | }); 175 | } else { 176 | this.debug('Create yuque doc %s', doc.title); 177 | return this.lark 178 | .createDoc(doc.dump()) 179 | .then(() => { 180 | signale.success(`创建 ${doc.title}[${doc.slug}]`); 181 | }) 182 | .catch((error) => { 183 | signale.error(`创建 ${doc.title}[${doc.slug}]`); 184 | this.log(error.response.data); 185 | }); 186 | } 187 | } 188 | } 189 | --------------------------------------------------------------------------------