diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..867d041a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.less] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..bd87608d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,94 @@ +const eslintrc = { + "parser": "babel-eslint", + "extends": "airbnb", + "env": { + "browser": true, + "node": true, + "es6": true, + "mocha": true, + "jest": true, + "jasmine": true + }, + "plugins": [ + "react", + "import" + ], + "parserOptions": { + parser: 'babel-eslint', + }, + "rules": { + "linebreak-style": 0, + "func-names": 0, + "sort-imports": 0, + "arrow-body-style": 0, + "prefer-destructuring": 0, + "max-len": 0, + "consistent-return": 0, + "comma-dangle": [ + "error", + "always-multiline" + ], + "function-paren-newline": 0, + "class-methods-use-this": 0, + "react/sort-comp": 0, + "react/prop-types": 0, + "react/jsx-first-prop-new-line": 0, + "react/require-extension": 0, + "react/jsx-filename-extension": [ + 1, + { + "extensions": [ + ".js", + ".jsx" + ] + } + ], + "import/extensions": 0, + "import/no-unresolved": 0, + "import/no-extraneous-dependencies": 0, + "import/prefer-default-export": 0, + "jsx-a11y/no-static-element-interactions": 0, + "jsx-a11y/anchor-has-content": 0, + "jsx-a11y/click-events-have-key-events": 0, + "jsx-a11y/anchor-is-valid": 0, + "jsx-a11y/label-has-for": 0, + "jsx-a11y/no-noninteractive-element-interactions": 0, + "jsx-a11y/mouse-events-have-key-events": 0, + "react/no-danger": 0, + "react/jsx-no-bind": 0, + "react/forbid-prop-types": 0, + "react/require-default-props": 0, + "react/no-did-mount-set-state": 0, + "react/no-array-index-key": 0, + "react/no-find-dom-node": 0, + "react/no-unused-state": 0, + "react/no-unused-prop-types": 0, + "react/default-props-match-prop-types": 0, + "react/jsx-curly-spacing": 0, + "react/no-render-return-value": 0, + 'react/jsx-uses-react': 0, + 'react/react-in-jsx-scope': 0, + "object-curly-newline": 0, + "no-param-reassign": 0, + "no-return-assign": 0, + "no-redeclare": 0, + "no-restricted-globals": 0, + "no-restricted-syntax": 0, + "no-underscore-dangle": 0, + "no-unused-expressions": 0, + "no-use-before-define": 0, + "semi": ["error", "never"], + "quotes": 0, + "no-plusplus": 0 + } +} + +if (process.env.NODE_ENV === 'development') { + Object.assign(eslintrc.rules, + { + 'no-console': 0, + 'no-unused-vars': 0, + }); +} + +module.exports = eslintrc diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..036ca2d3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: ['http://muyunyun.cn/sponsor/'] diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..9ce9bb1d --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,2 @@ +todo: + keyword: "@makeAnIssue" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..54947268 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# see https://github.com/yi-Xu-0100/traffic-to-badge/blob/main/.github/dependabot.yml +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'daily' + # Maintain dependencies for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..3603948a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,61 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pinned + - security + - "[Status] Maybe Later" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: wontfix + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed \ No newline at end of file diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 00000000..76c3afbb --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,44 @@ +name: GitHub Pages + +on: + push: + branches: + - main + pull_request: + +jobs: + deploy: + runs-on: ubuntu-22.04 + permissions: + contents: write + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + steps: + - uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '14' + + - name: Get yarn cache + id: yarn-cache + run: echo "YARN_CACHE_DIR=$(yarn cache dir)" >> "${GITHUB_OUTPUT}" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.yarn-cache.outputs.YARN_CACHE_DIR }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - run: yarn install --frozen-lockfile + - run: yarn build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: ${{ github.ref == 'refs/heads/main' }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .crd-dist diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 00000000..1a35382c --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,13 @@ +name: Greetings + +on: [pull_request, issues] + +jobs: + greeting: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Welcome your first issue here, thanks' + pr-message: 'Welcome your first pr here, thanks' diff --git a/.github/workflows/traffic2badge.yml b/.github/workflows/traffic2badge.yml new file mode 100644 index 00000000..7746f381 --- /dev/null +++ b/.github/workflows/traffic2badge.yml @@ -0,0 +1,59 @@ +name: traffic2badge +on: + push: + branches: + - main + schedule: + - cron: '1 0 * * *' #UTC + +jobs: + run: + name: Make GitHub Traffic to Badge + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2.3.3 + + - name: Get Commit Message + id: message + uses: actions/github-script@v3.0.0 + env: + FULL_COMMIT_MESSAGE: '${{ github.event.head_commit.message }}' + with: + result-encoding: string + script: | + var message = `${process.env.FULL_COMMIT_MESSAGE}`; + core.info(message); + if (message != '') return message; + var time = new Date(Date.now()).toISOString(); + core.info(time); + return `Get traffic data at ${time}`; + + - name: Set Traffic + id: traffic + uses: yi-Xu-0100/traffic-to-badge@v1.4.0 + with: + my_token: ${{ secrets.TRAFFIC_TOKEN }} + #(default) static_list: ${{ github.repository }} + #(default) traffic_branch: traffic + #(default) views_color: brightgreen + #(default) clones_color: brightgreen + #(default) logo: github + #(default) year: + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3.7.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: ${{ steps.traffic.outputs.traffic_branch }} + publish_dir: ${{ steps.traffic.outputs.traffic_path }} + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + full_commit_message: ${{ steps.message.outputs.result }} + + - name: Show Traffic Data + run: | + echo ${{ steps.traffic.outputs.traffic_branch }} + echo ${{ steps.traffic.outputs.traffic_path }} + cd ${{ steps.traffic.outputs.traffic_path }} + ls -a \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1de26e58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +node_modules +.create-react-doc-dist +package-lock.json +.cache +.DS_Store +.crd-dist/ + +*.bak +*.tem +*.log +*.temp +#.swp +*.*~ +~*.* + +docs/忽略文件.md \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..778d87ef --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +.cache +.gitignore +.editorconfig +.create-react-doc-dist +node_modules +package-lock.json +dist \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..188fea96 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +# .npmrc + +registry=https://registry.npmjs.org/ diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..56800745 --- /dev/null +++ b/.yarnrc @@ -0,0 +1,2 @@ +# .yarnrc +registry "https://registry.npmjs.org/" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4819722c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# HOW TO CONTRIBUTE + +1. Welcome your pr! Before pr, talk about situations in the [issue](https://github.com/MuYunyun/create-react-doc/issues/new) firstly. If the situation is reasonable, go to the next step; +2. Switch to the new branch based main, submit the pr to branch `qa/latest` after finishing development. + +## DEV + +Run these bash command firstly. + +```bash +$ git clone https://github.com/MuYunyun/create-react-doc +$ cd create-react-doc +$ yarn && yarn bootstrap && yarn start +``` + +And now you can see the document is running at http://localhost:3000. + +## Test + +After merging pr to qa/latest and publish beta package. You should verify the feature/bugfix with following bash: + +```js +yarn add create-react-doc@beta +``` \ No newline at end of file diff --git a/README-en.md b/README-en.md new file mode 100644 index 00000000..2fd0f917 --- /dev/null +++ b/README-en.md @@ -0,0 +1,113 @@ + _.-"\ + _.-" \ + ,-" \ + \ create \ + \ \ react \ + \ \ doc \ + \ \ _.-; + \ \ _.-" : + \ \,-" _.-" + \( _.-" + `--" + +[![npm version](https://img.shields.io/npm/v/create-react-doc)](https://badge.fury.io/js/create-react-doc) +[![week download](https://img.shields.io/npm/dw/create-react-doc.svg)](https://www.npmjs.com/package/create-react-doc) +![views](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/views.svg) +![views](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/views_per_week.svg) +![clones](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/clones_per_week.svg) +![LICENSE MIT](https://img.shields.io/npm/l/create-react-doc.svg) + +English | [简体中文](./README.md) + +# Create React Doc + +[Create React Doc](https://github.com/MuYunyun/create-react-doc) is a markdown document site generation tool using React just like [create-react-app](https://github.com/facebook/create-react-app), developers can use Create React Doc to develop, deploy documents or blog sites without worrying about additional environment configuration information. + +## Features + +* The idea of ​​building a site: Just write markdown files as a blog site [like me](https://github.com/MuYunyun/blog). +* Out of box: One-click generation of documents and blog sites by specifying directories or documents, no need to care about site environment configuration information. +* Performance: greatly improve site loading speed through pre-rendering and lazy loading. +* Based on mdx: Support writing React components, mathematical formulas, etc. in markdown. +* Search engine optimization: Support SEO, making documents easier to search. +* Personalization: Support [custom theme](https://muyunyun.cn/create-react-doc/9f41fc98). +* Workflow: Integrate Github actions, support automated packaging and publishing sites. + +> [Quick Start](https://muyunyun.cn/create-react-doc/290a4219) + +## Subject + +Create React Doc provides the official default theme [crd-seed](https://github.com/MuYunyun/create-react-doc/tree/main/packages/crd-seed). The theme supports the following features: + +* Adapt to mobile and PC multi-terminal display. +* Support dark mode. +* The document supports embedded codepen, codesandbox. +* GitHub linkage. +* Support using tags to customize aggregate article content. + +[my blog](http://muyunyun.cn/blog) is based [crd-seed](https://github.com/MuYunyun/create-react-doc/tree/main/packages/crd-seed) theme to build。 + +![](http://with.muyunyun.cn/90d3e357a31649b9466a828a92b6d88d.jpg) +![](http://with.muyunyun.cn/2e7440e4256debda2d73a4e6392c7146.jpg-300) + +If you want to customize or share personal themes, you can refer to the [Custom Theme](https://muyunyun.cn/create-react-doc/9f41fc98) chapter. + +## Get started quickly + +**Create React Doc** is very easy to use. Developers don't need to install or configure additional tools such as webpack or Babel, they are built-in and hidden in the scaffolding, so developers can concentrate on document writing. + +If you want to create a site file `doc` under the current file, here are three ways to quickly build a site: + +### npx + +```bash +npx create-react-doc doc +``` + +### npm + +```bash +npm init create-react-doc doc +``` + +### yarn + +```bash +yarn create react-doc doc +``` + +![](http://with.muyunyun.cn/0f0cf6e8cb68b18399eac2927f74b063.jpg) + +> If you want to pull the content of the template to the current folder, you can replace the `doc` of the above command with `.`, such as executing `npx create-react-doc .`. + +Then execute `cd doc && yarn && yarn start`, you can preview the site at `localhost: 3000`, if the site document changes, the site will automatically reload. + + + +## Site release + +In the [Quick Start](http://muyunyun.cn/create-react-doc/QuickStart) section, it introduces how to quickly build a site. This section will introduce how to package and publish the built site to gh-pages. + +### Automatically package and publish to gh-pages (recommended) + +The initialized template project integrates the [ci configuration](https://github.com/MuYunyun/create-react-doc/blob/main/packages/templates/default/.github/workflows/gh-pages.yml) of `Github action`, the user only needs to execute `git push` on the main branch to complete the automatic deployment of the site. + +![](http://with.muyunyun.cn/ea24d511f76efe5ba5d13bb6b1609aac.jpg) + +If it is the first deployment, after performing the following operations, you need to select Github Pages as gh-pages in the setting tab of the project. (See [First Deployment with GITHUB_TOKEN](https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-first-deployment-with-github_token) for details) + +```bash +git init +git add. +git commit -m "first commit" +git branch -M main +git remote add origin https://github.com/user or organization name/project name.git +git push -u origin main +``` + +> For more content, please visit [Site Release](http://muyunyun.cn/create-react-doc/SiteRelease), [Advanced Usage](http://muyunyun.cn/create-react-doc/HighOrderusage), [other tools](http://muyunyun.cn/create-react-doc/othertools) and other chapters. + +## Practice Sharing + +* [基于 SSR 的预渲染首屏直出方案](http://muyunyun.cn/blog/g3v1c5bq) +* [SEO 在 SPA 站点中的实践](http://muyunyun.cn/blog/ettzfags) diff --git a/README.md b/README.md index bcd70a2a..79e25724 100644 --- a/README.md +++ b/README.md @@ -1 +1,124 @@ -# create-react-doc \ No newline at end of file + + _.-"\ + _.-" \ + ,-" \ + \ create \ + \ \ react \ + \ \ doc \ + \ \ _.-; + \ \ _.-" : + \ \,-" _.-" + \( _.-" + `--" + +[![npm version](https://img.shields.io/npm/v/create-react-doc)](https://badge.fury.io/js/create-react-doc) +[![week download](https://img.shields.io/npm/dw/create-react-doc.svg)](https://www.npmjs.com/package/create-react-doc) +![views](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/views.svg) +![views](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/views_per_week.svg) +![clones](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/clones_per_week.svg) +![LICENSE MIT](https://img.shields.io/npm/l/create-react-doc.svg) + +[English](./README-en.md) | 简体中文 + +# Create React Doc + +[Create React Doc](https://github.com/MuYunyun/create-react-doc) 是一个使用 React 的 markdown 文档站点生成工具。就像 [create-react-app](https://github.com/facebook/create-react-app) 一样,开发者可以使用 Create React Doc 来开发、部署文档或者博客站点而无需关心额外的环境配置信息。 + +## 特性 + +* 建站理念: `文件即站点` (Files as a Site)。 +* 开箱即用: 通过指定目录或文档, 一键生成文档、博客站点, 无需关心站点环境配置信息。 +* 流畅的用户体验: 内置 SSR 首屏直出方案(基于 gp-pages 服务),以提升用户体验。 +* 基于 mdx: 支持在 markdown 中`书写 React 组件`、数学公式等。 +* 搜索引擎优化: 支持 SEO, 让文档更易被搜索。 +* 个性化: 支持[自定义主题](https://muyunyun.cn/create-react-doc/9f41fc98)。 +* 工作流: 集成 Github action, 支持自动化打包、发布站点。 + +> [快速上手](https://muyunyun.cn/create-react-doc/290a4219) + +## 主题 + +create-react-doc 提供了默认主题 [crd-seed](https://github.com/MuYunyun/create-react-doc/tree/main/packages/crd-seed)。 + +该主题支持以下特性: + +- [x] 适配网页/移动端展示。 +- [x] 支持暗黑模式。 +- [x] 支持标签页自定义聚合文章内容。 +- [x] 内置评论模块。 +- [x] 支持内嵌展示 codepen、codesandbox 案例。 +- [x] 支持从文档页快速跳转到对应的 Github 文档页进行在线编辑。 + +该主题效果可以参考[笔者博客](http://muyunyun.cn/blog)。 + +![](http://with.muyunyun.cn/90d3e357a31649b9466a828a92b6d88d.jpg) +![](http://with.muyunyun.cn/2e7440e4256debda2d73a4e6392c7146.jpg-300) + +如果您想定制化或者分享个人主题,可以参考[自定义主题](https://muyunyun.cn/create-react-doc/9f41fc98)章节。 + +## 快速上手 + +**create-react-doc** 非常容易上手。开发者不需要额外安装或配置 webpack 或者 Babel 等工具,它们被内置隐藏在脚手架中,因此开发者可以专心于文档的书写。 + +如果你想在当前文件下建立站点文件 `doc`, 这里提供如下三种方式快速建站: + +### npx + +```bash +npx create-react-doc doc +``` + +### npm + +```bash +npm init create-react-doc doc +``` + +### yarn + +```bash +yarn create react-doc doc +``` + +![](http://with.muyunyun.cn/0f0cf6e8cb68b18399eac2927f74b063.jpg) + +> 如果想把模板内容内容拉取到当前文件夹, 则可以将如上命令的 `doc` 替换为 `.`, 比如执行 `npx create-react-doc .`。 + +接着执行 `cd doc && yarn && yarn start`, 可以在 `localhost: 3000` 预览站点, 如果站点文档发生改变, 站点将自动重新加载。 + + + +## 站点发布 + +在 [快速上手](https://muyunyun.cn/create-react-doc/290a4219) 一节中介绍了如何快速搭建站点, 本节将介绍如何将搭建好的站点打包、发布到 gh-pages。 + +### 自动打包发布到 gh-pages (推荐) + +初始化的模板项目集成了 `Github action` 的 [ci 配置](https://github.com/MuYunyun/create-react-doc/blob/main/packages/templates/default/.github/workflows/gh-pages.yml), 使用方只需在 main 分支执行 `git push` 即可以完成站点的自动部署。 + +![](http://with.muyunyun.cn/ea24d511f76efe5ba5d13bb6b1609aac.jpg) + +如果是第一次部署, 在执行以下操作后, 需要在项目的 setting 选项卡中将 Github Pages 选择为 gh-pages。(详情见 [First Deployment with GITHUB_TOKEN](https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-first-deployment-with-github_token)) + +```bash +git init +git add . +git commit -m "first commit" +git branch -M main +git remote add origin https://github.com/用户或组织名/项目名.git +git push -u origin main +``` + +## 更多内容 + +* [站点发布](http://muyunyun.cn/create-react-doc/ude9296y) +* [高阶用法](http://muyunyun.cn/create-react-doc/9v9ug9h8) +* [其它工具](http://muyunyun.cn/create-react-doc/292h2c5k) +* [Front-matter](http://muyunyun.cn/create-react-doc/49g6b239) + +## 实践分享 + +* [基于 SSR 的预渲染首屏直出方案](http://muyunyun.cn/blog/g3v1c5bq) +* [SEO 在 SPA 站点中的实践](http://muyunyun.cn/blog/ettzfags) diff --git a/components/Button/index.jsx b/components/Button/index.jsx new file mode 100644 index 00000000..4880414c --- /dev/null +++ b/components/Button/index.jsx @@ -0,0 +1,9 @@ +import styles from './index.less' + +const Button = ({ + children, +}) => { + return +} + +export default Button diff --git a/components/Button/index.less b/components/Button/index.less new file mode 100644 index 00000000..35d4d2df --- /dev/null +++ b/components/Button/index.less @@ -0,0 +1,17 @@ +.btn { + color: #fff; + background: #1890ff; + border-color: #1890ff; + text-shadow: 0 -1px 0 rgb(0 0 0 / 12%); + box-shadow: 0 2px #0000000b; + height: 40px; + padding: 6.4px 15px; + font-size: 16px; + border-radius: 2px; + line-height: 1.5715; + position: relative; + display: inline-block; + font-weight: 400; + white-space: nowrap; + text-align: center; +} \ No newline at end of file diff --git a/components/index.jsx b/components/index.jsx new file mode 100644 index 00000000..cff0bc61 --- /dev/null +++ b/components/index.jsx @@ -0,0 +1,5 @@ +import Button from './Button/index.jsx' + +export { + Button, +} diff --git a/config.yml b/config.yml new file mode 100644 index 00000000..9d7f949a --- /dev/null +++ b/config.yml @@ -0,0 +1,54 @@ +# create-react-doc configuration. + +# Site +title: Create React Doc + +# Menu dir +## you can also set detailed dir, such as BasicSkill/css +## todo: auto menu +menu: [ + docs/快速上手.md, + docs/更新日志.md, + docs/主题, + docs/站点发布.md, + docs/Front-matter.md, + docs/数学公式.md, + docs/书写组件.md, + docs/高阶用法.md, + docs/其它工具.md, + docs/测试 +] +## set init open menu keys +# menuOpenKeys: + +# site theme +devTheme: packages/crd-seed/index +# theme: crd-seed + +# Github +## if you want to editing pages on github, you should config these arguments. +user: MuYunyun +repo: create-react-doc +github-ribbons: true +domain: https://muyunyun.cn +seo: + google: true + +# Available values: en | zh-cn +language: en + +# Inject Custom Logic +inject: injectLogic/index.js + +# Use abbrlink +abbrlink: true + +# Show Tags in head +tags: true + +# Config comment section +comment: + # Giscus Config, The config parameter that's supported can be seen in [giscus-component](https://github.com/giscus/giscus-component#documentation). + GiscusConfig: + repoId: MDEwOlJlcG9zaXRvcnkyNjgwMTE4MzA= + categoryId: DIC_kwDOD_mJNs4CSd1W diff --git a/docs/Front-matter.md b/docs/Front-matter.md new file mode 100644 index 00000000..69efbc82 --- /dev/null +++ b/docs/Front-matter.md @@ -0,0 +1,43 @@ + + +## Front-matter + +Front-matter 是文件最上方包裹在 `` 之间的区域,用于指定个别文件的变量,举例来说: + +```md + +``` + +以下是预先定义的参数,您可在模板中使用这些参数值并加以利用。 + +| 参数 | 描述 | 类型 | +| :------- | :-------------------------------------------------------------- | :------- | +| abbrlink | 短链。用于指定页面路由展示为指定短链,使用短链有助于 SEO 搜索。 | | +| tags | 自定义标签 | string[] | + +## 链接持久化 + +在以下场景需求场合中,可以展示短链以优化 URL 的显示。 + +* SEO 场景下需要链接持久化。 +* URL 链接中存在中文会被转码展示。 +* 文档的路径与文件名经常变更。 + +### 如何使用短链 + +在 `config.yml` 增加配置 `abbrlink: true` + +```diff ++ abbrlink: true +``` + +做好上述配置后,接着在控制台执行 `react-doc generate` 即可给 menu 配置属性中的所有文章目录文件加上短链资源。 + +```bash +react-doc generate // 一键给所有文章加上短链 +``` diff --git "a/docs/\344\270\273\351\242\230/\350\207\252\345\256\232\344\271\211\344\270\273\351\242\230.md" "b/docs/\344\270\273\351\242\230/\350\207\252\345\256\232\344\271\211\344\270\273\351\242\230.md" new file mode 100644 index 00000000..cd0f7103 --- /dev/null +++ "b/docs/\344\270\273\351\242\230/\350\207\252\345\256\232\344\271\211\344\270\273\351\242\230.md" @@ -0,0 +1,107 @@ + + +## 使用自定义主题 + +切换主题非常简单, 只需要将根目录文件 `config.yml` 中的 `theme` 更改为您想使用的主题即可。 + +```diff ++ theme: custom-theme +``` + +### 如何开发自定义主题包 + +create-react-doc 脚手架提供了脚本命令 `react-doc theme` 用来一键创建主题包开发环境。 + +![](http://with.muyunyun.cn/2e4a4b11f96c0d38759700c05fe96267.gif) + +```js +// 安装 create-react-doc +yarn add create-react-doc -g +// 执行 react-doc theme 并输入主题包名字 +react-doc theme +``` + +进入到所创建主题目录, 执行 `yarn && yarn start`, 此时会自动打开浏览器, 并在屏幕中央显示 `Write docs happily now.`。如下图所示: + +![](http://with.muyunyun.cn/1a2bf34700afd77a95014d2d5f359ffa.jpg) + +恭喜你, 此时你已经将主题开发环境配置完成。接着便可以开始愉快地定制个人主题了。 + +在所创建的主题项目中使用了 `react v18`,`react-router-dom v6`,项目支持使用 `less` 语法。 + +```js +import { Switch, Route } from 'react-router-dom' +import styles from './index.less' + +const CustomTheme = (props: CustomThemeProps) => { + return ( + + +
Welcome to your own theme
+
+
+ ) +} + +export default CustomTheme +``` + +CustomThemeProps 的接口类型暴露了菜单资源 `menuSource` 与路由资源 `routeData`, 自定义主题时可以按需使用它们。 + +```js +interface CustomThemeProps { + /** 菜单资源 */ + menuSource: { + /** 文件名称 eg: '快速上手.md' */ + name: string + /** 文件扩展名 eg: '.md' */ + extension: string + /** 文件路径 eg: '/docs/快速上手.md' */ + path: string + /** 路由路径 eg: ‘/快速上手’ */ + routePath: string + /** 文件大小 eg: 924 */ + size: number + /** 文件类型 eg: 'file' */ + type: string + /** 文件创建日期 eg: '2020-11-11' */ + birthtime: string + /** 文件修改日期 eg: '2021-01-14' */ + mtime: string + }[] + /** 路由资源 */ + routeData: { + /** 文件名称 eg: '快速上手.md' */ + article: string + /** 异步加载 markdown 组件函数 */ + component: AsyncRouteComponent(props) + /** markdown 文章信息对象。若为文件则有 title 字段, 若为文件夹则无 title 字段 */ + mdconf: { title?: string } + /** 文件路径 eg: '/docs/快速上手' */ + path: string + }[] +} +``` + +此外在自定义主题文件中可以自由使用由 webpack 注入的 `DOCSCONFIG` 对象中的变量, DOCSCONFIG 中的变量与项目根目录中的 `config.yml` 文件变量一一对应。 + +比如 `config.yml` 配置如下所示: + +```bash +menu: ['Introduction'] +theme: crd-seed +user: muyunyun +repo: https://github.com/MuYunyun/create-react-doc +language: en +``` + +则主题项目中可以通过如下方式获取到 `config.yml` 配置属性。 + +```js +const { menu, theme, user } = DOCSCONFIG || {} +``` diff --git "a/docs/\344\270\273\351\242\230/\351\273\230\350\256\244\344\270\273\351\242\230.md" "b/docs/\344\270\273\351\242\230/\351\273\230\350\256\244\344\270\273\351\242\230.md" new file mode 100644 index 00000000..0772a6db --- /dev/null +++ "b/docs/\344\270\273\351\242\230/\351\273\230\350\256\244\344\270\273\351\242\230.md" @@ -0,0 +1,48 @@ + + +## 默认主题 + +create-react-doc 的默认主题为 [crd-seed](https://github.com/MuYunyun/create-react-doc/tree/main/packages/crd-seed)。 + +该主题支持以下特性: + +- [x] 适配网页/移动端展示。 +- [x] 支持暗黑模式。 +- [x] 支持标签页自定义聚合文章内容。 +- [x] 内置评论模块。 +- [x] 支持内嵌展示 codepen、codesandbox 案例。 +- [x] 支持从文档页快速跳转到对应的 Github 文档页进行在线编辑。 + +使用该主题搭建的项目有: + +* [blog](https://github.com/MuYunyun/blog), [站点](http://muyunyun.cn/blog) + * ![](http://with.muyunyun.cn/90d3e357a31649b9466a828a92b6d88d.jpg) + * ![](http://with.muyunyun.cn/2e7440e4256debda2d73a4e6392c7146.jpg-300) +* [diana](https://github.com/MuYunyun/diana), [站点](https://muyunyun.cn/diana/) + +> 如果您有其它的改进优化想法, 欢迎留言补充 + +## config.yml + +[config.yml](https://github.com/MuYunyun/create-react-doc/blob/main/packages/templates/default/_config.yml) 文件是配置站点主题功能的地方。 + +它支持配置的属性如下: + +| 属性名 | 作用 | 类型 | 默认 | +| :------------: | :----------------------------------: | :---------------------------------------------------------------------------------: | :------: | +| title | 站点名 | string | | +| menu | 作为站点菜单的文件/文件夹路径 | string[] | | +| menuOpenKeys | 默认展开菜单的文件夹路径 | string | | +| user | Github 用户名 | string | | +| repo | Github 项目名 | string | | +| language | 站点语言 | en \| zh-cn | en | +| github-ribbons | 是否在右上角显示 github 丝带 | boolean | false | +| theme | 使用主题 | string | crd-seed | +| devTheme | 开发自定义主题时, 需设置其为 true | string | ./index | +| seo | 是否开启 SEO 优化 | { google?: boolean } | | +| domain | SEO 优化的站点域名, 用于生成 sitemap | string | | +| comment | 开启评论区,并进行相关配置 | { GiscusConfig: [Props](https://github.com/giscus/giscus-component#documentation) } | | + +详细用法可以参考 [config.yml](https://github.com/MuYunyun/blog/blob/main/config.yml)。 \ No newline at end of file diff --git "a/docs/\344\271\246\345\206\231\347\273\204\344\273\266.md" "b/docs/\344\271\246\345\206\231\347\273\204\344\273\266.md" new file mode 100644 index 00000000..39b327e2 --- /dev/null +++ "b/docs/\344\271\246\345\206\231\347\273\204\344\273\266.md" @@ -0,0 +1,21 @@ + + +import { Button } from '../components/index.jsx' + +## 书写组件 + +[create-react-doc](https://github.com/MuYunyun/create-react-doc) 内置了 [mdx](https://github.com/mdx-js/mdx), 你可以在 .md 文件中书写 React 组件, 因此你可以选择 create-react-doc 来快速搭建组件站点。 + +## 例子 +### Button 组件 + +```js +import { Button } from '../components/index.jsx' + + +``` + + \ No newline at end of file diff --git "a/docs/\345\205\266\345\256\203\345\267\245\345\205\267.md" "b/docs/\345\205\266\345\256\203\345\267\245\345\205\267.md" new file mode 100644 index 00000000..4ffac938 --- /dev/null +++ "b/docs/\345\205\266\345\256\203\345\267\245\345\205\267.md" @@ -0,0 +1,57 @@ + + +## 其它工具 + +### crd-leetcode-cli + +#### 背景 + +当新增 LeetCode 题解时需要[手动更新表格](https://github.com/MuYunyun/blog/blob/main/LeetCode/README.md), 较为不便。[crd-leetcode-cli](https://github.com/MuYunyun/create-react-doc/tree/main/packages/leetcode-cli) 提供了更新 leetcode 站点中已 ac 题解的能力。 + +#### 安装 + +执行 `yarn add crd-leetcode-cli -g`, 国内用户可以执行 `cnpm install crd-leetcode-cli -g` + +#### 使用 + +```bash +leetcode download // 增量拉取 AC 题目(若无登录, 则会先执行登录逻辑) +leetcode download -a // 全量拉取 AC 题目 +leetcode login // 登录 +leetcode logout // 登出 +``` + +#### 自定义渲染表格 + +插件提供了自定义渲染 markdown table 的能力。 + +在项目根目录创建 [config.js](https://github.com/MuYunyun/blog/blob/main/config.js) 文件。 + +在 config.js 内自定义生成 markdown 的 [transform_markdown_table 函数](https://github.com/MuYunyun/blog/blob/main/config.js#L5-L22)。 + +```js +const transform_markdown_table = (dataArr: QuestionProps[]): string => {} +module.exports = { transform_markdown_table } +``` + +QuestionProps 接口定义如下: + +| 名称 | 含义 | 例子 | +| :--------: | :--------------: | :-----: | +| questionId | 题号 | | +| title | 标题 | Two Sum | +| titleSlug | 标题的另一种模式 | two-sum | +| difficulty | 难度 | | +| topicTags | 题目所属标签 | | + +通过自定义 transform_markdown_table 函数, 便可得到如下 markdown table: + +![](http://with.muyunyun.cn/1938e43a45410090e8486e495e6d9fee.jpg) + +#### 技术细节 + +* 使用 puppeteer 登录 leetcode 获取 cookie 信息。 +* 获取 cookie 后, 使用 graphql-request 调用 graphql 接口获取题目详情信息。 +* 自定义生成 markdown table。 \ No newline at end of file diff --git "a/docs/\345\277\253\351\200\237\344\270\212\346\211\213.md" "b/docs/\345\277\253\351\200\237\344\270\212\346\211\213.md" new file mode 100644 index 00000000..5ca9254d --- /dev/null +++ "b/docs/\345\277\253\351\200\237\344\270\212\346\211\213.md" @@ -0,0 +1,37 @@ + + +## 快速上手 + +**create-react-doc** 非常容易上手。开发者不需要额外安装或配置 webpack 或者 Babel 等工具,它们被内置隐藏在脚手架中,因此开发者可以专心于文档的书写。 + +如果你想在当前文件下建立站点文件 `doc`, 这里提供如下三种方式快速建站: + +### npx + +```bash +npx create-react-doc doc +``` + +### npm + +```bash +npm init create-react-doc doc +``` + +### yarn + +```bash +yarn create react-doc doc +``` + +![](http://with.muyunyun.cn/0f0cf6e8cb68b18399eac2927f74b063.jpg) + +> 如果想把模板内容内容拉取到当前文件夹, 则可以将如上命令的 `doc` 替换为 `.`, 比如执行 `npx create-react-doc .`。 + +接着执行 `cd doc && yarn && yarn start`, 可以在 `localhost: 3000` 预览站点, 如果站点文档发生改变, 站点将自动重新加载。 + + diff --git "a/docs/\346\225\260\345\255\246\345\205\254\345\274\217.md" "b/docs/\346\225\260\345\255\246\345\205\254\345\274\217.md" new file mode 100644 index 00000000..fd6512d9 --- /dev/null +++ "b/docs/\346\225\260\345\255\246\345\205\254\345\274\217.md" @@ -0,0 +1,47 @@ + + +## 数学公式 + +支持在 `$$` 与 `$$` 之间书写 latex 数学公式即能显示在网页上。 + +```js +$$ +z = \frac{x}{y} +$$ +``` + +被转化为: + +$$ +z = \frac{x}{y} +$$ + +```js +$$ +C_1 \quad= \quad c_2 + c_4^3 +$$ +``` + +被转化为: + +$$ +C_1 \quad= \quad c_2 + c_4^3 +$$ + +```js +$$ +y = \sqrt {x_1^2 + x_2^2 + x_3^2 + x_4^2} +$$ +``` + +被转化为: + +$$ +y = \sqrt {x_1^2 + x_2^2 + x_3^2 + x_4^2} +$$ + +## 参考链接 + +* [latex 数学公式示例汇总](https://zhuanlan.zhihu.com/p/34799800) diff --git "a/docs/\346\233\264\346\226\260\346\227\245\345\277\227.md" "b/docs/\346\233\264\346\226\260\346\227\245\345\277\227.md" new file mode 100644 index 00000000..31e52649 --- /dev/null +++ "b/docs/\346\233\264\346\226\260\346\227\245\345\277\227.md" @@ -0,0 +1,303 @@ + + +# CHANGELOG + +`create-react-doc` 严格遵循 [Semantic Versioning 2.0.0](http://semver.org/lang/zh-CN/) 语义化版本规范。 + +### 1.10.3 + +`2022-11-26` + +- **Enhancement** + + - 🎈 更新主题字体样式为手写体风格。 + +### 1.10.2 + +`2022-11-19` + +- **Fix** + + - 🐞 修复 @giscus/react 包结构变更导致站点崩溃的问题。[issue](https://github.com/giscus/giscus-component/issues/783) + +- **Enhancement** + + - 🎈 使用 esbuild 代替 babel-loader 进行打包构建。[issue](https://github.com/MuYunyun/create-react-doc/issues/337) + +### 1.10.0 + +`2022-11-10` + +- **Feature** + + - 🚀 主题内置评论模块,支持在 [config.yml](https://muyunyun.cn/create-react-doc/85li8wdd) 配置开启评论模块。 + +### 1.9.2 + +`2022-04-09` + +- **Fix** + + - 🐞 修复标签页包含重复标签归档的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/286) + +### 1.9.1 + +`2022-04-05` + +- **Enhancement** + + - 🎈 升级基础包版本。[issue](https://github.com/MuYunyun/create-react-doc/issues/278) + - 更新 react 版本从 v17 至 v18 + - 更新 react-router-dom 版本从 v4 至 v6。 + +### 1.9.0 + +`2022-04-01` + +- **Feature** + + - 🚀 支持配置展示标签页以自定义聚合文章内容。[issue](https://github.com/MuYunyun/create-react-doc/issues/264) + +### 1.8.2 + +`2022-02-02` + +- **Enhancement** + + - 🎈 支持在本地环境调试项目源代码。[mr](https://github.com/MuYunyun/create-react-doc/pull/249) + +### 1.8.1 + +`2022-01-17` + +- **Fix** + + - 🐞 修复 crd-scripts、crd-seed 遗漏指定安装 crd-client-utils 的问题。 + +### 1.8.0 + +`2022-01-16` + +- **Feature** + + - 🚀 支持 SSR 首屏直出方案(基于 gp-pages 服务)以避免预渲染带来的二次刷新产生页面抖动的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/103) + - 🚀 新增 crd-client-utils 包以收录公用方法。 + +### 1.7.0 + +`2022-01-02` + +- **Feature** + + - 🚀 访问内容页路由时,收起菜单侧边栏以提高首屏加载体验。[issue](https://github.com/MuYunyun/create-react-doc/issues/219) + - 优势一: 用户可以聚焦访问内容区,提升阅读体验。 + - 优势二: 菜单区域渲染内容复杂,隐藏其加载过程,提升首屏体验。 + - 🚀 菜单侧边栏展开后,选中项自动滚动到视口内。 + +### 1.6.1 + +`2021-12-27` + +- **Fix** + + - 🐞 修复访问多层级子菜单目录对应的路由时,对应菜单未被展开的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/222)。 + +### 1.6.0 + +`2021-10-30` + +- **Feature** + + - 🚀 SEO 搜索标题优化,优化 document.title 与 meta 标签。[issue](https://github.com/MuYunyun/create-react-doc/issues/203) + +### 1.5.3 + +`2021-10-25` + +- **Fix** + + - 🐞 修复在使用短链时,站点 sitemap.xml 文件生成丢失的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/206)。 + +### 1.5.2 + +`2021-10-19` + +- **Fix** + + - 🐞 修复点击右上角 `Edit in GitHub` 链接跳转错误的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/205)。 + +### 1.5.1 + +`2021-10-19` + +- **Fix** + + - 🐞 修复页面首屏菜单栏未高亮选中的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/195)。 + +### 1.5.0 + +`2021-10-13` + +- **Feature** + + - 🚀 支持 react-doc generate 命令,给 md 文件自动补全短链。[issue](https://github.com/MuYunyun/create-react-doc/issues/87)、[mr](https://github.com/MuYunyun/create-react-doc/pull/194) + +### 1.4.0 + +`2021-10-08` + +- **Feature** + + - 🚀 支持展示短链以让文章链接持久化。[issue](https://github.com/MuYunyun/create-react-doc/issues/87)、[mr](https://github.com/MuYunyun/create-react-doc/pull/193) + - 🚀 支持在 Front-matter 区域中书写个别文件的变量。 + +### 1.3.5 + +`2021-09-24` + +- **Fix** + + - 🐞 修复路由过多时导致预渲染编译超时的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/182) + - 🐞 修复初始化模板 menu 参数类型错误的问题。[mr](https://github.com/MuYunyun/create-react-doc/pull/181) + +### 1.3.4 + +`2021-06-27` + +- **Fix** + + - 🐞 修复 npx create-react-doc doc 初始化生成文档项目报错的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/157)。 + +### 1.3.3 + +`2021-06-24` + +- **Fix** + + - 🐞 修复编译预渲染时, 缺少多层级目录文件生成的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/147)。 + +### 1.3.0 + +`2021-06-09` + +- **Feature** + + - 🚀 create-react-doc 集成 MDX。[issue](https://github.com/MuYunyun/create-react-doc/issues/138)、[mr](https://github.com/MuYunyun/create-react-doc/pull/143) + - 🚀 支持在 markdown 文件中`书写 React 组件`。 + - 🚀 支持在 markdown 文件中`书写数学公式`。 + +- **Enhancement** + + - 🎈 支持 yarn up、yarn up:dev 在 lerna 项目中快速安装包。[mr](https://github.com/MuYunyun/create-react-doc/pull/143/files?file-filters%5B%5D=.html&file-filters%5B%5D=.js&file-filters%5B%5D=.json&file-filters%5B%5D=.less&file-filters%5B%5D=.lock&file-filters%5B%5D=.sh) + +### 1.2.0 + +- **Fix** + + - 🐞 修复路由数量过多, puppeteer 运行超时的问题。[issue](https://github.com/MuYunyun/blog/issues/115)。 + +### 1.1.4 + +- **Fix** + + - 🐞 修复点击 Edit In Github 失效的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/86)。 + +### 1.1.0 + +- **Feature** + + - 🚀 新增 crd-generator-sitemap 包, 用于生成 sitemap。 + - 🚀 配置文件中新增 domain 字段用于生成 sitemap。 + +### 1.0.0 + +- **Feature** + + - 🚀 文档支持预渲染。[pr](https://github.com/MuYunyun/create-react-doc/pull/95/files) + - 🚀 路由由 hash 路由调整为 browser 路由。 + 🎈 站点 SEO 优化。[doc](https://github.com/MuYunyun/blog/issues/84#issuecomment-786418891) + - 🚀 配置文件中 menu 字段类型从 string 调整为 array。 + +### 0.3.30 + +- **Feature** + + - 🚀 支持显示展示 pv、uv。[pr](https://github.com/MuYunyun/create-react-doc/pull/85) + +### 0.2.29 + +- **Fix** + + - 🐞 修复首次进入菜单未高亮的问题。[issue](https://github.com/MuYunyun/create-react-doc/issues/78)。 +### 0.2.28 + +- **Feature** + + - 🚀 支持自定义主题 ci。[pr](https://github.com/MuYunyun/create-react-doc/pull/80) + +### 0.2.27 + +- **Feature** + + - 🚀 支持自定义主题。[pr](https://github.com/MuYunyun/create-react-doc/pull/77) + +### 0.2.22 + +- **Feature** + + - 🚀 升级 React 16 至 17。[pr](https://github.com/MuYunyun/create-react-doc/pull/71) + +### 0.2.21 + +- **Feature** + + - 🚀 升级 webpack4 至 webpack5。[pr](https://github.com/MuYunyun/create-react-doc/pull/65) + +### 0.2.14 + +- **Feature** + + - 🚀 支持 inject 与 injectWithPathname 注入自定义逻辑。[pr](https://github.com/MuYunyun/create-react-doc/pull/65) + +### 0.2.14 + +- **Feature** + + - 🚀 集成 Github action, 支持自动化打包、发布站点。 + +### 0.2.7 + +`2020-09-25` + +- **Feature** + + - 🚀 提供 @crd/leetcode-cli 提供将 leetcode 已 AC 的题目转化为 markdown table 的能力。[pr](https://github.com/MuYunyun/create-react-doc/pull/22) + +### 0.2.0 + +`2020-08-02` + +- **Feature** + + - 🚀 站点支持全局搜索菜单标题与文件内容。[pr](https://github.com/MuYunyun/create-react-doc/pull/22) + +### 0.1.20 + +`2020-07-13` + +- **Fix** + + - 🐞 fix [20](https://github.com/MuYunyun/create-react-doc/issues/20)。 + - 🐞 fix [17](https://github.com/MuYunyun/create-react-doc/issues/17)。 + +- **Enhancement** + + - 🎈 项目结构重构为 monorepo。[pr](https://github.com/MuYunyun/create-react-doc/pull/16) + +### 0.1.0 + +- **Feature** + + - 🚀 详细内容见 [issue](https://github.com/MuYunyun/create-react-doc/issues/2) \ No newline at end of file diff --git "a/docs/\346\265\213\350\257\225/\346\265\213\350\257\225\346\240\207\347\255\276.md" "b/docs/\346\265\213\350\257\225/\346\265\213\350\257\225\346\240\207\347\255\276.md" new file mode 100644 index 00000000..6ff4117a --- /dev/null +++ "b/docs/\346\265\213\350\257\225/\346\265\213\350\257\225\346\240\207\347\255\276.md" @@ -0,0 +1,6 @@ + + +该页面用来测试自定义标签。 diff --git "a/docs/\346\265\213\350\257\225/\346\265\213\350\257\225\350\267\257\347\224\261.md" "b/docs/\346\265\213\350\257\225/\346\265\213\350\257\225\350\267\257\347\224\261.md" new file mode 100644 index 00000000..6f61f3e3 --- /dev/null +++ "b/docs/\346\265\213\350\257\225/\346\265\213\350\257\225\350\267\257\347\224\261.md" @@ -0,0 +1,5 @@ + + +* 该页面用来测试未使用 abbrlink 的中文路径。 diff --git "a/docs/\347\253\231\347\202\271\345\217\221\345\270\203.md" "b/docs/\347\253\231\347\202\271\345\217\221\345\270\203.md" new file mode 100644 index 00000000..c841e5b6 --- /dev/null +++ "b/docs/\347\253\231\347\202\271\345\217\221\345\270\203.md" @@ -0,0 +1,30 @@ + + +## 站点发布 + +在 [快速上手](http://muyunyun.cn/create-react-doc/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) 一节中介绍了如何快速搭建站点, 本节将介绍如何将搭建好的站点打包、发布到 gh-pages。 + +### 自动打包发布到 gh-pages (推荐) + +初始化的模板项目集成了 `Github action` 的 [ci 配置](https://github.com/MuYunyun/create-react-doc/blob/main/packages/templates/default/.github/workflows/gh-pages.yml), 使用方只需在 main 分支执行 `git push` 即可以完成站点的自动部署。 + +![](http://with.muyunyun.cn/ea24d511f76efe5ba5d13bb6b1609aac.jpg) + +如果是第一次部署, 在执行以下操作后, 需要在项目的 setting 选项卡中将 Github Pages 选择为 gh-pages。(详情见 [First Deployment with GITHUB_TOKEN](https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-first-deployment-with-github_token)) + +```bash +git init +git add . +git commit -m "first commit" +git branch -M main +git remote add origin https://github.com/用户或组织名/项目名.git +git push -u origin main +``` + +### 手动打包发布 + +如果需要发布到自定义服务器, 可以执行 `npm run build` 或者 `yarn build` 对将要发布的文档站点进行打包构建, 此时的文档网站已准备好进行部署。 + +接着将打包出的 `.crd-dist` 的文件资源同步到目标服务器即可。 \ No newline at end of file diff --git "a/docs/\351\253\230\351\230\266\347\224\250\346\263\225.md" "b/docs/\351\253\230\351\230\266\347\224\250\346\263\225.md" new file mode 100644 index 00000000..e8e390c7 --- /dev/null +++ "b/docs/\351\253\230\351\230\266\347\224\250\346\263\225.md" @@ -0,0 +1,24 @@ + + +## 高阶用法 + +与 git 文件结构类似, 如果在展示的文件夹中有私有文件不方便展示在文档站点, 可以在 `.gitignore` 文件中设置过滤文件, 这样它们就不会展示在文档站点中了。eg: [.gitignore](https://github.com/MuYunyun/blog/blob/main/.gitignore) + +### 插入自定义脚本 + +在 `config.yml` 文件中加入 `inject` 字段。 + +```diff ++ inject: injectLogic/index.js +``` + +然后在根目录新建与 `inject` 字段相对应的文件, 声明 `injectWithPathname` 函数, 写入[自定义逻辑](https://github.com/MuYunyun/create-react-doc/injectLogic/index.js)。 + +```js +// perf injectWithPathname logic every pathname changes +const injectWithPathname = (pathname) => {} + +module.exports = { injectWithPathname } +``` \ No newline at end of file diff --git a/gh-pages.js b/gh-pages.js new file mode 100644 index 00000000..a90d9736 --- /dev/null +++ b/gh-pages.js @@ -0,0 +1,16 @@ +const ghPages = require('gh-pages') + +ghPages.publish( + '.crd-dist', + { + branch: 'gh-pages', + repo: 'https://github.com/MuYunyun/create-react-doc.git', + }, + (error) => { + if (error) { + console.error(error) + } else { + console.log('docs sync success') + } + } +) diff --git a/injectLogic/index.js b/injectLogic/index.js new file mode 100644 index 00000000..a1a94c89 --- /dev/null +++ b/injectLogic/index.js @@ -0,0 +1,14 @@ +/* eslint-disable no-empty */ +// seo: perf inject logic only once +const inject = () => { + // SEO for Google through https://search.google.com/search-console/welcome + const meta = document.createElement('meta') + meta.name = 'google-site-verification' + meta.content = '7fyp1NuvXSRLM9KpMq5_YNE_0zFZkPnuV-SbVVFgWbI' + document.head.appendChild(meta) +} + +// perf injectWithPathname logic every pathname changes +const injectWithPathname = (pathname) => {} + +module.exports = { inject, injectWithPathname } diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..7cf2643e --- /dev/null +++ b/lerna.json @@ -0,0 +1,20 @@ +{ + "version": "1.10.3", + "command": { + "bootstrap": { + "npmClientArgs": [ + "--no-package-lock" + ] + }, + "publish": { + "allowBranch": [ + "main", + "qa/latest" + ], + "message": "chore: publish" + } + }, + "//": "set yarn workspaces in root", + "useWorkspaces": true, + "npmClient": "yarn" +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..7c33bde6 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "create-react-doc", + "private": true, + "workspaces": [ + "packages/*" + ], + "description": "Fast static generated site. Just write markdown file.", + "homepage": "http://muyunyun.cn/create-react-doc", + "bin": { + "react-doc": "bin/react-doc.js" + }, + "scripts": { + "bootstrap": "lerna bootstrap", + "bootstrap --hoist": "lerna bootstrap --hoist", + "clean": "lerna clean", + "start": "yarn bootstrap && node packages/create-react-doc/index.js start", + "build": "node packages/create-react-doc/index.js build", + "deploy": "node packages/create-react-doc/index.js deploy", + "release": "lerna publish", + "release-qa": "lerna publish --npm-tag=beta", + "cleanup": "rm -rf node_modules/gh-pages/.cache", + "deploy:site": "npm run cleanup && node gh-pages", + "up:dev": "sh utils/uppackage-dev.sh", + "up": "sh utils/uppackage.sh" + }, + "repository": { + "type": "git", + "url": "https://github.com/MuYunyun/create-react-doc" + }, + "keywords": [ + "react", + "create-react-doc", + "blog", + "markdown" + ], + "author": "muyunyun", + "license": "MIT", + "devDependencies": { + "eslint": "^4.19.1", + "eslint-config-airbnb": "^16.1.0", + "eslint-plugin-import": "^2.11.0", + "eslint-plugin-jsx-a11y": "^6.0.3", + "eslint-plugin-react": "^7.7.0", + "gh-pages": "^3.1.0", + "lerna": "^3.22.1" + } +} diff --git a/packages/crd-client-utils/.npmrc b/packages/crd-client-utils/.npmrc new file mode 100644 index 00000000..7edb6af8 --- /dev/null +++ b/packages/crd-client-utils/.npmrc @@ -0,0 +1,10 @@ +# .npmrc + +registry=https://registry.npmjs.org/ + +# https://github.com/sass/node-sass#binary-configuration-parameters +sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ + +# https://github.com/Medium/phantomjs#deciding-where-to-get-phantomjs +# phantomjs_cdnurl=http://cnpmjs.org/downloads +phantomjs_cdnurl=https://npm.taobao.org/dist/phantomjs diff --git a/packages/crd-client-utils/README.md b/packages/crd-client-utils/README.md new file mode 100644 index 00000000..9a533715 --- /dev/null +++ b/packages/crd-client-utils/README.md @@ -0,0 +1,3 @@ +### crd-client-utils + +[create-react-doc](https://github.com/MuYunyun/create-react-doc) 的工具包。 \ No newline at end of file diff --git a/packages/crd-client-utils/index.js b/packages/crd-client-utils/index.js new file mode 100644 index 00000000..c6fd5591 --- /dev/null +++ b/packages/crd-client-utils/index.js @@ -0,0 +1,18 @@ +import { useLayoutEffect, useEffect } from 'react' + +const ifDev = env === 'dev' +const ifProd = env === 'prod' +const ifPrerender = window.__PRERENDER_INJECTED && window.__PRERENDER_INJECTED.prerender + +// Not only Prod env but also [some part of Dev env](https://github.com/MuYunyun/create-react-doc/blob/main/packages/crd-scripts/src/web/index.js#L10-L13) +// need using useLayoutEffect. If meeting this case in the future, thinking about passing extra tag from . +const useEnhancedEffect = ifProd + ? useLayoutEffect + : useEffect + +export { + ifDev, + ifProd, + ifPrerender, + useEnhancedEffect +} diff --git a/packages/crd-client-utils/package.json b/packages/crd-client-utils/package.json new file mode 100644 index 00000000..9542d2a0 --- /dev/null +++ b/packages/crd-client-utils/package.json @@ -0,0 +1,18 @@ +{ + "name": "crd-client-utils", + "version": "1.8.2", + "description": "Utils with create react doc", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/MuYunyun/create-react-doc", + "directory": "packages/utils" + }, + "keywords": [], + "publishConfig": { + "access": "public" + }, + "author": "muyunyun", + "license": "MIT", + "gitHead": "ffc5e4cbc94a7356da558c2dbf46e2f39bb8b199" +} diff --git a/packages/crd-generator-sitemap/.npmrc b/packages/crd-generator-sitemap/.npmrc new file mode 100644 index 00000000..7edb6af8 --- /dev/null +++ b/packages/crd-generator-sitemap/.npmrc @@ -0,0 +1,10 @@ +# .npmrc + +registry=https://registry.npmjs.org/ + +# https://github.com/sass/node-sass#binary-configuration-parameters +sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ + +# https://github.com/Medium/phantomjs#deciding-where-to-get-phantomjs +# phantomjs_cdnurl=http://cnpmjs.org/downloads +phantomjs_cdnurl=https://npm.taobao.org/dist/phantomjs diff --git a/packages/crd-generator-sitemap/README.md b/packages/crd-generator-sitemap/README.md new file mode 100644 index 00000000..fae75e63 --- /dev/null +++ b/packages/crd-generator-sitemap/README.md @@ -0,0 +1,3 @@ +### crd-generator-sitemap + +[create-react-doc](https://github.com/MuYunyun/create-react-doc) SEO 插件, 用于生成站点地图。 \ No newline at end of file diff --git a/packages/crd-generator-sitemap/generate.js b/packages/crd-generator-sitemap/generate.js new file mode 100644 index 00000000..bbee11d5 --- /dev/null +++ b/packages/crd-generator-sitemap/generate.js @@ -0,0 +1,45 @@ +const { getDocsConfig } = require('crd-utils') + +// template for google SEO +// +// muyunyun.cn/blog/xxx +// {{ sNow | formatDate }} +// daily +// 1.0 +// + +const docsConfig = getDocsConfig() + +const template = content => + ` + + ${content} + +` + +/** + * generate sitemap for google. + */ +const generateSiteMap = (routes) => { + const domain = docsConfig && docsConfig.domain + const repo = docsConfig && docsConfig.repo + let content = '' + for (let i = 0; i < routes.length; i++) { + if (i === routes.length - 1) { + content += +` + ${domain}/${repo}${routes[i]} + ` + } else { + content += +` + ${domain}/${repo}${routes[i]} + \n` + } + } + return template(content) +} + +module.exports = { + generateSiteMap, +} diff --git a/packages/crd-generator-sitemap/index.js b/packages/crd-generator-sitemap/index.js new file mode 100644 index 00000000..3533d7d1 --- /dev/null +++ b/packages/crd-generator-sitemap/index.js @@ -0,0 +1,5 @@ +const { generateSiteMap } = require('./generate') + +module.exports = { + generateSiteMap, +} diff --git a/packages/crd-generator-sitemap/package.json b/packages/crd-generator-sitemap/package.json new file mode 100644 index 00000000..689aa18d --- /dev/null +++ b/packages/crd-generator-sitemap/package.json @@ -0,0 +1,18 @@ +{ + "name": "crd-generator-sitemap", + "version": "1.3.4", + "description": "generator sitemap for create-react-doc", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/MuYunyun/create-react-doc", + "directory": "packages/crd-generator-sitemap" + }, + "keywords": [], + "publishConfig": { + "access": "public" + }, + "author": "muyunyun", + "license": "MIT", + "gitHead": "ffc5e4cbc94a7356da558c2dbf46e2f39bb8b199" +} diff --git a/packages/crd-scripts/.npmrc b/packages/crd-scripts/.npmrc new file mode 100644 index 00000000..7edb6af8 --- /dev/null +++ b/packages/crd-scripts/.npmrc @@ -0,0 +1,10 @@ +# .npmrc + +registry=https://registry.npmjs.org/ + +# https://github.com/sass/node-sass#binary-configuration-parameters +sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ + +# https://github.com/Medium/phantomjs#deciding-where-to-get-phantomjs +# phantomjs_cdnurl=http://cnpmjs.org/downloads +phantomjs_cdnurl=https://npm.taobao.org/dist/phantomjs diff --git a/packages/crd-scripts/README.md b/packages/crd-scripts/README.md new file mode 100644 index 00000000..612fabf1 --- /dev/null +++ b/packages/crd-scripts/README.md @@ -0,0 +1,15 @@ + _.-"\ + _.-" \ + ,-" \ + \ create \ + \ \ react \ + \ \ doc \ + \ \ _.-; + \ \ _.-" : + \ \,-" _.-" + \( _.-" + `--" + +# crd-scripts + +crd-scripts 封装提供了一系列构建、发布脚本。 diff --git a/packages/crd-scripts/index.js b/packages/crd-scripts/index.js new file mode 100644 index 00000000..8a84dd85 --- /dev/null +++ b/packages/crd-scripts/index.js @@ -0,0 +1,19 @@ +const initProject = require('./src/commands/initProject') +const initTheme = require('./src/commands/initTheme') +const initCache = require('./src/utils/initCache') +const Servers = require('./src/server') +const Build = require('./src/build') +const Deploy = require('./src/deploy') +const Generate = require('./src/generate') +const paths = require('./src/conf/path') + +module.exports = { + initProject, + initTheme, + initCache, + Servers, + Build, + Deploy, + Generate, + paths, +} diff --git a/packages/crd-scripts/package.json b/packages/crd-scripts/package.json new file mode 100644 index 00000000..485fef65 --- /dev/null +++ b/packages/crd-scripts/package.json @@ -0,0 +1,65 @@ +{ + "name": "crd-scripts", + "version": "1.10.3", + "description": "Scripts using with Create React Doc", + "main": "index.js", + "dependencies": { + "@mdx-js/loader": "^1.6.22", + "@nuxtjs/friendly-errors-webpack-plugin": "^2.0.2", + "babel-eslint": "^8.0.1", + "chalk": "^4.1.0", + "colors-cli": "^1.0.13", + "copy-markdown-image-webpack-plugin": "^2.0.0", + "copy-template-dir": "^1.3.0", + "crd-client-utils": "^1.8.2", + "crd-generator-sitemap": "^1.3.4", + "crd-prerender-spa-plugin": "^0.2.0", + "crd-theme": "^1.10.3", + "crd-utils": "^1.5.0", + "css-loader": "^0.28.7", + "css-minimizer-webpack-plugin": "^1.2.0", + "detect-port": "^1.2.2", + "esbuild-loader": "^2.20.0", + "eslint": "^7.11.0", + "eslint-loader": "^4.0.2", + "file-loader": "^1.1.11", + "fs-extra": "^5.0.0", + "gh-pages": "^1.2.0", + "html-webpack-plugin": "^4.5.1", + "less-loader": "^7.2.1", + "loading-cli": "^1.0.6", + "local-ip-url": "^1.0.1", + "mini-css-extract-plugin": "^0.4.0", + "open-browsers": "^1.1.1", + "path-browserify": "^1.0.1", + "postcss-flexbugs-fixes": "^3.3.1", + "postcss-loader": "^2.0.9", + "process": "^0.11.10", + "rehype-katex": "^5.0.0", + "remark-math": "^3.0.1", + "rimraf": "^2.6.2", + "string-replace-loader": "^3.0.1", + "style-loader": "^0.19.1", + "upath": "^1.0.2", + "url-replace-loader": "^1.0.0", + "webpack": "^5.12.2", + "webpack-dev-middleware": "^3.7.1", + "webpack-dev-server": "^3.8.1", + "webpack-hot-dev-clients": "^1.0.4", + "webpackbar": "^4.0.0", + "write": "^1.0.3", + "yamljs": "^0.3.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/MuYunyun/create-react-doc", + "directory": "packages/scripts" + }, + "keywords": [], + "publishConfig": { + "access": "public" + }, + "author": "muyunyun", + "license": "MIT", + "gitHead": "ffc5e4cbc94a7356da558c2dbf46e2f39bb8b199" +} diff --git a/packages/crd-scripts/src/build.js b/packages/crd-scripts/src/build.js new file mode 100644 index 00000000..baa3b8d6 --- /dev/null +++ b/packages/crd-scripts/src/build.js @@ -0,0 +1,33 @@ +const webpack = require('webpack') +const fs = require('fs') +const { docsConfig } = require('crd-utils') +const conf = require('./conf/webpack.config.prod') +require('colors-cli/toxic') + +module.exports = function serve(program) { + if (!fs.existsSync(docsConfig)) { + console.log('please check config.yml in root dir!\n') + return + } + const webpackConf = conf(program) + const compiler = webpack(webpackConf) + compiler.run((err, stats) => { + if (err) { throw err } + // 官方输出参数 + // https://webpack.js.org/configuration/stats/ + // https://github.com/webpack/webpack/issues/538#issuecomment-59586196 + /* eslint-disable */ + console.log(stats.toString({ + colors: true, + children: false, + chunks: false, + modules: false, + errors: true, + errorDetails: true, + errorStack: true, + warningsFilter: (warning) => { + return true + } + })) + }) +} diff --git a/packages/crd-scripts/src/commands/initProject.js b/packages/crd-scripts/src/commands/initProject.js new file mode 100644 index 00000000..d5d423e5 --- /dev/null +++ b/packages/crd-scripts/src/commands/initProject.js @@ -0,0 +1,51 @@ +const path = require('path') +const { execSync } = require('child_process') +const fs = require('fs-extra') +const { templatePath, resolveApp } = require('crd-utils') +const copyTemplate = require('copy-template-dir') +const paths = require('../conf/path') + +const log = console.log; // eslint-disable-line + +// eslint-disable-next-line no-unused-vars +module.exports = function (params) { + // process.argv[2] means the first argument after react-doc, eg react doc abc, process.argv[2] is abc + const outDir = path.join(paths.projectPath, process.argv[2]) + const projectName = path.basename(outDir) + + const crdpkg = require(paths.crdPackage); // eslint-disable-line + // replace the last vertion with x, so it'll install autoly when the last vertion changes. + const CRD_VERSION = crdpkg.version.split('.').slice(0, 2).concat('x').join('.') + + // clear output dir + if (!fs.pathExistsSync(outDir)) { + fs.ensureDirSync(outDir) + } + + const defaultTemplatePath = `${templatePath}/default` + + // copy template, see https://github.com/MuYunyun/create-react-doc/issues/50 + if (!fs.pathExistsSync(defaultTemplatePath)) { + execSync('mkdir temp && cd temp && yarn add crd-templates -D') + } + const templatePathInTemp = resolveApp('temp/node_modules/crd-templates/default') + if (fs.pathExistsSync(templatePathInTemp)) { + copyTemplate(templatePathInTemp, outDir, { + name: projectName, + crdVersion: CRD_VERSION, + }, (err, createdFiles) => { + if (err) return log(`Copy Tamplate Error: ${err} !!!`.red) + createdFiles.sort().forEach((createdFile) => { + log(` ${'create'.green} ${createdFile.replace(paths.projectPath, '')}`) + }) + // to hack https://github.com/yoshuawuyts/copy-template-dir/issues/16 + execSync(`cp -r ${templatePathInTemp}/.github ${outDir}`) + execSync('rm -rf temp') + log('\n initialization finished!\n'.green) + const cmdstr = `cd ${projectName} && yarn && yarn start`.cyan + log(` Run the ${cmdstr} to start the website.\n\n`) + }) + } else { + log(` ❎ crd-templates install fail.\n\n`) + } +} diff --git a/packages/crd-scripts/src/commands/initTheme.js b/packages/crd-scripts/src/commands/initTheme.js new file mode 100644 index 00000000..2ac2570a --- /dev/null +++ b/packages/crd-scripts/src/commands/initTheme.js @@ -0,0 +1,44 @@ +/** + * This file is to init theme quickly. + */ +const path = require('path') +const { execSync } = require('child_process') +const fs = require('fs-extra') +const { templatePath } = require('crd-utils') +const copyTemplate = require('copy-template-dir') +const paths = require('../conf/path') + +const log = console.log; // eslint-disable-line + +// eslint-disable-next-line no-unused-vars +module.exports = function (themeName) { + const outDir = path.join(paths.projectPath, themeName) + + const crdpkg = require(paths.crdPackage); // eslint-disable-line + // replace the last vertion with x, so it'll install autoly when the last vertion changes. + const CRD_VERSION = crdpkg.version.split('.').slice(0, 2).concat('x').join('.') + + // clear output dir + if (!fs.pathExistsSync(outDir)) { + fs.ensureDirSync(outDir) + } + + const defaultTemplatePath = `${templatePath}/theme/default` + + if (!fs.pathExistsSync(defaultTemplatePath)) { + execSync('mkdir temp && cd temp && yarn add crd-templates -D') + } + copyTemplate(defaultTemplatePath, outDir, { + name: themeName, + crdVersion: CRD_VERSION, + }, (err, createdFiles) => { + if (err) return log(`Copy Tamplate Error: ${err} !!!`.red) + createdFiles.sort().forEach((createdFile) => { + log(` ${'create'.green} ${createdFile.replace(paths.projectPath, '')}`) + }) + execSync('rm -rf temp') + log('\n initialization finished!\n'.green) + const cmdstr = `cd ${themeName} && yarn && yarn start`.cyan + log(` Run the ${cmdstr} to start the website.\n\n`) + }) +} diff --git a/packages/crd-scripts/src/conf/getDirTree.js b/packages/crd-scripts/src/conf/getDirTree.js new file mode 100644 index 00000000..5deb1ed9 --- /dev/null +++ b/packages/crd-scripts/src/conf/getDirTree.js @@ -0,0 +1,26 @@ +const { directoryTree } = require('./node-directory-tree') + +const getDirTree = (cmd) => { + const dir = cmd.markdownPaths + const dirs = Array.isArray(dir) ? dir : [dir] + const otherProps = { + mdconf: true, + extensions: /\.md/, + prerender: true, + } + const mapTagsWithArticle = [] + const dirTree = dirs.map(path => directoryTree({ + path, + options: otherProps, + mapTagsWithArticle + })) + return { + dirTree, + // map tags with path. [{ tagName: 'custom Tag 1', mapArticle: [{ path, name }]}] + mapTagsWithArticle + } +} + +module.exports = { + getDirTree, +} diff --git a/packages/crd-scripts/src/conf/getPrerenderRoutes.js b/packages/crd-scripts/src/conf/getPrerenderRoutes.js new file mode 100644 index 00000000..d5b046b9 --- /dev/null +++ b/packages/crd-scripts/src/conf/getPrerenderRoutes.js @@ -0,0 +1,51 @@ +// eg: ['docs/quick_start.md', 'a'] +// output: ['/quick_start', '/a/b', '/a/b/c'] +const getPrerenderRoutes = (dirTree) => { + const dpCloneDirTree = JSON.parse(JSON.stringify(dirTree)) + const result = recursiveDirTree(dpCloneDirTree) + result.push('/404') + result.push('/tags') + return result +} + +function recursiveDirTree(data) { + return recursive( + data, + '', + [] + ) +} + +function recursive( + data, + routePath, + prerenderRouteArr, +) { + data.forEach((item) => { + const { mdconf } = item || {} + const { abbrlink, tags } = mdconf || {} + const composeRouteName = `${routePath}/${item.name}`.replace(/.md$/, '') + + if (item.type === 'directory') { + if (item.children && item.children.length > 0) { + item.children = recursive( + item.children, + composeRouteName, + prerenderRouteArr, + ) + } else { + item.children = [] + } + } else if (item.type === 'file') { + const prerenderRouteName = abbrlink + ? `/${abbrlink}` + : composeRouteName + prerenderRouteArr.push(prerenderRouteName) + } + }) + return prerenderRouteArr +} + +module.exports = { + getPrerenderRoutes +} diff --git a/packages/crd-scripts/src/conf/node-directory-tree.js b/packages/crd-scripts/src/conf/node-directory-tree.js new file mode 100644 index 00000000..deeac6f3 --- /dev/null +++ b/packages/crd-scripts/src/conf/node-directory-tree.js @@ -0,0 +1,193 @@ +/* eslint-disable no-undef */ +const fs = require('fs') +const PATH = require('path') +const YAML = require('yamljs') +const { execSync } = require('child_process') +const { + replaceForFrontMatter, + generateRandomId, +} = require('crd-utils') +const { getDigitFromDir, timeFormat } = require('../utils') + +const constants = { + DIRECTORY: 'directory', + FILE: 'file', +} + +function safeReadDirSync(path) { + let dirData = {} + try { + /** + * to make sure it's ordered like such rules, link issue: https://github.com/nodejs/node/issues/3232 + * 1.x 1.x + * 2.x instead of 10.x + * 10.x 2.x + */ + dirData = fs.readdirSync(path).sort((dir1, dir2) => { + const dir1Digit = getDigitFromDir(dir1) + const dir2Digit = getDigitFromDir(dir2) + if (dir1Digit && dir2Digit) { + return dir1Digit - dir2Digit + } + return 0 + }) + } catch (ex) { + if (ex.code === 'EACCES') + // User does not have permissions, ignore directory + // eslint-disable-next-line brace-style + { return null } + throw ex + } + return dirData +} + +// collect unique tags from all articles. +const tagsArr = [] + +/** build directory Tree, fork from https://github.com/mihneadb/node-directory-tree + * path: path for file + * options: { + * exclude: RegExp|RegExp[] - A RegExp or an array of RegExp to test for exclusion of directories. + * extensions : RegExp - A RegExp to test for exclusion of files with the matching extension. + * mdconf: Boolean. + * prerender: Boolean. Used for prerender. + * generate: Boolean. Used for generating info in front-matter. + * } + * mapTagsWithArticle: [{ + * tagName: 'customTag1', // tag type name + * mapArticle: [{ + * path, // click tag to jump route such as /tags/customTag1 + * title // the name of article + * }] + * }] + */ +function directoryTree({ + path, + options, + routePath = '', + mapTagsWithArticle +}) { + const name = PATH.basename(path, '.md') + const item = { name } + const routePropsCurrent = `${routePath}/${name}` + if (!options.prerender) { + item.path = path + } + let stats + try { + stats = fs.statSync(path) + } catch (e) { + return null + } + + // Skip if it matches the exclude regex + if (options && options.exclude && options.exclude.test(path)) return null + + if (stats.isFile()) { + const ext = PATH.extname(path).toLowerCase() + + // Skip if it does not match the extension regex + if (options && options.extensions && !options.extensions.test(ext)) { return null } + + if (options && options.mdconf) { + item.type = constants.FILE + const contentStr = fs.readFileSync(path).toString() + if (!contentStr) return + const contentMatch = contentStr.match(/^/) + /** generate abbrlink in FrontMatter */ + if (options.generate) { + const randomId = generateRandomId(8) + if (!contentMatch) { + replaceForFrontMatter({ + path, + target: `\n` + }) + } + if (contentMatch && contentMatch[1].indexOf('abbrlink') === -1) { + replaceForFrontMatter({ + path, + source: contentMatch[1], + target: `\nabbrlink: ${randomId}${contentMatch[1]}` + }) + console.log('✅ replaceForFrontMatter success') + } + } + + const yamlParse = contentMatch ? YAML.parse(contentMatch[1]) : {} + const { tags: articleTags, abbrlink } = yamlParse + if (Array.isArray(articleTags) && Array.isArray(mapTagsWithArticle)) { + const cpArticleTags = Array.from(new Set(articleTags)) + for (let i = 0; i < cpArticleTags.length; i++) { + const articleTag = cpArticleTags[i] + const articleTagIndex = tagsArr.indexOf(articleTag) + if (articleTagIndex > -1) { + mapTagsWithArticle[articleTagIndex]['mapArticle'].push({ + path: abbrlink ? `/${abbrlink}` : routePropsCurrent, + title: name + }) + } else { + tagsArr.push(cpArticleTags[i]) + mapTagsWithArticle.push({ + tagName: cpArticleTags[i], + mapArticle: [{ + path: abbrlink ? `/${abbrlink}` : routePropsCurrent, + title: name + }] + }) + } + } + } + + item.mdconf = yamlParse + try { + // see https://stackoverflow.com/questions/2390199/finding-the-date-time-a-file-was-first-added-to-a-git-repository/2390382#2390382 + const result = execSync(`git log --format=%aD ${path} | tail -1`) + item.birthtime = + Buffer.isBuffer(result) && timeFormat(new Date(result)) + } catch (error) { + console.log(`❎ error: ${error.message}`) + } + try { + // see https://stackoverflow.com/questions/22497597/get-the-last-modification-data-of-a-file-in-git-repo + const result = execSync(`git log -1 --pretty="format:%ci" ${path}`) + item.mtime = Buffer.isBuffer(result) && timeFormat(new Date(result)) + } catch (error) { + console.log(`❎ error: ${error.message}`) + } + item.size = stats.size // File size in bytes + item.extension = ext + if (!options.prerender) { + item.relative = item.path.replace(process.cwd(), '') + item.isEmpty = contentMatch + ? !String.prototype.trim.call(contentStr.replace(contentMatch[0], '')) + : true + const uglifyContent = contentStr.replace(/\s/g, '') + item.content = uglifyContent + } + } + } else if (stats.isDirectory()) { + const dirData = safeReadDirSync(path) + if (dirData === null) return null + item.children = dirData + .map(child => + directoryTree({ + path: PATH.join(path, child), + options, + routePath: routePropsCurrent, + mapTagsWithArticle + }), + ) + .filter(e => !!e) + item.type = constants.DIRECTORY + if (!options.prerender) { + item.size = item.children.reduce((prev, cur) => prev + cur.size, 0) + } + } else { + return null // Or set item.size = 0 for devices, FIFO and sockets ? + } + return item +} + +module.exports = { + directoryTree +} diff --git a/packages/crd-scripts/src/conf/path.js b/packages/crd-scripts/src/conf/path.js new file mode 100644 index 00000000..2b6b9624 --- /dev/null +++ b/packages/crd-scripts/src/conf/path.js @@ -0,0 +1,143 @@ +const path = require('path') +const fs = require('fs') +const { execSync } = require('child_process') +const { resolveApp, docsConfig, getDocsConfig } = require('crd-utils') +const chalk = require('chalk') + +// handle the problem of symbol in any platform +const appDirectory = fs.realpathSync(process.cwd()) + +const modPath = resolveApp('node_modules') +// get config crd from package.json +function getCrdConf() { + const packagePath = resolveApp('./package.json') + let conf = {} + if (fs.existsSync(packagePath)) { + const confPkg = require(packagePath); // eslint-disable-line + conf = confPkg.crd + } + return conf +} + +function getConfigFilePath(fileName, type) { + const conf = getCrdConf() + // read config + if (conf && conf[type]) { + // load theme dir + if (type === 'theme') { + if (!conf[type]) conf[type] = fileName + const _path = path.resolve(appDirectory, 'theme', conf[type]) + const _NodeModulesPath = path.resolve( + appDirectory, + 'node_modules', + conf[type], + ) + if (fs.existsSync(_path)) { + return fs.realpathSync(_path) + } else if (fs.existsSync(_NodeModulesPath)) { + return fs.realpathSync(_NodeModulesPath) + } + return false + } + if (/^(favicon|logo)$/.test(type)) { + return path.resolve(appDirectory, conf[type]) + } + } + const _filepath = path.resolve(appDirectory, fileName) + if (fs.existsSync(_filepath)) { + // favicon|logo in default root dir. + return _filepath + } + return false +} + +// Get favicon path +const faviconPath = () => { + const _path = getConfigFilePath('./favicon.ico', 'favicon') + if (_path) return _path + // the path'll be writen dynamiclly in the future + return resolveApp('node_modules/crd-theme/favicon.ico') +} + +// Get logo path +const logoPath = () => { + const _path = getConfigFilePath('./logo.svg', 'logo') + if (_path) return _path + return false +} + +let theme = '' +// theme in develop mode. +let devTheme = false + +const getTheme = () => { + if (docsConfig) { + const docsConfigObj = getDocsConfig() + if (!docsConfigObj) return + if (docsConfigObj.devTheme) { + devTheme = docsConfigObj.devTheme + theme = docsConfigObj.devTheme + } else { + theme = docsConfigObj.theme + + // install custom theme + if (!fs.existsSync(resolveApp(`node_modules/${theme}`))) { + // todo: chalkblue(xxx) not show in the terminal + chalk.blue(`Install theme ${theme}`) + // -W means ignore-workspace-root-check + execSync(`yarn add ${theme} -D -W`) + chalk.blue(`Install theme ${theme} done`) + } else { + chalk.blue(`Upgrade theme ${theme}`) + // -W means ignore-workspace-root-check + execSync(`yarn upgrade ${theme}`) + chalk.blue(`Upgrade theme ${theme} done`) + } + } + } +} + +getTheme() + +// get exclude folders +function getExcludeFoldersRegExp() { + if (!fs.existsSync(modPath)) return [] + let regexp = fs.readdirSync(modPath) + /** whitelist to include */ + const whiteListRegExp = new RegExp(`create-react-doc(.*)|crd-scripts|crd-theme|${theme}`) + regexp = regexp.filter( + item => !whiteListRegExp.test(item), + // item => !/create-react-doc(.*)|crd-scripts|crd-theme/.test(item), + ) + regexp = regexp.map((item) => { + let rgxPath = `node_modules${path.sep}${item}` + if (path.sep === '\\') { + // to watch: is '\\' needful? + rgxPath = `node_modules\\${path.sep}${item}` + } + return new RegExp(rgxPath) + }) + return regexp +} + +// crd tool dir +const toolDirectory = fs.realpathSync(__dirname) +const resolveTool = relativePath => path.resolve(toolDirectory, relativePath) + +module.exports = { + // markdown dir + crdConf: getCrdConf(), + defaultTheme: devTheme + ? resolveApp(`${devTheme}`) + : resolveApp(`node_modules/${theme}`), + defaultNodeModules: modPath, + projectPath: appDirectory, + publicPath: '', + logoPath: logoPath(), + // crd tool dir + getExcludeFoldersRegExp: getExcludeFoldersRegExp(), + crdPackage: resolveTool('../../package.json'), + defaultFaviconPath: faviconPath(), + appIndexJs: resolveTool('../web/index.js'), + appDir: resolveTool('../web'), +} diff --git a/packages/crd-scripts/src/conf/rawTreeReplaceLoader.js b/packages/crd-scripts/src/conf/rawTreeReplaceLoader.js new file mode 100644 index 00000000..817894b1 --- /dev/null +++ b/packages/crd-scripts/src/conf/rawTreeReplaceLoader.js @@ -0,0 +1,82 @@ +// A variation of https://github.com/react-doc/node-directory-tree-md/blob/master/lib/directory-tree-md.js +const { directoryTree } = require('./node-directory-tree') +const PATH = require('path') +const { ifInGitIgnore } = require('../utils/index') + +function getAllWatchPath(arr, pathArr = []) { + arr.forEach((item) => { + const mdfilePathInProject = item.path.replace( + process.cwd() + PATH.sep, + '', + ) + if (!ifInGitIgnore(mdfilePathInProject) && item.type === 'file') { + pathArr.push(item.path) + } + if (item.children && item.children.length > 0) { + pathArr.concat(getAllWatchPath(item.children, pathArr)) + } + }) + return pathArr +} + +function replacePath(dirs, path) { + for (let i = 0; i < dirs.length; i += 1) { + const element = PATH.dirname(dirs[i]) + const reg = new RegExp(`^${element}`, 'gi') + if (reg.test(path)) { + path = path.replace(reg, '') + break + } + } + return path +} + +function getRelativePath(arr, relativePath, dirs) { + const pathArr = [] + arr.forEach((item) => { + if (relativePath && item.path) { + item.path = replacePath(dirs, item.path) + } + if (item.children && item.children.length > 0) { + item.children = getRelativePath(item.children, relativePath, dirs) + } + const notInGitIgnore = !ifInGitIgnore(item.path.replace(PATH.sep, '')) + if (notInGitIgnore) { + pathArr.push(item) + } + }) + return pathArr +} + +module.exports = function (source) { + // get option config from webpack loader, here is https://github.com/MuYunyun/create-react-doc/blob/main/packages/scripts/src/conf/webpack.config.prod.js#L61-L70 + const options = this.getOptions() || {} + const { include, directoryTrees } = options + const { dir, relativePath, ...otherProps } = directoryTrees + let content = typeof source === 'string' ? JSON.parse(source) : source + // It's said loader resuls are flagged as cacheable. See https://webpack.js.org/api/loaders/#thiscacheable. + // if (this.cacheable) this.cacheable() + if (directoryTrees && (!include || include.test(this.resourcePath))) { + const dirs = Array.isArray(dir) ? dir : [dir] + const dirTree = dirs.map(path => directoryTree({ + path, + options: otherProps, + })) + + const filemd = getAllWatchPath(dirTree) + filemd.forEach((fileItem) => { + this.addDependency(fileItem) + }) + // replace full file path with relative path + content = getRelativePath( + dirTree, + relativePath, + dirs, + ) + } + content = JSON.stringify(content) + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') + + return `module.exports = ${content}` +} diff --git a/packages/crd-scripts/src/conf/webpack.config.dev.js b/packages/crd-scripts/src/conf/webpack.config.dev.js new file mode 100644 index 00000000..9853e7be --- /dev/null +++ b/packages/crd-scripts/src/conf/webpack.config.dev.js @@ -0,0 +1,141 @@ +const autoprefixer = require('autoprefixer') +const webpack = require('webpack') +const path = require('path') +const upath = require('upath') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { defaultHTMLPath } = require('crd-utils') +const FriendlyErrorsWebpackPlugin = require('@nuxtjs/friendly-errors-webpack-plugin') +const { getDocsConfig } = require('crd-utils') +const { getDirTree } = require('./getDirTree') +const config = require('./webpack.config') +const paths = require('./path') + +module.exports = function (cmd) { + const docsConfig = getDocsConfig() + const { mapTagsWithArticle } = getDirTree(cmd) + config.mode = 'development' + config.devtool = 'eval-source-map' + config.entry = [ + require.resolve('react-hot-loader/patch'), + require.resolve('webpack-hot-dev-clients/webpackHotDevClient'), + paths.appIndexJs, + ] + config.output.publicPath = '/' + config.module.rules = config.module.rules.map((item) => { + if (item.oneOf) { + const loaders = [] + loaders.push({ + // Process JS with Babel. + test: /\.(js|jsx)$/, + exclude: paths.getExcludeFoldersRegExp.concat(/\.(cache)/), + use: [ + { + loader: require.resolve('string-replace-loader'), + options: { + multiple: [ + { search: '__project_root__', replace: upath.normalizeSafe(paths.projectPath), flags: 'ig' }, + { search: '__project_theme__', replace: upath.normalizeSafe(paths.defaultTheme), flags: 'ig' }, + ], + }, + }, + { + loader: 'esbuild-loader', + options: { + loader: 'jsx', + target: 'es2015', + // This will make esbuild automatically generate import statements, + // making the ProviderPlugin unnecesary if used only for "react". + // Note that this option makes sense only when used in conjuction + // with React >16.40.0 || >17 + // https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + jsx: 'automatic', + } + }, + ], + }) + // https://ilikekillnerds.com/2018/03/disable-webpack-4-native-json-loader/ + loaders.push({ + test: /crd\.json$/, + // 禁用 Webpack 4 本身的 JSON 加载程序 + type: 'javascript/auto', + use: [ + { + loader: `${path.join(__dirname, './rawTreeReplaceLoader.js')}`, + options: { + include: /crd\.json$/, + directoryTrees: { + dir: cmd.markdownPaths, + mdconf: true, + extensions: /\.md/, + relativePath: true, + }, + }, + }, + ], + }) + + loaders.push({ + test: /\.(css|less)$/, + use: [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: { + modules: true, + localIdentName: '[local]--[hash:base64:5]', + importLoaders: 1, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: { + // Necessary for external CSS imports to work + // https://github.com/facebookincubator/create-react-app/issues/2677 + ident: 'postcss', + plugins: () => [ + require("postcss-flexbugs-fixes"), // eslint-disable-line + autoprefixer({ + browsers: [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie < 9', // React doesn't support IE8 anyway + ], + flexbox: 'no-2009', + }), + ], + }, + }, + require.resolve('less-loader'), + ], + }) + + item.oneOf = loaders.concat(item.oneOf) + } + return item + }) + + config.optimization = { + // 将模块名称添加到工厂功能,以便它们显示在浏览器分析器中。 + // 当接收到热更新信号时,在浏览器 console 控制台打印更多可读性高的模块名称等信息 + moduleIds: 'named', + } + + config.plugins = config.plugins.concat([ + new webpack.DefinePlugin({ + env: JSON.stringify('dev'), + mapTagsWithArticle: JSON.stringify(mapTagsWithArticle) + }), + new webpack.HotModuleReplacementPlugin(), + new HtmlWebpackPlugin({ + inject: true, + favicon: paths.defaultFaviconPath, + template: defaultHTMLPath, + title: docsConfig && docsConfig.title ? docsConfig.title : 'Create React Doc', + }), + new FriendlyErrorsWebpackPlugin({ + clearConsole: true, + }), + ]) + return config +} diff --git a/packages/crd-scripts/src/conf/webpack.config.js b/packages/crd-scripts/src/conf/webpack.config.js new file mode 100644 index 00000000..86f4cd4d --- /dev/null +++ b/packages/crd-scripts/src/conf/webpack.config.js @@ -0,0 +1,138 @@ +/* eslint-disable global-require */ +/* eslint-disable import/no-dynamic-require */ +const webpack = require('webpack') +const webpackbar = require('webpackbar') +const fs = require('fs') +const { resolveApp, docsConfig, docsBuildDist } = require('crd-utils') +const { getDocsConfig } = require('crd-utils') +// const { getSearchContent } = require('../utils'); +const remarkMath = require('remark-math') +const rehypeKatex = require('rehype-katex') +const paths = require('./path') +const pkg = require('../../package.json') + +const define = { + FOOTER: null, + DOCSCONFIG: null, + INJECT: null, +} +if (paths.crdConf && paths.crdConf.footer && typeof paths.crdConf.footer === 'string') { + define.FOOTER = JSON.stringify(paths.crdConf.footer) +} +/* custom define docs config */ +if (docsConfig) { + // const searchContent = getSearchContent(); + const docsConfigObj = getDocsConfig() + define.DOCSCONFIG = JSON.stringify(docsConfigObj) + // todo: searchContent affects the performance, so take annotation here templately. + // define.SEARCHCONTENT = searchContent && searchContent.toString(); + + // if there is inject logic in docsConfigObj + if (docsConfigObj && docsConfigObj.inject && fs.existsSync(resolveApp(docsConfigObj.inject))) { + define.INJECT = require(resolveApp(docsConfigObj.inject)) + } +} + +module.exports = { + entry: {}, + output: { + path: docsBuildDist, + publicPath: paths.publicPath, + filename: 'js/[name].[hash:8].js', + chunkFilename: 'js/[name].[hash:8].js', + }, + module: { + rules: [ + { + // “oneOf”将遍历所有以下加载程序,直到一个符合要求。 + // 当没有加载器匹配时,它将返回到加载程序列表末尾的“file”加载器。 + oneOf: [ + { + test: /\.(svg|png|bmp|jpg|jpeg|gif)$/, + loader: require.resolve('url-replace-loader'), + options: { + limit: 10000, + name: 'img/[name].[hash:8].[ext]', + replace: [ + { + test: /crd\.logo\.svg$/, + path: paths.logoPath, + }, + ], + }, + }, + { + test: /\.md$/, + use: [ + { + loader: 'esbuild-loader', + options: { + loader: 'jsx', + target: 'es2015', + // This will make esbuild automatically generate import statements, + // making the ProviderPlugin unnecesary if used only for "react". + // Note that this option makes sense only when used in conjuction + // with React >16.40.0 || >17 + // https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + jsx: 'automatic', + } + }, + { + loader: require.resolve('@mdx-js/loader'), + options: { + remarkPlugins: [ + [ + remarkMath, + { + /* options */ + }, + ], + ], + rehypePlugins: [ + [ + rehypeKatex, + { + /* options */ + }, + ], + ], + }, + }, + ], + exclude: /(node_modules)/, + }, + // “file-loader”确保这些资源由WebpackDevServer服务。 + // 当您导入资源时,您将获得(虚拟)文件名。 + // 在生产中,它们将被复制到`build`文件夹。 + // 此加载程序不使用“test”,因此它将捕获所有模块 + { + // 排除`js`文件以保持“css”加载器工作,因为它注入它的运行时,否则将通过“文件”加载器处理。 + // 还可以排除“html”和“json”扩展名,以便它们被webpacks内部加载器处理。 + exclude: [/\.js$/, /\.html$/, /\.json$/], + loader: require.resolve('file-loader'), + options: { + name: 'static/[name].[hash:8].[ext]', + }, + }, + ], + }, + ], + }, + plugins: [ + // eslint-disable-next-line new-cap + new webpackbar({ name: pkg.name }), + new webpack.DefinePlugin({ + VERSION: JSON.stringify(pkg.version), + ...define, + }), + // fix "process is not defined" error: + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + resolve: { + fallback: { + path: require.resolve('path-browserify'), + }, + }, +} diff --git a/packages/crd-scripts/src/conf/webpack.config.prod.js b/packages/crd-scripts/src/conf/webpack.config.prod.js new file mode 100755 index 00000000..6a83103c --- /dev/null +++ b/packages/crd-scripts/src/conf/webpack.config.prod.js @@ -0,0 +1,216 @@ +const autoprefixer = require('autoprefixer') +const path = require('path') +const upath = require('upath') +const webpack = require('webpack') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const CopyMarkdownImageWebpackPlugin = require('copy-markdown-image-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const PrerenderSPAPlugin = require('crd-prerender-spa-plugin') +const { generateSiteMap } = require('crd-generator-sitemap') +const fs = require('fs-extra') +const { defaultHTMLPath, docsBuildDist } = require('crd-utils') +const { getDocsConfig } = require('crd-utils') +const config = require('./webpack.config') +const paths = require('./path') +const { getPrerenderRoutes } = require('./getPrerenderRoutes') +const { getDirTree } = require('./getDirTree') + +const Renderer = PrerenderSPAPlugin.PuppeteerRenderer + +module.exports = function (cmd) { + const docsConfig = getDocsConfig() + const { dirTree, mapTagsWithArticle } = getDirTree(cmd) + const routes = getPrerenderRoutes(dirTree) + + config.mode = 'production' + config.entry = [paths.appIndexJs] + // config.output.filename = 'js/[hash:8].js' + config.output.chunkFilename = 'js/[name].[hash:8].js' + config.output.publicPath = docsConfig.repo ? `/${docsConfig.repo}/` : '/' + config.output.path = docsConfig.repo ? `${docsBuildDist}/${docsConfig.repo}` : docsBuildDist + config.module.rules = config.module.rules.map((item) => { + if (item.oneOf) { + const loaders = [] + loaders.push({ + // Process JS with Babel. + test: /\.(js|jsx)$/, + exclude: paths.getExcludeFoldersRegExp.concat(/\.(cache)/), + use: [ + { + loader: require.resolve('string-replace-loader'), + options: { + multiple: [ + { search: '__project_root__', replace: upath.normalizeSafe(paths.projectPath), flags: 'ig' }, + { search: '__project_theme__', replace: upath.normalizeSafe(paths.defaultTheme), flags: 'ig' }, + ], + }, + }, + { + loader: 'esbuild-loader', + options: { + loader: 'jsx', + target: 'es2015', + // This will make esbuild automatically generate import statements, + // making the ProviderPlugin unnecesary if used only for "react". + // Note that this option makes sense only when used in conjuction + // with React >16.40.0 || >17 + // https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + jsx: 'automatic', + } + }, + ], + }) + // https://ilikekillnerds.com/2018/03/disable-webpack-4-native-json-loader/ + loaders.push({ + test: /crd\.json$/, + // 禁用 Webpack 4 本身的 JSON 加载程序 + type: 'javascript/auto', + use: [ + { + loader: `${path.join(__dirname, './rawTreeReplaceLoader.js')}`, + options: { + include: /crd\.json$/, // 检查包含的文件名字 + directoryTrees: { + // 指定目录生成目录树,json + dir: cmd.markdownPaths, + mdconf: true, + extensions: /\.md/, + relativePath: true, + }, + }, + }, + ], + }) + + loaders.push({ + test: /\.(css|less)$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: require.resolve('css-loader'), + options: { + modules: true, + localIdentName: '[local]-[hash:base64:5]', + importLoaders: 1, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: { + // Necessary for external CSS imports to work + // https://github.com/facebookincubator/create-react-app/issues/2677 + ident: 'postcss', + plugins: () => [ + require('postcss-flexbugs-fixes'), // eslint-disable-line + autoprefixer({ + browsers: [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie < 9', // React doesn't support IE8 anyway + ], + flexbox: 'no-2009', + }), + ], + }, + }, + require.resolve('less-loader'), + ], + }) + + item.oneOf = loaders.concat(item.oneOf) + } + return item + }) + config.optimization = { + // minimize: true, + // minimizer: [ + // // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line + // // `...`, + // new CssMinimizerPlugin(), + // ], + } + + config.plugins = config.plugins.concat([ + new webpack.DefinePlugin({ + env: JSON.stringify('prod'), + mapTagsWithArticle: JSON.stringify(mapTagsWithArticle) + }), + new HtmlWebpackPlugin({ + inject: true, + favicon: paths.defaultFaviconPath, + template: defaultHTMLPath, + title: + docsConfig && docsConfig.title ? docsConfig.title : 'Create React Doc', + minify: { + removeAttributeQuotes: true, + collapseWhitespace: true, + html5: true, + minifyCSS: true, + removeComments: true, + removeEmptyAttributes: true, + }, + }), + new CopyMarkdownImageWebpackPlugin({ + dir: cmd.markdownPaths, + toDir: config.output.path, + }), + // new webpack.optimize.DedupePlugin(), + new MiniCssExtractPlugin({ + // Options similar to the same options in webpackOptions.output + // both options are optional + filename: 'css/[contenthash].css', + chunkFilename: 'css/[id].css', + }), + new PrerenderSPAPlugin({ + // Required - The path to the webpack-outputted app to prerender. + staticDir: docsBuildDist, + outputDir: docsConfig.repo + ? `${docsBuildDist}/${docsConfig.repo}` + : docsBuildDist, + indexPath: docsConfig.repo + ? `${docsBuildDist}/${docsConfig.repo}/index.html` + : `${docsBuildDist}/index.html`, + // Required - Routes to render. + routes, + successCb: async () => { + if (docsConfig.repo) { + // not use fs.move here or it'll throw error in github action + await fs.copy(`${docsBuildDist}/${docsConfig.repo}`, docsBuildDist) + await fs.remove(`${docsBuildDist}/${docsConfig.repo}`) + const defaultPath = (dirTree.find(data => data.name === 'README.md') + && dirTree.find(data => data.name === 'README.md').mdconf + && dirTree.find(data => data.name === 'README.md').mdconf.abbrlink) || 'README' + // move README as root index.html + await fs.copy(`${docsBuildDist}/${defaultPath}/index.html`, `${docsBuildDist}/index.html`) + console.log('✅ generate prerender file success!') + if (docsConfig.seo) { + if (docsConfig.seo.google) { + fs.writeFileSync(`${docsBuildDist}/sitemap.xml`, generateSiteMap(routes)) + } + } + console.log('✅ generate sitemap file success!') + } + }, + // The actual renderer to use. (Feel free to write your own) + // Available renderers: https://github.com/Tribex/prerenderer/tree/master/renderers + renderer: new Renderer({ + // Optional - The name of the property to add to the window object with the contents of `inject`. + injectProperty: '__PRERENDER_INJECTED', + // Optional - Any values you'd like your app to have access to via `window.injectProperty`. + inject: { + prerender: true, + }, + // Optional - defaults to 0, no limit. + // Routes are rendered asynchronously. + // Use this to limit the number of routes rendered in parallel. + maxConcurrentRoutes: 4, + // https://pptr.dev/#?product=Puppeteer&version=v5.5.0&show=api-pagegotourl-options + navigationOptions: { + timeout: 0 + } + }), + }), + ]) + return config +} diff --git a/packages/crd-scripts/src/conf/webpack.config.server.js b/packages/crd-scripts/src/conf/webpack.config.server.js new file mode 100644 index 00000000..4de9437d --- /dev/null +++ b/packages/crd-scripts/src/conf/webpack.config.server.js @@ -0,0 +1,34 @@ +const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' + +module.exports = (cmd, webpackConf) => { + return { + // 启用生成文件的gzip压缩。 + compress: true, + // 沉默WebpackDevServer自己的日志,因为它们通常没有用处。 + // 这个设置仍然会显示编译警告和错误。 + clientLogLevel: 'none', + // contentBase: conf.output.appPublic, + publicPath: webpackConf.output.publicPath, + hot: true, + historyApiFallback: { + // 带点的路径仍应使用历史回退。 + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, + }, + // historyApiFallback: true, + // WebpackDevServer默认是嘈杂的,所以我们发出自定义消息 + // 通过上面的`compiler.plugin`调用来监听编译器事件。 + quiet: true, + // 如果HTTPS环境变量设置为“true”,则启用HTTPS + https: protocol === 'https', + // 告诉服务器从哪里提供内容。提供静态文件,这只是必要的。 + contentBase: cmd.markdownPaths, + // 通知服务器观察由devServer.contentBase选项提供的文件。 + // it'll reload page when file change. + watchContentBase: true, + // avoid cpu overload in some case + watchOptions: { + ignored: /node_modules/, + }, + } +} diff --git a/packages/crd-scripts/src/deploy.js b/packages/crd-scripts/src/deploy.js new file mode 100644 index 00000000..3f48e5c5 --- /dev/null +++ b/packages/crd-scripts/src/deploy.js @@ -0,0 +1,40 @@ +const ghpages = require('gh-pages') +const loading = require('loading-cli') + +const log = console.log; // eslint-disable-line + +module.exports = function server(cmd, docsConfig) { + if (!docsConfig) { + console.log('please check config.yml in root dir!\n') + return + } + if (!docsConfig.user || !docsConfig.repo) { + console.log('please check user and repo in config.yml!\n') + return + } + const { user, repo, publish } = docsConfig + log(' Start deploy to your git repo'.green) + const load = loading({ + text: 'Please wait ...'.blue, + color: 'blue', + interval: 100, + stream: process.stdout, + }).start() + + ghpages.publish( + cmd.output, + { + branch: cmd.branch, + repo: publish || `https://github.com/${user}/${repo}.git`, + message: `Update website, ${new Date()}!`, + }, + (err) => { + load.stop() + if (err) { + return log(err) + } + log(`\n Push to ${cmd.branch} success!\n`.green.bold) + }, + ) +} + diff --git a/packages/crd-scripts/src/generate.js b/packages/crd-scripts/src/generate.js new file mode 100644 index 00000000..89043823 --- /dev/null +++ b/packages/crd-scripts/src/generate.js @@ -0,0 +1,23 @@ +const fs = require('fs') +const { docsConfig } = require('crd-utils') +const { directoryTree } = require('./conf/node-directory-tree') + +module.exports = function generate(program) { + if (!fs.existsSync(docsConfig)) { + console.log('❎ please check config.yml in root dir!\n') + return + } + + const dir = program.markdownPaths + const dirs = Array.isArray(dir) ? dir : [dir] + const otherProps = { + mdconf: true, + extensions: /\.md/, + generate: true + } + dirs.map(path => directoryTree({ + path, + options: otherProps, + })) + console.log('✅ generate success!') +} diff --git a/packages/crd-scripts/src/server.js b/packages/crd-scripts/src/server.js new file mode 100644 index 00000000..a30fe164 --- /dev/null +++ b/packages/crd-scripts/src/server.js @@ -0,0 +1,52 @@ +const webpack = require('webpack') +const WebpackDevServer = require('webpack-dev-server') +const openBrowsers = require('open-browsers') +const detect = require('detect-port') +const fs = require('fs') +const { docsConfig } = require('crd-utils') +const prepareUrls = require('local-ip-url/prepareUrls') +const conf = require('./conf/webpack.config.dev') +const createDevServerConfig = require('./conf/webpack.config.server') +require('colors-cli/toxic') + +function clearConsole() { + // process.stdout.write(process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'); +} + +module.exports = function server(cmd) { + if (!fs.existsSync(docsConfig)) { + console.log('please check config.yml in root dir!\n') + return + } + const HOST = cmd.host + let DEFAULT_PORT = cmd.port + const webpackConf = conf(cmd) + const compiler = webpack(webpackConf) + + detect(DEFAULT_PORT).then((_port) => { + if (DEFAULT_PORT !== _port) DEFAULT_PORT = _port + + const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' + const urls = prepareUrls({ protocol, host: HOST, port: DEFAULT_PORT }) + // https://webpack.js.org/api/compiler-hooks/#aftercompile + // print log after being compiled + compiler.hooks.done.tap('done', () => { + /* eslint-disable */ + console.log(`Dev Server Listening at Local: ${urls.localUrl.green}`); + console.log(` On Your Network: ${urls.lanUrl.green}`); + console.log(`\nTo create a production build, use ${'npm run build'.blue_bt}.`); + /* eslint-enable */ + }) + + new WebpackDevServer(compiler, createDevServerConfig(cmd, webpackConf)).listen(DEFAULT_PORT, HOST, (err) => { + if (err) { + return console.log(err); // eslint-disable-line + } + clearConsole() + // open browser + openBrowsers(urls.localUrl) + }) + }).catch((err) => { + console.log(err); // eslint-disable-line + }) +} diff --git a/packages/crd-scripts/src/utils/index.js b/packages/crd-scripts/src/utils/index.js new file mode 100644 index 00000000..5b9af2be --- /dev/null +++ b/packages/crd-scripts/src/utils/index.js @@ -0,0 +1,52 @@ +const fs = require('fs') +const { docsGitIgnore, searchFilePath } = require('crd-utils') + +/** + * judege cur file if in git ignore. + */ +exports.ifInGitIgnore = (mdfilePathInProject) => { + let gitIgnoreContentArr = [] + if (fs.existsSync(docsGitIgnore)) { + const gitIgnoreContent = fs.readFileSync(docsGitIgnore) + gitIgnoreContentArr = gitIgnoreContent.toString().split('\n') + } + return gitIgnoreContentArr.indexOf(mdfilePathInProject) > -1 +} + +/** + * to get dight from cur dir. + * If there are order && unorder file in one same folder, the unorder file'll be in front of order file. + * eg: + * '1.xx' => 1 + * 'xx' => 0 + */ +exports.getDigitFromDir = (dir) => { + const matchedResult = dir.match(/^((\d)*)\.(\s|\S)*$/) + if (matchedResult && matchedResult[1]) { + return parseInt(matchedResult[1], 10) + } + return 0 +} + +function paddingTwoDigits(digit) { + return digit < 10 ? `0${digit}` : digit +} + +/** + * format time + */ +exports.timeFormat = (date) => { + if (isNaN(date.getFullYear()) || isNaN(date.getMonth()) || isNaN(date.getDate())) return null + return `${date.getFullYear()}-${paddingTwoDigits( + date.getMonth() + 1, + )}-${paddingTwoDigits(date.getDate())}` +} + +exports.getSearchContent = () => { + if (!fs.existsSync(searchFilePath)) { + console.log('there is no find .cache/search.js in root dir!\n') + return null + } + return fs.readFileSync(searchFilePath) +} + diff --git a/packages/crd-scripts/src/utils/initCache.js b/packages/crd-scripts/src/utils/initCache.js new file mode 100644 index 00000000..5105856d --- /dev/null +++ b/packages/crd-scripts/src/utils/initCache.js @@ -0,0 +1,71 @@ +const write = require('write') +const path = require('path') +const { cacheDirPath, getDocsConfig } = require('crd-utils') +const { directoryTree } = require('../conf/node-directory-tree') + +module.exports = function (program, cb) { + const treeData = program.markdownPaths.map((markdownPath) => { + return directoryTree({ + path: markdownPath, + options: { + mdconf: true, // Markdown config for exsiting file. + extensions: /\.md/, + }, + }) + }) + // to collect search data + const searchData = [] + const docsConfig = getDocsConfig() + const useSearchPlugin = docsConfig.search && docsConfig.host + + function dfsMap(data) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < data.length; i++) { + if (data[i].children) { + dfsMap(data[i].children) + } else { + const searchMapKeys = docsConfig.search_map ? Object.keys(docsConfig.search_map) : [] + // eslint-disable-next-line no-plusplus + for (let x = 0; x < searchMapKeys.length; x++) { + if (data[i].relative) { + const searchMapIndex = data[i].relative.indexOf(searchMapKeys[x]) + if (searchMapIndex !== -1 && typeof searchMapIndex === 'number') { + const effectedPath = data[i].relative.replace( + searchMapKeys[x], + docsConfig.search_map[searchMapKeys[x]], + ) + searchData.push({ + title: data[i].name, + url: `${effectedPath.replace(/.md/g, '')}`, + content: data[i].content, + }) + break + } + } + } + } + } + } + if (useSearchPlugin) { + // README + searchData.push({ + title: 'README', + url: 'README', + content: treeData[0].content, + }) + // map treeData to generate search data source + dfsMap(treeData) + const writeSearchPath = path.resolve( + process.cwd(), + cacheDirPath, + 'search.js', + ) + write.sync( + writeSearchPath, + `${JSON.stringify( + searchData, + )}`, + ) + } + cb() +} diff --git a/packages/crd-scripts/src/web/Router.js b/packages/crd-scripts/src/web/Router.js new file mode 100644 index 00000000..b8502ed7 --- /dev/null +++ b/packages/crd-scripts/src/web/Router.js @@ -0,0 +1,75 @@ +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import theme from 'crd-theme' +import menuSource from './crd.json' + +/** + * serialize router data + */ +function routeData(data, arrayRoute = [], routePath = '/', article) { + data.forEach((item) => { + const routePropsCurrent = `${routePath}${item.name}`.replace(/.md$/, '') + const { mdconf, ...otherItem } = item + arrayRoute.push({ + path: routePropsCurrent, + mdconf: mdconf || { title: item.name }, + props: { ...otherItem }, + article: article || item.name, + }) + if (item.children && item.children.length > 0) { + arrayRoute.concat(routeData(item.children, arrayRoute, `${routePropsCurrent}/`, article || item.name)) + } + }) + return arrayRoute +} + +function menuSourceFormat(data, routePath, article) { + const arr = [] + data.forEach((item) => { + const routePropsCurrent = `${routePath || ''}/${item.name}`.replace(/.md$/, '') + if (item.type === 'directory') { + if (item.children && item.children.length > 0) { + item.title = item.name.replace(item.extension, '') + item.mdconf = {} + item.props = { isEmpty: true } + item.children = menuSourceFormat(item.children, routePropsCurrent, article || item.name) + } else { + item.title = item.name.replace(item.extension, '') + item.mdconf = { title: item.name } + item.props = { isEmpty: true } + item.children = [] + } + } else { + item.title = item.mdconf && item.mdconf.title ? item.mdconf.title : item.name.replace(item.extension, '') + if (!item.mdconf) { + item.props = { isEmpty: true } + } + } + item.routePath = routePropsCurrent + item.article = article || item.name + arr.push(item) + }) + return arr +} + +const RoutersContainer = ({ ...props }) => { + return ( + + + + ) +} + +export default function RouterRoot() { + return ( + + + + ) +} diff --git a/packages/crd-scripts/src/web/crd.json b/packages/crd-scripts/src/web/crd.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/crd-scripts/src/web/crd.json @@ -0,0 +1 @@ +[] diff --git a/packages/crd-scripts/src/web/index.js b/packages/crd-scripts/src/web/index.js new file mode 100644 index 00000000..ef26ec5b --- /dev/null +++ b/packages/crd-scripts/src/web/index.js @@ -0,0 +1,33 @@ +import { hydrate } from 'react-dom' +import { renderToString } from 'react-dom/server'; +// import { hydrateRoot } from 'react-dom/client' +import { ifDev, ifPrerender } from 'crd-client-utils' +import RouterRoot from './Router' + +if (ifDev) { + // dev render + document.getElementById('root').innerHTML = renderToString() + hydrate( + , + document.getElementById('root'), + ) + // hydrateRoot( + // document.getElementById('root'), + // , + // ) +} else if (ifPrerender) { + // prerender + document.getElementById('root').innerHTML = renderToString() +} else { + // prod render: + // It'll cause some [unkown error](https://github.com/MuYunyun/create-react-doc/issues/278) using hydrateRoot here. + // So still using hydrate temporarily. + hydrate( + , + document.getElementById('root'), + ) + // hydrateRoot( + // document.getElementById('root'), + // , + // ) +} diff --git a/packages/crd-seed/.npmrc b/packages/crd-seed/.npmrc new file mode 100644 index 00000000..7edb6af8 --- /dev/null +++ b/packages/crd-seed/.npmrc @@ -0,0 +1,10 @@ +# .npmrc + +registry=https://registry.npmjs.org/ + +# https://github.com/sass/node-sass#binary-configuration-parameters +sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ + +# https://github.com/Medium/phantomjs#deciding-where-to-get-phantomjs +# phantomjs_cdnurl=http://cnpmjs.org/downloads +phantomjs_cdnurl=https://npm.taobao.org/dist/phantomjs diff --git a/packages/crd-seed/README.md b/packages/crd-seed/README.md new file mode 100644 index 00000000..96c7d883 --- /dev/null +++ b/packages/crd-seed/README.md @@ -0,0 +1,17 @@ +## 主题 + +create-react-doc 提供了官方默认主题 [crd-seed](https://github.com/MuYunyun/create-react-doc/tree/main/packages/crd-seed)。该主题支持以下特性: + +* 适配移动、PC 多端展示。 +* 支持暗黑模式。 +* 文档支持内嵌 codepen、codesandbox。 +* GitHub 联动。 + +使用该主题搭建的项目有: + +* [blog](https://github.com/MuYunyun/blog), [站点](http://muyunyun.cn/blog) + * ![](http://with.muyunyun.cn/90d3e357a31649b9466a828a92b6d88d.jpg) + * ![](http://with.muyunyun.cn/2e7440e4256debda2d73a4e6392c7146.jpg-300) +* [diana](https://github.com/MuYunyun/diana), [站点](https://muyunyun.cn/diana/) + +如果您想定制化或者分享个人主题, 可以参考[自定义主题](http://muyunyun.cn/create-react-doc/自定义主题)章节。 \ No newline at end of file diff --git a/packages/crd-seed/component/Affix/affix.js b/packages/crd-seed/component/Affix/affix.js new file mode 100644 index 00000000..d2bf4965 --- /dev/null +++ b/packages/crd-seed/component/Affix/affix.js @@ -0,0 +1,129 @@ +import { useState, useEffect, useRef } from 'react' +import { throttle } from './utils' + +const Affix = ({ + offsetTop, + offsetBottom, + children, + target, + onChange, + className, + wrapperClassName, + style, + width, + affixStyle, +}) => { + const placeholderRef = useRef(null) + const wrapperRef = useRef(null) + const widthRef = useRef(width) + const [positionStyle, setPositionStyle] = useState({}) + // 滚动元素 + let scrollElm = window + // 是否是绝对布局模式 + const fixedRef = useRef(false) + const [fixed, setFixed] = useState(fixedRef.current) + + useEffect(() => { + widthRef.current = width + }, [width]) + + useEffect(() => { + // 在子节点移开父节点后保持原来占位 + setWrapperDimension() + }, [fixed, width]) + + useEffect(() => { + if (target) scrollElm = target() + scrollElm.addEventListener('scroll', scroll) + return () => { + if (target) scrollElm = target() + scrollElm.removeEventListener('scroll', scroll) + } + }, [offsetTop, offsetBottom]) + + const validValue = (value) => { + return typeof value === 'number' + } + const setWrapperDimension = () => { + const { width: wrapperRefWidth, height: wrapperRefHeight } = wrapperRef.current + ? wrapperRef.current.getBoundingClientRect() + : {} + placeholderRef.current && + (placeholderRef.current.style.height = `${wrapperRefHeight}px`) + placeholderRef.current && + (placeholderRef.current.style.width = + typeof width === 'number' ? `${width}px` : `${wrapperRefWidth}px`) + wrapperRef.current && + (wrapperRef.current.style.width = + typeof width === 'number' ? `${width}px` : `${wrapperRefWidth}px`) + } + const updateFixed = () => { + fixedRef.current = !fixedRef.current + setFixed(fixedRef.current) + } + const handleScroll = () => { + const rect = + placeholderRef.current && placeholderRef.current.getBoundingClientRect() + if (!rect) return + let { top, bottom } = rect + const updatePositionStyle = { + width: + typeof widthRef.current === 'number' + ? widthRef.current + : placeholderRef.current && + placeholderRef.current.getBoundingClientRect().width, + zIndex: 999, + } + let containerTop = 0 // 容器距离视口上侧的距离 + let containerBottom = 0 // 容器距离视口下侧的距离 + + if (scrollElm === window) { + bottom = window.innerHeight - bottom + } else { + const containerRect = scrollElm && scrollElm.getBoundingClientRect() + containerTop = containerRect && containerRect.top + containerBottom = containerRect && containerRect.bottom + top -= containerTop // 距离容器顶部的距离 + bottom = containerBottom - bottom // 距离容器底部的距离 + } + + if ( + (validValue(offsetTop) && top <= offsetTop) || + (validValue(offsetBottom) && bottom <= offsetBottom) + ) { + if (!fixedRef.current) { + updatePositionStyle.position = 'fixed' + validValue(offsetTop) && (updatePositionStyle.top = offsetTop + containerTop) + validValue(offsetBottom) && + (updatePositionStyle.bottom = + scrollElm === window + ? bottom + : window.innerHeight - (containerBottom - offsetBottom)) + onChange && onChange(true) + updateFixed() + setPositionStyle(updatePositionStyle) + } + } else if (fixedRef.current) { + updatePositionStyle.position = 'relative' + onChange && onChange(false) + updateFixed() + setPositionStyle(updatePositionStyle) + } + } + + const scroll = throttle(handleScroll, 20) + + return ( +
+
+ {children} +
+
+ ) +} + +export default Affix diff --git a/packages/crd-seed/component/Affix/index.js b/packages/crd-seed/component/Affix/index.js new file mode 100644 index 00000000..11cb04b2 --- /dev/null +++ b/packages/crd-seed/component/Affix/index.js @@ -0,0 +1,3 @@ +import Affix from './affix' + +export default Affix diff --git a/packages/crd-seed/component/Affix/utils/index.js b/packages/crd-seed/component/Affix/utils/index.js new file mode 100644 index 00000000..6f0f7334 --- /dev/null +++ b/packages/crd-seed/component/Affix/utils/index.js @@ -0,0 +1,25 @@ +const throttle = (fn, wait) => { + let inThrottle + let lastFn + let lastTime + return function () { + const context = this + // eslint-disable-next-line prefer-rest-params + const args = arguments + if (!inThrottle) { + fn.apply(context, args) + lastTime = Date.now() + inThrottle = true + } else { + clearTimeout(lastFn) + lastFn = setTimeout(() => { + if (wait - (Date.now() - lastTime) <= 0) { + fn.apply(context, args) + lastTime = Date.now() + } + }, Math.max(wait - (Date.now() - lastTime), 0)) + } + } +} + +export { throttle } diff --git a/packages/crd-seed/component/Footer/index.js b/packages/crd-seed/component/Footer/index.js new file mode 100644 index 00000000..3fa51f6d --- /dev/null +++ b/packages/crd-seed/component/Footer/index.js @@ -0,0 +1,41 @@ +import cx from 'classnames' +import styles from './index.less' + +const version = VERSION; // eslint-disable-line +const footer = FOOTER; // eslint-disable-line + +const FooterView = ({ inlineCollapsed }) => { + return ( +
+ {footer ? ( +
+ ) : ( + <> +
+ {`Powered by${' '}`} + + Create React Doc + + . +
+
+ + | + +
+ + + )} +
+ ) +} + +export default FooterView diff --git a/packages/crd-seed/component/Footer/index.less b/packages/crd-seed/component/Footer/index.less new file mode 100644 index 00000000..651028d7 --- /dev/null +++ b/packages/crd-seed/component/Footer/index.less @@ -0,0 +1,34 @@ +.footer { + font-size: 14px; + text-align: center; + border-top: 1px solid #e9e9e9; + margin: 50px 0 0 240px; + padding: 20px 0 50px 0; + clear: both; + color: #999; + transition: margin .2s ease-in-out; + + a { + color: #758AC5; + + &:hover { + color: #0800ff; + } + } + + .powered_by { + margin-bottom: 8px; + } + + &-inlineCollapsed { + margin: 50px 0 0 0; + } + + .uv_count, .pv_count { + margin-left: 5px; + } + + .split { + margin: 0 5px; + } +} \ No newline at end of file diff --git a/packages/crd-seed/component/Header/index.js b/packages/crd-seed/component/Header/index.js new file mode 100644 index 00000000..170c959a --- /dev/null +++ b/packages/crd-seed/component/Header/index.js @@ -0,0 +1,90 @@ +import { useState } from 'react' +import cx from 'classnames' +import { Link } from 'react-router-dom' +import Switch from 'react-switch' +import { ifProd } from 'crd-client-utils' +import { isMobile } from '../../utils' +import Search from '../Search' +import styles from './index.less' + +const Header = ({ + className, + logo, +}) => { + // eslint-disable-next-line no-undef + const { user, repo, tags } = DOCSCONFIG || {} + const [checked, setChecked] = useState(false) + const handleChange = (value) => { + value + ? document.body.classList.add(styles.darkMode) + : document.body.classList.remove(styles.darkMode) + setChecked(value) + } + return ( +
+
+ +
+ {logo && logo} + {!isMobile && ( + + {(DOCSCONFIG && DOCSCONFIG.title) || 'Create React Doc'} + + )} +
+ + {DOCSCONFIG && DOCSCONFIG.search ? : null} +
+
+ { + tags + ? + 标签 + + : null + } + + } + checkedIcon={ + moon + } + /> + + + +
+
+ ) +} + +export default Header diff --git a/packages/crd-seed/component/Header/index.less b/packages/crd-seed/component/Header/index.less new file mode 100644 index 00000000..80bc804a --- /dev/null +++ b/packages/crd-seed/component/Header/index.less @@ -0,0 +1,117 @@ +@import '../../style/base.less'; + +.logo { + float: left; + padding: 0 0 0 15px; + + img { + height: 28px; + vertical-align: middle; + } + + span { + vertical-align: middle; + } + + img+span { + margin-left: 10px; + } +} + +.header { + box-shadow: 0 2px 8px #f0f1f2; + line-height: 60px; + height: 60px; + position: relative; + width: 100%; + top: 0; + z-index: @header-zIndex; + display: flex; + + .wrapper { + flex: 1; + display: flex; + align-items: center; + + .titleLink { + display: inline-block; + } + + .search { + margin-left: 40px; + max-width: 200px; + display: flex; + align-items: center; + } + + .select { + width: 200px; + } + } + + .rightArea { + display: flex; + align-items: center; + + .sun { + position: absolute; + left: 5px; + top: 4px; + } + + .moon { + position: absolute; + right: 5px; + top: 4px; + } + + .github-corner { + display: block; + + &:hover { + .octo-arm { + animation: octocat-wave 560ms ease-in-out; + } + } + } + + svg { + vertical-align: bottom; + } + } +} + +.tags { + font-size: 15px; + margin-right: 16px; +} + +@keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(100deg); + } +} + +.pageTitle { + font-size: 30px; + line-height: 38px; + color: #0d1a26; + font-weight: 500; + margin-bottom: 20px; + margin-top: 8px; + padding-left: 20px; +} + +.darkMode { + filter: invert(100%) hue-rotate(180deg); + + .no-dark-mode { + filter: invert(100%) hue-rotate(180deg); + } +} \ No newline at end of file diff --git a/packages/crd-seed/component/Icon/Icon.js b/packages/crd-seed/component/Icon/Icon.js new file mode 100644 index 00000000..fc4cbd40 --- /dev/null +++ b/packages/crd-seed/component/Icon/Icon.js @@ -0,0 +1,38 @@ +import { useEffect } from 'react' +import cx from 'classnames' +import loadSprite from './loadSprite' +import styles from './style/index.less' + +/* omit some props depends on arr */ +const omit = (props, arr) => + Object.keys(props) + .filter(k => arr.indexOf(k) === -1) + // eslint-disable-next-line no-sequences + .reduce((acc, key) => ((acc[key] = props[key]), acc), {}) + +function Icon(props) { + const { type, color, prefixCls = 'icon', size, style, className, ...rest } = props + + useEffect(() => { + loadSprite(props) + }, [type]) + + const newClassName = cx(styles[prefixCls], className) + const cloneStyle = { ...style } + if (color) { + cloneStyle.color = color + } + if (size) { + cloneStyle.fontSize = size + } + + const restProps = omit(rest, ['svgContent']) + + return ( + + + + ) +} + +export default Icon diff --git a/packages/crd-seed/component/Icon/iconsInfo.js b/packages/crd-seed/component/Icon/iconsInfo.js new file mode 100644 index 00000000..3970706d --- /dev/null +++ b/packages/crd-seed/component/Icon/iconsInfo.js @@ -0,0 +1,14 @@ +export const IconsInfo = { + folder: + '', + file: + '', + edit: + '', + 'update-time': + '', + 'create-time': + '', + search: + '', +} diff --git a/packages/crd-seed/component/Icon/index.js b/packages/crd-seed/component/Icon/index.js new file mode 100644 index 00000000..dc6d0a6b --- /dev/null +++ b/packages/crd-seed/component/Icon/index.js @@ -0,0 +1,3 @@ +import Icon from './Icon' + +export default Icon diff --git a/packages/crd-seed/component/Icon/loadSprite.js b/packages/crd-seed/component/Icon/loadSprite.js new file mode 100644 index 00000000..5f9feb4d --- /dev/null +++ b/packages/crd-seed/component/Icon/loadSprite.js @@ -0,0 +1,64 @@ +import { IconsInfo } from './iconsInfo' + +/* tslint:disable:max-line-length */ +// inspried by https://github.com/kisenka/svg-sprite-loader/blob/master/runtime/browser-sprite.js +// Much simplified, do make sure run this after document ready +const svgSprite = contents => ` + +` +/** + * '' => + * ' viewBox="0 0 1024 1024"> { + return svgContent.split('svg')[1] +} + +const renderSvgSprite = (props) => { + const { svgContent, type } = props + const customIconsInfo = svgContent + ? { + [`${type}`]: svgContent, + } + : {} + const mergeSvgInfo = { ...IconsInfo, ...customIconsInfo } + const symbols = Object.keys(mergeSvgInfo) + .map((iconName) => { + const getContent = handleSvgContent(mergeSvgInfo[iconName]) + return `` + }) + .join('') + return svgSprite(symbols) +} + +const loadSprite = (props) => { + const { type, svgContent } = props + if (!document) { + return + } + const existing = document.getElementById('__CRD_SVG_SPRITE_NODE__') + const mountNode = document.body + + if (!existing) { + mountNode && + typeof mountNode.insertAdjacentHTML === 'function' && + mountNode.insertAdjacentHTML('afterbegin', renderSvgSprite(props)) + } else if (svgContent) { + const defs = existing.children[0] + const svgChildren = defs.children + const svgChildrenIds = svgChildren ? [].slice.call(svgChildren).map(r => (r).id) : [] + if (svgChildrenIds.indexOf(type) !== -1) return + defs.innerHTML += `` + } +} + +export default loadSprite diff --git a/packages/crd-seed/component/Icon/style/index.less b/packages/crd-seed/component/Icon/style/index.less new file mode 100644 index 00000000..382c3d3b --- /dev/null +++ b/packages/crd-seed/component/Icon/style/index.less @@ -0,0 +1,8 @@ +.icon { + font-size: 24px; + vertical-align: middle; + fill: currentColor; + background-size: cover; + width: 1em; + height: 1em; +} diff --git a/packages/crd-seed/component/Menu/Menu.js b/packages/crd-seed/component/Menu/Menu.js new file mode 100644 index 00000000..a2555f3b --- /dev/null +++ b/packages/crd-seed/component/Menu/Menu.js @@ -0,0 +1,75 @@ +import { useState } from 'react' +import cx from 'classnames' +import MenuItem from './MenuItem' +import { SubMenu } from './SubMenu' +import { MenuProvider } from './context' +import styles from './style/index.less' + +const Menu = ({ + theme = 'light', + children, + selectedKey, + onSelect = () => {}, + inlineCollapsed = false, + defaultOpenKeys = [], + menuStyle, + toggle, +}) => { + /* 存储 hover 状态的 key 值, 在垂直模式中需要根据 hover 的 key 值高亮父节点 */ + const [hoverKey, setHoverKey] = useState('') + const MenuContext = { + theme, + mode: 'inline', + inlineCollapsed, + defaultOpenKeys, + selectedKey, + onSelect, + hoverKey, + onHoverKey: setHoverKey, + } + const renderToggle = () => { + return ( +
+ +
+ ) + } + const renderMenu = () => { + return ( +
    + {children} +
+ ) + } + + return ( + + {renderToggle()} + {renderMenu()} + + ) +} + +Menu.Item = MenuItem +Menu.SubMenu = SubMenu + +export default Menu diff --git a/packages/crd-seed/component/Menu/MenuItem.js b/packages/crd-seed/component/Menu/MenuItem.js new file mode 100644 index 00000000..25105ad8 --- /dev/null +++ b/packages/crd-seed/component/Menu/MenuItem.js @@ -0,0 +1,62 @@ +import { useEffect, useRef } from 'react' +import cx from 'classnames' +import { getMenuStyle } from './util' +import { useMenuContext } from './context' +import styles from './style/index.less' + +function MenuItem({ + title = '', + icon, + keyValue = '', + level = 0, +}) { + const { + theme, + selectedKey, + onSelect, + onHoverKey, + inlineCollapsed + } = useMenuContext() + const menuItemRef = useRef(null) + const menuItemselected = keyValue.indexOf(selectedKey) > -1 + const menuUnFoldDelayTime = 300 + + useEffect(() => { + if (menuItemselected && (inlineCollapsed === false)) { + setTimeout(() => { + menuItemRef.current.scrollIntoView({ + block: 'center', + behavior: 'smooth' + }) + }, menuUnFoldDelayTime) + } + }, [keyValue, selectedKey, inlineCollapsed]) + + const handleOnClick = () => { + onSelect(keyValue) + } + + const renderMenuItem = () => { + return ( +
  • { + onHoverKey(keyValue) + }} + onMouseLeave={() => onHoverKey('')} + onClick={handleOnClick} + style={getMenuStyle(level, 'menuItem')} + ref={menuItemRef} + > + {icon ? {icon} : null} + {title} +
  • + ) + } + + return renderMenuItem() +} + +export default MenuItem diff --git a/packages/crd-seed/component/Menu/SubMenu.js b/packages/crd-seed/component/Menu/SubMenu.js new file mode 100644 index 00000000..fd7170ff --- /dev/null +++ b/packages/crd-seed/component/Menu/SubMenu.js @@ -0,0 +1,160 @@ +import { useState, useRef, Fragment, Children, cloneElement } from 'react' +import cx from 'classnames' +import { useEnhancedEffect } from 'crd-client-utils' +import Transition from './transition' +import { getMenuStyle } from './util' +import { useMenuContext } from './context' +import styles from './style/index.less' + +function useCurrent( + initialValue +) { + const currentRef = useRef(initialValue) + const [state, setState] = useState(initialValue) + currentRef.current = state + const set = (value) => { + currentRef.current = value + setState(value) + } + const get = () => currentRef.current + return [get, set] +} + +function SubMenu({ + children, + title, + icon, + level = 0, + keyValue = '', + onTitleClick = () => {}, +}) { + const { + selectedKey, + mode, + hoverKey, + onHoverKey, + defaultOpenKeys = [], + } = useMenuContext() + const [menuOpen, setMenuOpen] = useState(defaultOpenKeys.indexOf(keyValue) !== -1) + const curSubmenu = useRef(null) + const popupSubMenu = useRef(null) + + const [getParentMenuHover, setParentMenuHover] = useCurrent(false) + + const gapDistance = 4 + + useEnhancedEffect(() => { + if (popupSubMenu.current && curSubmenu.current) { + popupSubMenu.current.style.left = `${curSubmenu.current.getBoundingClientRect().right + + gapDistance}px` + popupSubMenu.current.style.top = `${curSubmenu.current.getBoundingClientRect().top}px` + } + }, [getParentMenuHover()]) + + /** + * judege if is React Fragment. + */ + function isReactFragment(variableToInspect) { + if (variableToInspect.type) { + return variableToInspect.type === Fragment + } + return variableToInspect === Fragment + } + + /* 行内模式下, 渲染子节点 */ + const renderChild = (child) => { + return ( + // eslint-disable-next-line quotes + <> + {Children.map(child || children, (reactNode) => { + if (!reactNode || typeof reactNode !== 'object') { + return null + } + const childElement = reactNode + if ( + isReactFragment(childElement) && + childElement.props.children + ) { + return renderChild(childElement.props.children) + } + return cloneElement(childElement, { + level: level + 1, + ...childElement.props, + }) + // eslint-disable-next-line quotes + })} + + ) + } + + const handleParentMouseEnter = () => { + setParentMenuHover(true) + onHoverKey(keyValue) + } + + const handleParentMouseLeave = () => { + onHoverKey('') + } + + /* 处理 menu 开闭状态 */ + const handleMenuStatus = () => { + onTitleClick(keyValue) + mode === 'inline' && setMenuOpen(!menuOpen) + } + + /* 判断 subMenu 是否被选中, 当子节点被选中时, 父节点也会被高亮; + 同时在 vertical 模式时, 当子节点被 hover 时, 父节点也会被高亮; */ + const judgeSubmenuSelect = (reactChildren) => { + const result = Children.toArray(reactChildren).some((reactNode) => { + if (!reactNode || typeof reactNode !== 'object') { + return false + } + + const childElement = reactNode + // eslint-disable-next-line no-shadow + const { keyValue } = childElement.props + const originKey = keyValue ? String(keyValue) : '' + if (childElement.type.name === 'MenuItem') { + return selectedKey.split('').indexOf(originKey) !== -1 || hoverKey === originKey + } + if (childElement.type.name === 'SubMenu') { + return judgeSubmenuSelect(childElement.props.children) || hoverKey === originKey + } + return false + }) + return result + } + + return ( +
  • onHoverKey(keyValue)} + ref={curSubmenu} + > +
    + + {icon ? {icon} : null} + {title} +
    + {mode === 'inline' ? ( + +
      {renderChild()}
    +
    + ) : null} +
  • + ) +} + +export { SubMenu } diff --git a/packages/crd-seed/component/Menu/context.js b/packages/crd-seed/component/Menu/context.js new file mode 100644 index 00000000..a8d8ab50 --- /dev/null +++ b/packages/crd-seed/component/Menu/context.js @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react' + +const MenuContext = createContext(undefined) + +export const MenuProvider = ({ children, value }) => ( + {children} +) + +export const useMenuContext = () => useContext(MenuContext) diff --git a/packages/crd-seed/component/Menu/index.js b/packages/crd-seed/component/Menu/index.js new file mode 100644 index 00000000..7e045e4b --- /dev/null +++ b/packages/crd-seed/component/Menu/index.js @@ -0,0 +1,3 @@ +import Menu from './Menu' + +export default Menu diff --git a/packages/crd-seed/component/Menu/style/index.less b/packages/crd-seed/component/Menu/style/index.less new file mode 100644 index 00000000..42b23e0b --- /dev/null +++ b/packages/crd-seed/component/Menu/style/index.less @@ -0,0 +1,295 @@ +@import './theme.less'; + +.menu { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0; + color: @menu-color; + background: @menu-background; + user-select: none; + transition: background .2s ease-in-out, width .2s ease-in-out; + overflow: auto; + user-select: none; + font-weight: 600; + + &-item { + // margin: 0 16px 0 15px; + white-space: nowrap; + cursor: pointer; + height: 40px; + line-height: 40px; + padding: 0 16px; + color: @menu-color; + position: relative; + cursor: pointer; + // overflow: hidden; + // white-space: nowrap; + // text-overflow: ellipsis; + + &-selected { + position: relative; + background: @menu-background-selected; + color: @menu-color-selected; + + a { + color: @menu-color-selected!important; + } + } + + /* icon */ + i { + margin-right: 10px; + } + + a { + color: rgba(0, 0, 0, 0.8); + cursor: pointer; + display: inline-block; + text-decoration: none; + + &::before { + top: 0; + left: 0; + right: 0; + bottom: 0; + content: ''; + position: absolute; + background-color: transparent; + } + } + + &-title { + vertical-align: middle; + } + } + + &-item:hover { + color: @menu-color-hover; + + & .menu-icon { + color: @menu-color-hover; + } + } + + .submenu { + line-height: 40px; + cursor: pointer; + overflow: hidden; + + &-title { + position: relative; + padding: 0 28px 0 0; + font-size: 14px; + cursor: pointer; + margin-left: 20px; + // overflow: hidden; + // white-space: nowrap; + // text-overflow: ellipsis; + } + + &-title:hover { + color: @menu-color-hover; + + .submenu-arrow { + + &::before, + &::after { + background: @menu-color-hover; + } + } + } + + &-arrow { + display: inline-block; + width: 10px; + flex-shrink: 0; + // margin: 0 3px 0 1px; + margin: 0 3px 0 -12px; + position: relative; + top: -5px; + + &::before, + &::after { + content: ''; + position: absolute; + width: 6px; + height: 1.5px; + background: @arrow-background; + transition: background .2s cubic-bezier(0.645, 0.045, 0.355, 1), + transform .2s cubic-bezier(0.645, 0.045, 0.355, 1), + top .2s cubic-bezier(0.645, 0.045, 0.355, 1), + -webkit-transform .2s cubic-bezier(0.645, 0.045, 0.355, 1); + } + + &::before { + transform: rotate(45deg) translateY(-2px); + } + + &::after { + transform: rotate(-45deg) translateY(2px); + } + + &-open { + &::before { + transform: rotate(-45deg) translateX(2px); + } + + &::after { + transform: rotate(45deg) translateX(-2px); + } + } + } + + &-selected { + color: @menu-color-selected; + } + } + + &-inline { + width: 100%; + + .menu-item { + &-selected { + &::after { + content: ''; + position: absolute; + width: 3px; + background: @menu-inline-selected; + top: 0; + bottom: 0; + right: 0; + } + } + } + + .submenu { + &-arrow { + &::before { + transform: rotate(45deg) translateY(-2px); + } + + &::after { + transform: rotate(-45deg) translateY(2px); + } + + &-open { + &::before { + transform: rotate(-45deg) translateX(2px); + } + + &::after { + transform: rotate(45deg) translateX(-2px); + } + } + } + } + + /* fold */ + &-collapsed { + width: 0px; + + .menu-item { + padding: 0 24px !important; + + &-title { + opacity: 0; + } + } + + .submenu { + &-title { + padding: 0 24px !important; + + &-field { + opacity: 0; + } + + .submenu-arrow { + display: none; + } + } + } + } + } + + &-icon { + margin-right: 4px; + } +} + +/* submenu fold/unfold animation */ +.collapse-transition { + overflow: hidden; + transition: height .2s ease-in-out; +} + +.toggle { + position: absolute; + width: 30px; + height: 30px; + left: 240px; + top: 200px; + transition: left .2s linear; + box-shadow: 2px 0 8px rgba(0, 0, 0, .15); + border-radius: 0 4px 4px 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + + &-icon { + position: relative; + background: 0 0; + width: 12px; + height: 2px; + background: #333; + transition: background .2s cubic-bezier(.78, .14, .15, .86); + + &::before, + &::after { + content: ''; + display: block; + position: absolute; + background: #333; + width: 50%; + height: 2px; + } + + &::before { + transform: translateY(-2px) rotate(-45deg); + } + + &::after { + transform: translateY(2px) rotate(45deg); + } + + &-close { + background: #333; + + &::before, + &::after { + content: ''; + display: block; + position: absolute; + background: #333; + width: 100%; + height: 2px; + transform: rotate(0deg); + } + + &::before { + top: -4px; + } + + &::after { + top: 4px; + } + } + } + + &-collapsed { + left: 0px; + } +} \ No newline at end of file diff --git a/packages/crd-seed/component/Menu/style/theme.less b/packages/crd-seed/component/Menu/style/theme.less new file mode 100644 index 00000000..096786ea --- /dev/null +++ b/packages/crd-seed/component/Menu/style/theme.less @@ -0,0 +1,8 @@ +/* light */ +@menu-color: rgba(0, 0, 0, 0.65); +@menu-background: #fff; +@menu-color-hover: #1890ff; +@menu-color-selected: #1890ff; +@menu-inline-selected: #1199ee; +@menu-background-selected: #e6f7ff; +@arrow-background: #333; \ No newline at end of file diff --git a/packages/crd-seed/component/Menu/transition.js b/packages/crd-seed/component/Menu/transition.js new file mode 100644 index 00000000..71f4546d --- /dev/null +++ b/packages/crd-seed/component/Menu/transition.js @@ -0,0 +1,93 @@ +import { useEffect, useRef, useCallback } from 'react' +import styles from './style/index.less' + +const ANIMATION_DURATION = 200 + +export default function Transition({ + isShow, + children, +}) { + const mounted = useRef(false) + const collapseRef = useRef(null) + const timer = useRef({}) + + // prepare + const beforeEnter = () => { + const el = collapseRef.current + el.style.height = '0px' + } + + const afterEnter = useCallback(() => { + const el = collapseRef.current + el.style.display = 'block' + el.style.height = '' + }, []) + + // start + const enter = useCallback(() => { + const el = collapseRef.current + el.style.display = 'block' + if (el.scrollHeight !== 0) { + el.style.height = `${el.scrollHeight}px` + } + + timer.current.enterTimer = setTimeout(() => afterEnter(), ANIMATION_DURATION) + }, [afterEnter]) + + const beforeLeave = useCallback(() => { + const el = collapseRef.current + + el.style.display = 'block' + if (el.scrollHeight !== 0) { + el.style.height = `${el.scrollHeight}px` + } + }, []) + + const afterLeave = useCallback(() => { + const el = collapseRef.current + + el.style.display = 'none' + el.style.height = '' + }, []) + + const leave = useCallback(() => { + const el = collapseRef.current + if (el.scrollHeight !== 0) { + el.style.height = '0px' + } + timer.current.leaveTimer = setTimeout(() => afterLeave(), ANIMATION_DURATION) + }, [afterLeave]) + + const triggerChange = useCallback( + (isShow) => { + clearTimeout(timer.current.enterTimer) + clearTimeout(timer.current.leaveTimer) + if (isShow) { + beforeEnter() + enter() + } else { + beforeLeave() + leave() + } + }, + [beforeLeave, enter, leave] + ) + + useEffect(() => { + if (!mounted.current) { + mounted.current = true + beforeEnter() + if (isShow) { + enter() + } + } else { + triggerChange(isShow) + } + }, [enter, isShow, triggerChange]) + + return ( +
    + {children} +
    + ) +} diff --git a/packages/crd-seed/component/Menu/util.js b/packages/crd-seed/component/Menu/util.js new file mode 100644 index 00000000..4a137926 --- /dev/null +++ b/packages/crd-seed/component/Menu/util.js @@ -0,0 +1,20 @@ +/* 获取 menu 样式 + level: 层级 +*/ +const getMenuStyle = (level, type) => { + const basicStyle = { + fontSize: level === 0 ? '14px' : '12px', + } + if (type === 'menuItem') { + return { + ...basicStyle, + paddingLeft: `${21 + (level * 16)}px`, + } + } + return { + ...basicStyle, + paddingLeft: `${level * 16}px`, + } +} + +export { getMenuStyle } diff --git a/packages/crd-seed/component/NoMatch/index.js b/packages/crd-seed/component/NoMatch/index.js new file mode 100644 index 00000000..f5e20f03 --- /dev/null +++ b/packages/crd-seed/component/NoMatch/index.js @@ -0,0 +1,21 @@ +import styles from './index.less' + +const NoMatch = () => { + // eslint-disable-next-line no-undef + const { user, repo } = DOCSCONFIG || {} + return ( + + + + + + +
    +

    404

    +
    你似乎来到了没有知识存在的荒原...
    +
    在 github 访问该项目
    +
    + ) +} + +export default NoMatch diff --git a/packages/crd-seed/component/NoMatch/index.less b/packages/crd-seed/component/NoMatch/index.less new file mode 100644 index 00000000..ebb66f94 --- /dev/null +++ b/packages/crd-seed/component/NoMatch/index.less @@ -0,0 +1,16 @@ +.noMatch { + position: relative; + width: 100%; + height: 80%; + text-align: center; + border-spacing: 0; + border-collapse: collapse; + color: #a2a2a2; + + h1 { + color: #717171; + font-size: 68px; + line-height: 54px; + font-weight: 500; + } +} \ No newline at end of file diff --git a/packages/crd-seed/component/Search/README.md b/packages/crd-seed/component/Search/README.md new file mode 100644 index 00000000..640d88e1 --- /dev/null +++ b/packages/crd-seed/component/Search/README.md @@ -0,0 +1,6 @@ +### API + +| props | description | type | default | +| :---------: | :---------: | :----: | :------: | +| placeholder | placeholder | string | 'Search' | +| className | css | string | -- | diff --git a/packages/crd-seed/component/Search/index.js b/packages/crd-seed/component/Search/index.js new file mode 100644 index 00000000..8a1fd809 --- /dev/null +++ b/packages/crd-seed/component/Search/index.js @@ -0,0 +1,55 @@ +import { useState, useEffect } from 'react' +import cx from 'classnames' +import Icon from '../Icon' +import styles from './index.less' + +const Search = ({ + placeholder = 'Search', + className, +}) => { + const [value, setValue] = useState('') + // const [searchContent, setSearchContent] = useState([]); + // const showSearchContent = value.length > 0 && searchContent.length > 0; + useEffect(() => { + /* eslint-disable-next-line no-undef */ + // if (SEARCHCONTENT) { + // /* eslint-disable-next-line no-undef */ + // const filterSearch = SEARCHCONTENT.filter((r) => { + // return r.title.includes(value) || r.content.includes(value); + // }); + // setSearchContent(filterSearch); + // } + }, [value]) + return ( +
    + + { + setValue(e.target.value) + }} + /> + {/* {showSearchContent ? ( +
      + {searchContent.map((search) => { + return ( +
    • { + location.hash = search.url; + }} + key={search.url} + > + {search.title} + {search.content} +
    • + ); + })} +
    + ) : null} */} +
    + ) +} + +export default Search diff --git a/packages/crd-seed/component/Search/index.less b/packages/crd-seed/component/Search/index.less new file mode 100644 index 00000000..cec3b9be --- /dev/null +++ b/packages/crd-seed/component/Search/index.less @@ -0,0 +1,50 @@ +.search { + position: relative; + + input { + line-height: 1.5; + font-size: 14px; + color: #999; + outline: 0; + border-radius: 4px; + border: 0; + width: 200px; + } + + .panel { + position: absolute; + width: 350px; + top: 50px; + left: 0; + background: rgba(255, 255, 255); + max-height: 400px; + box-shadow: rgba(101, 119, 134, 0.2) 0px 0px 15px, rgba(101, 119, 134, 0.15) 0px 0px 3px 1px; + overflow-y: auto; + + li { + padding: 0 5px; + list-style: none; + border: 1px solid rgb(230, 236, 240); + } + } + + .searchItem { + display: flex; + } + + .title { + display: inline-block; + width: 90px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .content { + display: inline-block; + width: 245px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +} \ No newline at end of file diff --git a/packages/crd-seed/component/Tags/index.js b/packages/crd-seed/component/Tags/index.js new file mode 100644 index 00000000..48e43aa4 --- /dev/null +++ b/packages/crd-seed/component/Tags/index.js @@ -0,0 +1,45 @@ +import { Link, useMatch } from 'react-router-dom' +import { ifProd } from 'crd-client-utils' +import { ifAddPrefix } from '../../utils' +import styles from './index.less' + +/** + * name: the name of tag category. + */ +const Tags = () => { + const { user, repo } = DOCSCONFIG || {} + const path = ifAddPrefix ? `/${repo}/tags/:name` : '/tags/:name' + const routeMatch = useMatch(path) || {} + const { name } = routeMatch.params || {} + + return ( +
    +
    {name || 'Tags'}
    +
    + { + name + ? mapTagsWithArticle.find(({ tagName }) => tagName === name)?.mapArticle.map(({ path, title }) => { + return + {title} + + }) + : mapTagsWithArticle.map(({ tagName }) => { + return + {tagName} + + }) + } +
    +
    + ) +} + +export default Tags diff --git a/packages/crd-seed/component/Tags/index.less b/packages/crd-seed/component/Tags/index.less new file mode 100644 index 00000000..a602fafa --- /dev/null +++ b/packages/crd-seed/component/Tags/index.less @@ -0,0 +1,26 @@ +.tags { + width: 100%; + padding: 0 20px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + &-title { + font-size: 24px; + font-weight: 400; + text-align: center; + } + + &-content { + width: 100%; + margin-top: 20px; + } + + &-text { + color: #6f6f6f; + padding: 10px; + display: inline-flex; + } +} \ No newline at end of file diff --git a/packages/crd-seed/crd.logo.svg b/packages/crd-seed/crd.logo.svg new file mode 100644 index 00000000..08a299f1 --- /dev/null +++ b/packages/crd-seed/crd.logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/crd-seed/favicon.ico b/packages/crd-seed/favicon.ico new file mode 100644 index 00000000..4737dbd7 Binary files /dev/null and b/packages/crd-seed/favicon.ico differ diff --git a/packages/crd-seed/index.js b/packages/crd-seed/index.js new file mode 100644 index 00000000..b68dd97e --- /dev/null +++ b/packages/crd-seed/index.js @@ -0,0 +1,16 @@ +import { Routes, Route, Navigate } from 'react-router-dom' +import BasicLayout from './layout' +import NoMatch from './component/NoMatch' +import './index.less' + +// run in the Web/Router.js +const ThemeSeed = (props) => { + return ( + + } /> + } /> + + ) +} + +export default ThemeSeed diff --git a/packages/crd-seed/index.less b/packages/crd-seed/index.less new file mode 100644 index 00000000..e1d6c9af --- /dev/null +++ b/packages/crd-seed/index.less @@ -0,0 +1,39 @@ +body { + font-family: cursive, sans-serif; +} + +body, +html { + position: relative; +} + +body, +html, +ul, +li { + margin: 0; + padding: 0; +} + +a { + color: #314659; + text-decoration: none; + transition: color .3s; + + &:hover { + color: #1890ff; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +:global { + #root { + height: 100%; + background: white; + } +} \ No newline at end of file diff --git a/packages/crd-seed/language/index.js b/packages/crd-seed/language/index.js new file mode 100644 index 00000000..abb50c73 --- /dev/null +++ b/packages/crd-seed/language/index.js @@ -0,0 +1,12 @@ +const languageMap = { + en: { + create_tm: 'create', + modify_tm: 'modify', + }, + 'zh-cn': { + create_tm: '创建', + modify_tm: '修改', + }, +} + +export default languageMap diff --git a/packages/crd-seed/layout/index.js b/packages/crd-seed/layout/index.js new file mode 100644 index 00000000..c755a7df --- /dev/null +++ b/packages/crd-seed/layout/index.js @@ -0,0 +1,337 @@ +import * as React from 'react' +import { Routes, Link, Route, Navigate, useLocation } from 'react-router-dom' +import cx from 'classnames' +import { ifDev, ifProd, ifPrerender } from 'crd-client-utils' +const Giscus = require('@giscus/react') +import Menu from '../component/Menu' +import Icon from '../component/Icon' +import Affix from '../component/Affix' +import Header from '../component/Header' +import Footer from '../component/Footer' +import Tags from '../component/Tags' +import languageMap from '../language' +import { isMobile, ifAddPrefix } from '../utils' +import { getOpenSubMenuKeys } from './utils' +import logo from '../crd.logo.svg' +import styles from './index.less' +import '../style/mobile.less' + +const { useState, useEffect, useMemo } = React +const SubMenu = Menu.SubMenu + +function BasicLayout({ + routeData, + menuSource +}) { + const location = useLocation() + const { pathname } = location + const { + user, + repo, + branch = 'main', + language = 'en', + menuOpenKeys, + tags, + comment + } = DOCSCONFIG || {} + + const [inlineCollapsed, setInlineCollapsed] = useState(true) + const [selectedKey, setSelectedKey] = useState('') + const curOpenKeys = getOpenSubMenuKeys({ + pathname, + menuSource, + menuOpenKeys + }) + const defaultPath = (routeData.find(data => data.path === '/README') + && routeData.find(data => data.path === '/README').mdconf + && routeData.find(data => data.path === '/README').mdconf.abbrlink) || 'README' + + useEffect(() => { + if (ifPrerender) { + scrollToTop() + INJECT?.inject?.() + } + }, []) + + useEffect(() => { + INJECT?.injectWithPathname?.(pathname) + }, [pathname]) + + useEffect(() => { + const { pathname } = location + let newPathName = pathname + // fix https://github.com/MuYunyun/create-react-doc/issues/195 + if (newPathName.endsWith('/')) { + newPathName = newPathName.slice(0, newPathName.length - 1) + } + if (newPathName.startsWith(`/${repo}`)) { + newPathName = newPathName.slice(`/${repo}`.length, newPathName.length) + } + setSelectedKey(newPathName || defaultPath) + }, location.pathname) + + const scrollToTop = () => { + document.body.scrollTop = 0 + document.documentElement.scrollTop = 0 + window.scrollTo(0, 0) + } + const renderSubMenuItem = (menus) => { + return ( + <> + {menus.map((item, index) => { + const { mdconf, routePath } = item || {} + const { abbrlink } = mdconf || {} + const path = abbrlink ? `/${abbrlink}` : routePath + // item.path carrys .md here. + return item.children && item.children.length > 0 ? ( + } + > + {renderSubMenuItem(item.children)} + + ) : ( + } + keyValue={abbrlink ? `/${abbrlink}` : item.path} + title={ + item && + item.type === "directory" && + item.props && + item.props.isEmpty ? ( + + {(item.mdconf && item.mdconf.title) || item.name} + + ) : ( + -1} + > + {item && item.mdconf && item.mdconf.title + ? item.mdconf.title + : item.title} + + ) + } + /> + ) + })} + + ) + } + const renderMenu = (menus) => { + if (menus.length < 1) return null + + return ( + + { + setInlineCollapsed(!inlineCollapsed) + }} + menuStyle={{ + height: "100vh", + overflow: "auto", + }} + selectedKey={selectedKey} + onSelect={(keyValue) => { + setSelectedKey(keyValue) + }} + defaultOpenKeys={curOpenKeys} + > + {renderSubMenuItem(menus || [])} + + + ) + } + /** + * This section is to show article's relevant information + * such as edit in github and so on. + */ + const renderPageHeader = () => { + const curMenuSource = routeData.filter(r => { + if (r.props.type === 'directory') return false + return pathname.indexOf(r.mdconf.abbrlink) > -1 || decodeURIComponent(pathname).indexOf(r.path) > -1 + }) + const editPathName = curMenuSource[0] && curMenuSource[0].props.path + const isNotTagPage = location.pathname.indexOf('/tags') === -1 + return ( +
    + {user && repo && isNotTagPage ? ( + + + Edit in GitHub + + ) : null} +
    + ) + } + + /** + * This section is to render comment area. + * Every pathname should has its own comment module. + */ + const renderComment = useMemo(() => { + return + }, [pathname]) + + /** + * This section is to show article's relevant information + * such as edit in created time、edited time and so on. + */ + const renderPageFooter = () => { + // in local env, data.path is to be /READEME, however pathname may be /Users/mac/.../.crd-dist/READEME/index.html + const matchData = routeData.find((data) => pathname.indexOf(data.path) > -1) + const matchProps = matchData && matchData.props + return ( +
    + {matchProps && matchProps.birthtime ? ( + + + {languageMap[language].create_tm}: + {matchProps.birthtime} + + ) : null} + {matchProps && matchProps.mtime ? ( + + + {languageMap[language].modify_tm}: + + {routeData.find((data) => pathname.indexOf(data.path) > -1).props.mtime} + + + ): null} +
    + ) + } + const isCurentChildren = () => { + const getRoute = routeData.filter((data) => pathname.indexOf(data.path) > -1) + const article = getRoute.length > 0 ? getRoute[0].article : null + const childs = menuSource.filter( + (data) => + article === data.article && data.children && data.children.length > 1 + ) + return childs.length > 0 + } + const isChild = isCurentChildren() + const renderMenuContainer = () => { + return ( + <> + +
    { + e.stopPropagation() + setInlineCollapsed(true) + }} + /> + + ) + } + + const renderContent = () => { + return ( +
    + + {/* see https://reacttraining.com/react-router/web/api/Redirect/exact-bool */} + } + /> + {routeData.map((item) => { + const { path, mdconf, component } = item + const { abbrlink } = mdconf + const enhancePath = abbrlink ? `/${abbrlink}` : path + const Comp = component + return ( + } + /> + ) + })} + { + tags + ? <> + } + /> + } + /> + + : null + } + {/* Todo: follow up how to use Redirect to back up the rest of route. */} + {/* */} + + {comment?.GiscusConfig ? renderComment : null} + {renderPageFooter()} +
    + ) + } + + return ( +
    +
    +
    + {renderPageHeader()} + {renderMenuContainer()} + {renderContent()} +
    +
    +
    + ) +} + +export default BasicLayout diff --git a/packages/crd-seed/layout/index.less b/packages/crd-seed/layout/index.less new file mode 100644 index 00000000..9b4f8c51 --- /dev/null +++ b/packages/crd-seed/layout/index.less @@ -0,0 +1,126 @@ +@import '../style/base.less'; + +.wrapper { + &::after { + content: ''; + display: block; + clear: both; + } +} + +.wrapperContent { + padding: 40px 0 0 0; + margin: 0px auto 0; + position: relative; + min-height: 100vh; + + .pageHeader, + .pageFooter { + position: absolute; + right: 40px; + font-size: 13px; + display: flex; + align-items: center; + } + + .pageHeader { + top: 20px; + } + + .pageFooter { + bottom: -23px; + + .position { + margin-right: 6px; + display: flex; + align-items: center; + } + } + + .icon { + margin-right: 3px; + } +} + +.wrapperMobile { + // padding-top: 0; +} + +.menuWrapper { + width: 240px; + float: left; + position: absolute; + font-size: 14px; + color: #6d6d6d; + transition: width .2s linear; + z-index: @menu-zIndex; + + ul li { + list-style: none; + } + + ul ul { + padding: 0 0 0 0; + } + + li { + // padding: 0 0 0 21px; + line-height: 40px; + } + + li li { + padding: 0 0 0 0; + } + + // :global { + // .active a { + // color: #1890ff; + // } + // } + + &-inlineCollapsed { + width: 0; + } + + .affixPlaceholder { + transition: width .2s linear; + } + + .affixWrapper { + border-right: 1px solid rgb(233, 233, 233); + transition: width .2s linear; + width: 0px; + } +} + +.menuMask { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, .3); + z-index: @menu-mask-zIndex; + transition: opacity .3s cubic-bezier(.78, .14, .15, .86) +} + +.content { + margin-left: 240px; + min-height: 300px; + transition: margin-left .2s ease-in-out; + position: relative; + + &-fullpage { + margin-left: 0px; + } +} + +.contentNoMenu { + min-height: 400px; +} + +giscus-widget { + display: flex; + margin: auto; + max-width: 940px; +} diff --git a/packages/crd-seed/layout/utils.js b/packages/crd-seed/layout/utils.js new file mode 100644 index 00000000..3d66c544 --- /dev/null +++ b/packages/crd-seed/layout/utils.js @@ -0,0 +1,49 @@ +/** + * get keys of open sub menu from pathname + * { + * pathname: pathname of location, + * when pathname is /9f41fc98, the result is ['/docs/主题']. + * when pathname is /测试/测试路由, the result is ['/docs/测试'] + * menuOpenKeys: means extra show open keys in config.yml + * } + */ +function getOpenSubMenuKeys({ + pathname, + menuSource, + menuOpenKeys +}) { + const result = [] + getOpenSubMenuKeysForAbbrLink( + menuSource, + decodeURI(pathname), + result + ) + + /** default open menu from config.yml */ + if (menuOpenKeys) { + result.push(...menuOpenKeys.split(',')) + } + + return result +} + +function getOpenSubMenuKeysForAbbrLink(source, pathname, result) { + for (let i = 0; i < source.length; i++) { + const { type, path, mdconf } = source[i] + if (type === 'directory') { + result.push(path) + const ifFind = getOpenSubMenuKeysForAbbrLink(source[i].children, pathname, result) + if (ifFind) return true + result.pop() + } else { + if ( + pathname.indexOf(mdconf.abbrlink) > -1 // used with abbrlink + || (pathname.indexOf(source[i].routePath) > -1) // used not with abbrlink + ) { + return true + } + } + } +} + +export { getOpenSubMenuKeys } diff --git a/packages/crd-seed/package.json b/packages/crd-seed/package.json new file mode 100644 index 00000000..e1afb6f5 --- /dev/null +++ b/packages/crd-seed/package.json @@ -0,0 +1,24 @@ +{ + "name": "crd-seed", + "version": "1.10.3", + "description": "Default Theme with Create React Doc", + "main": "index.js", + "dependencies": { + "@giscus/react": "2.2.2", + "crd-client-utils": "^1.8.2", + "react-router-dom": "^6.3.0", + "react-switch": "^5.0.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/MuYunyun/create-react-doc", + "directory": "packages/theme-seed" + }, + "keywords": [], + "publishConfig": { + "access": "public" + }, + "author": "muyunyun", + "license": "MIT", + "gitHead": "ffc5e4cbc94a7356da558c2dbf46e2f39bb8b199" +} diff --git a/packages/crd-seed/style/base.less b/packages/crd-seed/style/base.less new file mode 100644 index 00000000..07004ac8 --- /dev/null +++ b/packages/crd-seed/style/base.less @@ -0,0 +1,4 @@ +// z-index +@menu-zIndex: 99; +@menu-mask-zIndex: 90; +@header-zIndex: 100; \ No newline at end of file diff --git a/packages/crd-seed/style/mobile.less b/packages/crd-seed/style/mobile.less new file mode 100644 index 00000000..a513c4a7 --- /dev/null +++ b/packages/crd-seed/style/mobile.less @@ -0,0 +1,12 @@ +// remove the blue area in the mobile +:global { + div, + input, + textarea, + button, + select, + a, + li { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + } +} \ No newline at end of file diff --git a/packages/crd-seed/utils/index.js b/packages/crd-seed/utils/index.js new file mode 100644 index 00000000..2ae16251 --- /dev/null +++ b/packages/crd-seed/utils/index.js @@ -0,0 +1,10 @@ +import isClient from 'diana/lib/isClient' +import { ifProd, ifPrerender } from 'crd-client-utils' + +/** judge if is in mobile */ +const isMobile = isClient() ? 'ontouchend' in window : false + +// decide to if add prefix for path, eg: '/' or '/${repo}' +const ifAddPrefix = ifProd && !ifPrerender + +export { isMobile, ifAddPrefix } diff --git a/packages/crd-templates/.npmrc b/packages/crd-templates/.npmrc new file mode 100644 index 00000000..188fea96 --- /dev/null +++ b/packages/crd-templates/.npmrc @@ -0,0 +1,3 @@ +# .npmrc + +registry=https://registry.npmjs.org/ diff --git a/packages/crd-templates/README.md b/packages/crd-templates/README.md new file mode 100644 index 00000000..71799131 --- /dev/null +++ b/packages/crd-templates/README.md @@ -0,0 +1,15 @@ + _.-"\ + _.-" \ + ,-" \ + \ create \ + \ \ react \ + \ \ doc \ + \ \ _.-; + \ \ _.-" : + \ \,-" _.-" + \( _.-" + `--" + +# crd-templates + +crd-templates 集成了 create-react-doc 中相关模板文件。 diff --git a/packages/crd-templates/default/.github/workflows/gh-pages.yml b/packages/crd-templates/default/.github/workflows/gh-pages.yml new file mode 100644 index 00000000..b4896ff3 --- /dev/null +++ b/packages/crd-templates/default/.github/workflows/gh-pages.yml @@ -0,0 +1,37 @@ +name: github pages +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup Node + uses: actions/setup-node@v2.1.3 + with: + node-version: '12.x' + + - name: Get yarn cache + id: yarn-cache + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-website-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-website- + + - run: yarn install --frozen-lockfile + - run: yarn build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .crd-dist \ No newline at end of file diff --git a/packages/crd-templates/default/.npmrc b/packages/crd-templates/default/.npmrc new file mode 100644 index 00000000..a7b95f94 --- /dev/null +++ b/packages/crd-templates/default/.npmrc @@ -0,0 +1,2 @@ +# .npmrc +registry=https://registry.npmjs.org/ diff --git a/packages/crd-templates/default/Introduction/hello_world.md b/packages/crd-templates/default/Introduction/hello_world.md new file mode 100644 index 00000000..9183fa2e --- /dev/null +++ b/packages/crd-templates/default/Introduction/hello_world.md @@ -0,0 +1,3 @@ +Write docs happily now. + +If there is any problem, welcome give issue [there](https://github.com/MuYunyun/create-react-doc/issues). diff --git a/packages/crd-templates/default/README.md b/packages/crd-templates/default/README.md new file mode 100644 index 00000000..1435cd62 --- /dev/null +++ b/packages/crd-templates/default/README.md @@ -0,0 +1,85 @@ + _.-"\ + _.-" \ + ,-" \ + \ create \ + \ \ react \ + \ \ doc \ + \ \ _.-; + \ \ _.-" : + \ \,-" _.-" + \( _.-" + `--" + +[![npm version](https://img.shields.io/npm/v/create-react-doc)](https://badge.fury.io/js/create-react-doc) +[![week download](https://img.shields.io/npm/dw/create-react-doc.svg)](https://www.npmjs.com/package/create-react-doc) +![views](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/views.svg) +![views](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/views_per_week.svg) +![clones](https://raw.githubusercontent.com/MuYunyun/create-react-doc/traffic/traffic-create-react-doc/clones_per_week.svg) +![LICENSE MIT](https://img.shields.io/npm/l/create-react-doc.svg) + +# Create React Doc + +Create React Doc 是一个使用 React 的 markdown 文档站点生成工具。就像 [create-react-app](https://github.com/facebook/create-react-app) 一样,开发者可以使用 Create React Doc 来开发、部署 markdown 站点或者博客而无需关心站点环境配置信息。 + +## 特性 + +* 建站理念: 文件即站点 (Files as a site)。 +* 开箱即用: 一键生成可运行文档站点, 无需关心站点环境配置信息。 +* 自定义展示目录: 天然适合搭建 monorepo 文档、博客等站点。 +* 性能: 文档支持懒加载提升站点加载速度。 +* 工作流: 集成 Github action, 自动化打包、发布站点。 + +> [快速上手](http://muyunyun.cn/create-react-doc/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) + +## 主题 + +当前默认使用的主题是 [crd-seed](https://github.com/MuYunyun/create-react-doc/tree/main/packages/crd-seed)。 + +使用该主题搭建的站点 + +* [blog](http://muyunyun.cn/blog) + * ![](http://with.muyunyun.cn/90d3e357a31649b9466a828a92b6d88d.jpg) + * ![](http://with.muyunyun.cn/2e7440e4256debda2d73a4e6392c7146.jpg-300) +* [diana](https://muyunyun.cn/diana/) + +> 如果你的产品从中受益,欢迎留言补充 + +## 快速上手 + +**create-react-doc** 非常容易上手。开发者不需要额外安装或配置 webpack 或者 Babel 等工具,它们被内置隐藏在脚手架中,因此开发者可以专心于文档的书写。 + +如果你想在当前文件下建立站点文件 `doc`, 这里提供如下三种方式快速建站: + +### npx + +```bash +npx create-react-doc doc +``` + +### npm + +```bash +npm init create-react-doc doc +``` + +### yarn + +```bash +yarn create react-doc doc +``` + +![](http://with.muyunyun.cn/0f0cf6e8cb68b18399eac2927f74b063.jpg) + +> 如果想把模板内容内容拉取到当前文件夹, 则可以将如上命令的 `doc` 替换为 `.`, 比如执行 `npx create-react-doc .`。 + +接着执行 `cd doc && yarn && yarn start`, 可以在 `localhost: 3000` 预览站点, 如果站点文档发生改变, 站点将自动重新加载。 + + + +## 高阶用法 + +与 git 文件结构类似, 如果在展示的文件夹中有私有文件不方便展示在文档站点, 可以在 `.gitignore` 文件中设置过滤文件, 这样它们就不会展示在文档站点中了。eg: [.gitignore](https://github.com/MuYunyun/blog/blob/main/.gitignore) + +## 其它工具 + +* [crd-leetcode-cli](https://github.com/MuYunyun/create-react-doc/tree/main/packages/leetcode-cli): 提供将 [leetcode](https://leetcode-cn.com/) 中已 AC 的题目转化为 markdown 表格的能力。 \ No newline at end of file diff --git a/packages/crd-templates/default/_.gitignore b/packages/crd-templates/default/_.gitignore new file mode 100644 index 00000000..7a10c704 --- /dev/null +++ b/packages/crd-templates/default/_.gitignore @@ -0,0 +1,5 @@ +node_modules +package-lock.json +.DS_Store +.cache +.crd-dist diff --git a/packages/crd-templates/default/_config.yml b/packages/crd-templates/default/_config.yml new file mode 100644 index 00000000..36393c22 --- /dev/null +++ b/packages/crd-templates/default/_config.yml @@ -0,0 +1,21 @@ +# details see http://muyunyun.cn/create-react-doc/默认主题 + +# Site +# title: + +# Menu dir +## you can also set detailed dir, such as BasicSkill/css +menu: ['Introduction'] +## set init open menu keys +# menuOpenKeys: + +# site theme +theme: crd-seed + +# Github +## if you want to editing pages on github, you should config these arguments. +# user: +# repo: + +# Available values: en | zh-cn +language: en \ No newline at end of file diff --git a/packages/crd-templates/default/_package.json b/packages/crd-templates/default/_package.json new file mode 100644 index 00000000..eb11fff4 --- /dev/null +++ b/packages/crd-templates/default/_package.json @@ -0,0 +1,20 @@ +{ + "name": "{{name}}", + "version": "1.0.0", + "description": "Describe {{name}} here", + "scripts": { + "start": "react-doc start", + "build": "react-doc build", + "deploy": "react-doc deploy" + }, + "keywords": [ + "{{name}}", + "react-doc", + "react" + ], + "devDependencies": { + "create-react-doc": "{{crdVersion}}" + }, + "author": "", + "license": "MIT" +} diff --git a/packages/crd-templates/package.json b/packages/crd-templates/package.json new file mode 100644 index 00000000..fcc5d503 --- /dev/null +++ b/packages/crd-templates/package.json @@ -0,0 +1,17 @@ +{ + "name": "crd-templates", + "version": "1.10.0", + "description": "Default Templates with Create React Doc", + "repository": { + "type": "git", + "url": "https://github.com/MuYunyun/create-react-doc", + "directory": "packages/templates" + }, + "keywords": [], + "publishConfig": { + "access": "public" + }, + "author": "muyunyun", + "license": "MIT", + "gitHead": "" +} diff --git a/packages/crd-templates/theme/default/Introduction/hello_world.md b/packages/crd-templates/theme/default/Introduction/hello_world.md new file mode 100644 index 00000000..48e01fc1 --- /dev/null +++ b/packages/crd-templates/theme/default/Introduction/hello_world.md @@ -0,0 +1,3 @@ +Write docs happily now. + +If there is any problem, welcome give issue [here](https://github.com/MuYunyun/create-react-doc/issues). diff --git a/packages/crd-templates/theme/default/README.md b/packages/crd-templates/theme/default/README.md new file mode 100644 index 00000000..e7f495ba --- /dev/null +++ b/packages/crd-templates/theme/default/README.md @@ -0,0 +1,15 @@ + _.-"\ + _.-" \ + ,-" \ + \ create \ + \ \ react \ + \ \ doc \ + \ \ _.-; + \ \ _.-" : + \ \,-" _.-" + \( _.-" + `--" + +### {{name}} + +This is {{name}} theme for [create-react-doc](https://github.com/MuYunyun/create-react-doc). \ No newline at end of file diff --git a/packages/crd-templates/theme/default/_.gitignore b/packages/crd-templates/theme/default/_.gitignore new file mode 100644 index 00000000..ac737c85 --- /dev/null +++ b/packages/crd-templates/theme/default/_.gitignore @@ -0,0 +1,7 @@ +node_modules +package-lock.json +.DS_Store +.cache +.crd-dist +config.yml +Introduction \ No newline at end of file diff --git a/packages/crd-templates/theme/default/_.npmrc b/packages/crd-templates/theme/default/_.npmrc new file mode 100644 index 00000000..a7b95f94 --- /dev/null +++ b/packages/crd-templates/theme/default/_.npmrc @@ -0,0 +1,2 @@ +# .npmrc +registry=https://registry.npmjs.org/ diff --git a/packages/crd-templates/theme/default/_config.yml b/packages/crd-templates/theme/default/_config.yml new file mode 100644 index 00000000..c8c018e5 --- /dev/null +++ b/packages/crd-templates/theme/default/_config.yml @@ -0,0 +1,21 @@ +# details see http://muyunyun.cn/create-react-doc/默认主题 + +# Site +# title: + +# Menu dir +## you can also set detailed dir, such as BasicSkill/css +menu: ['Introduction'] +## set init open menu keys +# menuOpenKeys: + +# site theme +devTheme: ./index + +# Github +## if you want to editing pages on github, you should config these arguments. +# user: +# repo: + +# Available values: en | zh-cn +language: en \ No newline at end of file diff --git a/packages/crd-templates/theme/default/_index.js b/packages/crd-templates/theme/default/_index.js new file mode 100644 index 00000000..fb98dc0a --- /dev/null +++ b/packages/crd-templates/theme/default/_index.js @@ -0,0 +1,28 @@ +// The position of current index.js should be kept. +import { Routes, Route, Navigate } from 'react-router-dom' +import styles from './index.less' + +const {{name}} = ({routeData, menuSource}) => { + return ( +
    + + } + /> + {routeData.map((item) => { + const Comp = item.component + return ( + } + /> + ) + })} + +
    + ) +} + +export default {{name}} diff --git a/packages/crd-templates/theme/default/_index.less b/packages/crd-templates/theme/default/_index.less new file mode 100644 index 00000000..c8cef1f3 --- /dev/null +++ b/packages/crd-templates/theme/default/_index.less @@ -0,0 +1,7 @@ +.center { + height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/packages/crd-templates/theme/default/_package.json b/packages/crd-templates/theme/default/_package.json new file mode 100644 index 00000000..0bcd71e5 --- /dev/null +++ b/packages/crd-templates/theme/default/_package.json @@ -0,0 +1,24 @@ +{ + "name": "{{name}}", + "version": "0.1.0", + "description": "{{name}} theme for create-react-doc", + "main": "index.js", + "scripts": { + "start": "react-doc start" + }, + "keywords": [ + "{{name}}", + "react-doc", + "react", + "create-react-doc", + "theme" + ], + "devDependencies": { + "create-react-doc": "{{crdVersion}}" + }, + "dependencies": { + "react-router-dom": "^6.3.0" + }, + "author": "", + "license": "MIT" +} diff --git a/packages/crd-theme/.npmrc b/packages/crd-theme/.npmrc new file mode 100644 index 00000000..7edb6af8 --- /dev/null +++ b/packages/crd-theme/.npmrc @@ -0,0 +1,10 @@ +# .npmrc + +registry=https://registry.npmjs.org/ + +# https://github.com/sass/node-sass#binary-configuration-parameters +sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ + +# https://github.com/Medium/phantomjs#deciding-where-to-get-phantomjs +# phantomjs_cdnurl=http://cnpmjs.org/downloads +phantomjs_cdnurl=https://npm.taobao.org/dist/phantomjs diff --git a/packages/crd-theme/README.md b/packages/crd-theme/README.md new file mode 100644 index 00000000..a606b9da --- /dev/null +++ b/packages/crd-theme/README.md @@ -0,0 +1,3 @@ +### crd-theme + +[create-react-doc](https://github.com/MuYunyun/create-react-doc) 的主题加载包。提供了懒加载, 文件内容解析等能力。 \ No newline at end of file diff --git a/packages/crd-theme/component/Loading/index.js b/packages/crd-theme/component/Loading/index.js new file mode 100644 index 00000000..206f0827 --- /dev/null +++ b/packages/crd-theme/component/Loading/index.js @@ -0,0 +1,11 @@ +import styles from './index.less' + +const Loading = () => { + return ( +
    + 正在加载中.... +
    + ) +} + +export default Loading diff --git a/packages/crd-theme/component/Loading/index.less b/packages/crd-theme/component/Loading/index.less new file mode 100644 index 00000000..9370cd86 --- /dev/null +++ b/packages/crd-theme/component/Loading/index.less @@ -0,0 +1,5 @@ +.loading { + text-align: center; + min-height: 450px; + padding: 30px 0; +} diff --git a/packages/crd-theme/crd.logo.svg b/packages/crd-theme/crd.logo.svg new file mode 100644 index 00000000..08a299f1 --- /dev/null +++ b/packages/crd-theme/crd.logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/crd-theme/favicon.ico b/packages/crd-theme/favicon.ico new file mode 100644 index 00000000..4737dbd7 Binary files /dev/null and b/packages/crd-theme/favicon.ico differ diff --git a/packages/crd-theme/index.html b/packages/crd-theme/index.html new file mode 100644 index 00000000..02b7a91c --- /dev/null +++ b/packages/crd-theme/index.html @@ -0,0 +1,21 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + + + + +
    + + + + diff --git a/packages/crd-theme/index.js b/packages/crd-theme/index.js new file mode 100644 index 00000000..5618a22a --- /dev/null +++ b/packages/crd-theme/index.js @@ -0,0 +1,21 @@ +import * as React from 'react' +import Markdown from './markdown' +import './index.less' + +export default function (props) { + // routing load component + if (props.routeData && props.routeData.length > 0) { + props.routeData.map((item) => { + item.component = Markdown + return item + }) + } + + // support for custom theme. + const CustomTheme = require('__project_theme__').default + + // use custom theme here. + return ( + + ) +} diff --git a/packages/crd-theme/index.less b/packages/crd-theme/index.less new file mode 100644 index 00000000..ea4aab5f --- /dev/null +++ b/packages/crd-theme/index.less @@ -0,0 +1,35 @@ +body, +html { + position: relative; +} + +body, +html, +ul, +li { + margin: 0; + padding: 0; +} + +a { + color: #314659; + text-decoration: none; + transition: color .3s; + + &:hover { + color: #1890ff; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +:global { + #root { + height: 100%; + background: white; + } +} \ No newline at end of file diff --git a/packages/crd-theme/markdown/Link.js b/packages/crd-theme/markdown/Link.js new file mode 100644 index 00000000..66b0335a --- /dev/null +++ b/packages/crd-theme/markdown/Link.js @@ -0,0 +1,33 @@ +import styles from './Link.less' + +export default ({ title, href, children }) => { + let link = href.replace(/(\/|\/show\/|\/show)$/g, '') + if ( + /^(http(?:|s):)\/\/(jsfiddle.net|runjs.cn|codepen.io|codesandbox.io)/.test(link) && + !/^(https|http):\/\/(jsfiddle.net|runjs.cn|codepen.io|codesandbox.io)(?:|\/)$/.test(link) + ) { + const regexRunjs = /(https|http):\/\/runjs.cn\/code\/(.*)/gi + const regexCodepen = /(https|http):\/\/codepen.io\/(.*)\/pen\/(.*)/gi + const regexCodesandbox = /(https|http):\/\/codesandbox.io\/(s|embed)\/(.*)/gi + const runjs = regexRunjs.exec(link) + const codepen = regexCodepen.exec(link) + const codesandbox = regexCodesandbox.exec(link) + if (runjs && runjs.length > 2) { + link = `http://sandbox.runjs.cn/show/${runjs[2]}` + } else if (codepen && codepen.length === 4) { + link = `https://codepen.io/${codepen[2]}/embed/${codepen[3]}?height=400` + } else if (codesandbox && codesandbox.length === 4) { + link = `https://codesandbox.io/embed/${codesandbox[3]}` + } else { + link = `${link}/show/` + } + return ( +