likes
comments
collection
share

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

作者站长头像
站长
· 阅读数 7

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

这篇文章旨在为大家提供一种系统的方法来创建、部署和维护一个使用 TypeScript 编写的开源项目。

全文详细讲述了从环境准备、通过 pnpm 和 create-vite 配置和初始化项目、编写和测试代码、设置 CI/CD,到使用 VitePress 撰写和发布项目文档以及如何构建社区等步骤。

我期望这篇教程能帮助大家全面的学习创建和管理自己的开源项目。

以下是文章的大纲:

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

我们在教程中使用的开源项目名为魔法抽奖(Magic Lottery),这是一个真实的且一经发布就被大量使用的项目。

"Magic Lottery" 是一个直观且易用的库,旨在简化你的抽奖体验,使之更为简单、愉快和公平。默认使用 Fisher-Yates 洗牌算法作为混洗方法。

该项目的代码托管在 GitHub 上,你可以通过下面的链接访问项目,如果喜欢,请点个 Star(🌟):github.com/logeast/mag…

第 01 章 环境配置和初始化项目

1. 环境准备

确保你的计算机已经安装了 Node.js、npm 以及 git,若未安装则需先行进行安装。

你可以通过下面的链接来安装相应的软件:

  • Node.js (版本需要大于 16): 你可以从 Node.js 的官方网站下载并安装:nodejs.org/
  • Git: 你可以从 Git 的官方网站下载并安装:git-scm.com/downloads

注意进行安装时要根据你的操作系统(Windows、MacOS 或者 Linux)选择合适的版本。在安装完成后,在命令行中验证一下他们的版本确保正常安装。

node -v
git --version

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

2. 配置包管理器

现在我们来设置 pnpm 作为项目的包管理器。

pnpm 是一个包管理工具,其优点在于它能有效地节省磁盘空间和提高安装速度,同时严格管理 node_modules,避免版本冲突导致的问题。

安装 pnpm 的命令如下:

npm install -g pnpm
# 或者
yarn global add pnpm

之后运行 pnpm -v 验证一下确保安装成功。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

3. 初始化项目

接下来我们使用 create-vite 来创建一个 TypeScript 的项目。进入到一个工作目录,然后请按照以下步骤进行:

  1. 运行 create vite 命令 使用 pnpm 运行 create-vite 命令以创建一个新的 vite 项目。在你的命令行界面中输入:

    pnpm create vite
    
  2. 指定项目名称 在脚本提示“Project name: ”后,你输入并确认你的项目名,比如我们这里的 'magic-lottery'。

  3. 选择框架 当出现 “Select a framework” 提示时,我们选择 'Vanilla' ,表示不使用任何组件框架。

  4. 选择语言版本 在“Select a variant” 提示选项后, 我们这里选择 'TypeScript'。

此后,vite 就会自动在你指定的目录下创建一个预设好的 TypeScript 项目模板。它将打印出接下来需要执行的命令:

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

接下来我们依次运行下面的 3 条命令。

  • cd magic-lottery:切换到你新创建的项目目录。
  • pnpm install:安装所有由项目依赖的 npm 包。
  • pnpm run dev:开启开发服务器,开始你的开发工作。

到此为止,我们已经成功地初始化了 "Magic Lottery" 项目。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

打开浏览器,访问 http://localhost:5173/ ,如果出现如下页面表明一切顺利。在终端按下 ctrl + c 终止服务,后续我们的开发不涉及网页的展示。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

4. 改造和打包项目

接着我们来对项目进行一些改造和配置,使其更加贴近 npm 包的开发需求。

步骤1:删除无用文件

首先我们需要删除项目目录下的以下几个文件,这些文件是 Vite 在创建模板时生成的例子文件,对于我们的库并无实际作用。

  • src/counter.ts
  • src/style.css
  • src/typescript.svg
  • public/vite.svg

请确保下这两个文件没有被删掉:

  • src/main.ts
  • src/vite-env.d.ts

然后我们清空 main.ts 中的内容,并保存,这个文件会作为我们 npm 包的入口点。

步骤2:库模式以及打包配置

我们需要对 Vite 项目进行一些更改,将其更改为库模式,并设置一些打包配置,这样我们的项目就可以打包成一个 npm 包了。

  1. 创建并编辑 vite.config.ts 文件

    在项目根目录下,新建一个文件名为 vite.config.ts 的文件,并写入以下内容。此设置会将我们的项目设定为库模式并指定入口点以及输出等信息。

    // https://github.com/logeast/magic-lottery/blob/101/vite.config.ts
    
    import { resolve } from "path";
    import { defineConfig } from "vite";
    
    export default defineConfig({
      build: {
        sourcemap: true,
        lib: {
          entry: resolve(__dirname, "src/main.ts"),
          name: "MagicLottery",
          fileName: "main",
        },
      },
    });
    
    
  2. 更新 package.json 文件

    然后,我们需要更新 package.json 文件以指明包的主体、模块和类型声明文件的路径等基本信息。

    // https://github.com/logeast/magic-lottery/blob/101/package.json
    
    {
      "name": "magic-lottery",
      "private": true,
      "version": "0.0.0",
      "type": "module",
      "files": [
        "dist",
        "src"
      ],
      "main": "./dist/main.umd.cjs",
      "module": "./dist/main.js",
      "types": "./dist/main.d.ts",
      "scripts": {
        "dev": "vite",
        "build": "tsc && vite build",
        "preview": "vite preview"
      },
      "devDependencies": {
        "typescript": "^5.0.2",
        "vite": "^4.4.5"
      }
    }
    
  3. 安装并配置 vite-plugin-dts 插件

    Vite 在执行打包操作时不会自动生成 TypeScript 类型声明文件 .d.ts,因此我们需要借助 vite-plugin-dts 插件来生成这个文件。首先,运行以下命令来安装此插件:

    pnpm add -D vite-plugin-dts
    

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

    安装完成后,我们需要更新刚刚创建的 vite.config.ts 文件,引入并使用 vite-plugin-dts 插件。

    // https://github.com/logeast/magic-lottery/blob/101/vite.config.ts
    
    import { resolve } from "path";
    import { defineConfig } from "vite";
    import dts from "vite-plugin-dts";
    
    export default defineConfig({
      build: {
        sourcemap: true,
        lib: {
          entry: resolve(__dirname, "src/main.ts"),
          name: "MagicLottery",
          fileName: "main",
        },
      },
      plugins: [dts()],
    });
    
  4. 进行打包测试

    现在,我们再次运行打包过程,查看是否已经正确生成了期望的结果(包括类型声明文件)。在项目的终端中运行:

    pnpm build
    

    如果一切正常,你应该会看到 dist 文件夹被新建,里面包含了我们项目打包之后的文件,包括 JavaScript 文件和对应的 TypeScript 类型声明文件。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤3:代码格式配置

这一步是可选的。 如果你希望所编写的代码格式一致且整洁,你可以通过以下步骤配置和使用 Prettier 和 ESLint 以及 .editorconfig 文件。

这将有助于在项目中实现统一的编码风格与规范。以下是基于 VSCode 编辑器的配置步骤。

  1. 安装 Prettier 插件

    Prettier 是一个强大的代码格式化工具。首先需要在你的 VSCode 中安装 Prettier 扩展插件,你可以在拓展商店中搜索 Prettier 或直接访问此链接进行安装。

  2. 安装并配置 ESLint

    ESLint 能够帮助我们检测到 JavaScript 和 TypeScript 代码中的问题。你需要在 VSCode 中安装 ESLint 插件,并进行以下配置:

    • 首先,打开 VSCode 的扩展市场,搜索 ESLint,或者直接访问 此链接 进行安装。

    • 安装完成后,在终端中运行命令 pnpm create @eslint/config 来初始化你的 ESLint 配置。

    • 根据你的需求选择相应的配置选项,例如这里我们选择如下图所示的配置条件:

      全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

    • 在生成的 .eslintrc.cjs (或其他 ESLint 配置文件)中增加 rules.indent: ["error", 2], ,这表示我们默认采用空格作为缩进,并且每个缩进为 2 个空格。

      // https://github.com/logeast/magic-lottery/blob/101/.eslintrc.cjs
      
      module.exports = {
        env: {
          browser: true,
          es2021: true,
        },
        extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
        overrides: [
          {
            env: {
              node: true,
            },
            files: [".eslintrc.{js,cjs}"],
            parserOptions: {
              sourceType: "script",
            },
          },
        ],
        parser: "@typescript-eslint/parser",
        parserOptions: {
          ecmaVersion: "latest",
          sourceType: "module",
        },
        plugins: ["@typescript-eslint"],
        rules: {
          indent: ["error", 2],
          "linebreak-style": ["error", "unix"],
          quotes: ["error", "double"],
          semi: ["error", "always"],
        },
      };
      
    • 在 package.json 文件中加入 eslint 的命令。

      接着,我们需要在 package.json 文件中配置相关的 Lint 脚本。打开 package.json 文件并定位到 "scripts" 部分,添加一个新脚本。文件内容应如下:

      // https://github.com/logeast/magic-lottery/blob/101/package.json
      
      "scripts": {
      	//...other scripts...
        "lint": "eslint --cache ."
      }
      

      这样,当你在命令行运行 pnpm lint 的时候,ESLint 就会运行相应的代码检查活动。后面我们会在自动集成中自动运行这个命令。

  3. 创建 EditorConfig

    EditorConfig 帮助开发人员在不同的编辑器和 IDE 之间定义和维护一致的编码风格。在你的项目根目录下创建一个名为 .editorconfig 的文件,然后复制粘贴以下内容:

    # https://github.com/logeast/magic-lottery/blob/101/.editorconfig
    root = true
    
    [*]
    charset = utf-8
    indent_style = space
    indent_size = 2
    end_of_line = lf
    insert_final_newline = true
    trim_trailing_whitespace = true
    

通过以上步骤,就可以开始按照团队一致的编程风格进行有效的协作了。

步骤4:提交 Git

现在,我们将进一步使用 Git 来管理项目的版本控制,同时用 GitHub 托管我们的源代码。

在将你的项目提交到 GitHub 之前,先去 GitHub 创建一个新的空仓库。接下来我们就开始将本地的项目提交给这个远程仓库。

我们先使用 git 工具初始化项目,并添加 GitHub 仓库为远程的目标。

  1. 初始化 Git:在你的终端中,导航到你的项目目录,然后输入以下命令:

    git init
    
  2. 添加远程仓库:首先需要在 GitHub 创建一个新的仓库(假设命名为 magic-lottery),然后再回到你的终端,输入以下命令以连接到 GitHub:

    git remote add origin https://github.com/logeast/magic-lottery.git
    

    <logeast> 替换成你的 GitHub 用户名。

  3. 暂存和提交改动:所有的代码改动必须被暂存(Staged)才能被提交(Commit)。我们可以使用以下命令来暂存所有改动:

    git add .
    

    然后运行此命令提交暂存的改动,并附上一条简明的消息:

    git commit -m 'feat: initialize the project environment'
    
  4. 推送到远程仓库:最后,我们把主干分支命名为 main,并用如下命令将本地的改动推送(Push)到 GitHub :

    git branch -M main
    git push -u origin main
    

这样,你的项目就成功地被推送到了 GitHub 上的 magic-lottery 仓库里。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

在完成了之前的步骤后,我们已经建立好了开发环境并配置了 Vite,同时引入了必要的代码格式化与规范工具(Prettier 和 ESLint),并将代码同步到了 Github。

现在通过我们的设置,无论单独进行开发,还是希望更多的人参与到项目中共同开发,都能得到很好的支持。接下来我们开始库源码的编写和测试。

第 02 章 编码及单元测试

接下来我们设计和实现 Magic Lottery 工具库的核心部分。

1. 设计接口

接口的设计应该是出于提高代码质量,简化开发流程,避免潜在错误,并使得其他人和程序更容易理解并使用为目的。

MagicLottery 的目标是帮你轻松完成以下任务:

  1. 设置抽奖项及规则:快速定义抽奖池里的元素和抽取规则。
  2. 保证抽奖公正性:通过科学的混洗算法,确保每次抽取都是完全随机和公正。
  3. 管理抽奖池:方便地添加、删除抽奖项,查看或重置抽奖状态。
  4. 模拟实时抽奖:模拟逐个宣布获奖者的情景,提升活动乐趣。
  5. 处理异常情况:对于特殊情形,例如空抽奖池或超额抽取,给出友好提示。

为此,我们以构造函数和一系列公开方法来定义整个 "Magic Lottery" 类。它们包括如下方法。

// https://github.com/logeast/magic-lottery/blob/101/src/main.ts

interface Options<T> {
  shuffle?: (input: T[]) => T[];
  channelName?: string;
  replacement?: boolean;
}

interface DrawOptions<T> {
  replacement?: Options<T>["replacement"];
}

class MagicLottery<T> {
  constructor(entries: T[], options: Options<T>);

  setChannelName(channelName: string): void;
  getChannelName(): string | undefined;

  add(entries: T[]): void;
  remove(entry: T): void;
  hasEntry(entry: T): boolean;
  size(): number;
  isEmpty(): boolean;
  reset(): void;

  draw(): T[];
  drawOriginal(): T[];
  drawWinner(options: DrawOptions<T>): T;
  drawWinners(num: number, options: DrawOptions<T>): T[];

  setShuffle(shuffle: (input: T[]) => T[]): void;
  getShuffle(): (input: T[]) => T[];
  
  async nextWinner(options: DrawOptions<T>): Promise<T | undefined>;
}

2. 单元测试环境配置

在开始编程之前,确保我们的单元测试环境已经配置好是很重要的。现在,我们使用 Vitest 配置我们的单元测试环境。下面是具体步骤:

步骤1:安装 Vitest

首先我们需要安装 Vitest 库到我们的开发依赖中。在命令行中,导航到项目的根目录,并执行以下指令:

pnpm add -D vitest

这条命令会将 Vitest 添加到你的 package.json 文件的 devDependencies 列表中。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤2:设置测试脚本

接着,我们需要在 package.json 文件中配置相关的测试脚本。打开 package.json 文件并定位到 "scripts" 部分,添加两个新脚本 "test""coverage"。文件内容应如下:

// https://github.com/logeast/magic-lottery/blob/101/package.json

"scripts": {
	// ...other scripts...
  "test": "vitest",
  "coverage": "vitest run --coverage"
}

这样,当你在命令行运行 pnpm testpnpm coverage 的时候,Vitest 就会运行相应的测试活动。

当我们运行 pnpm coverage 时,会提醒我们安装 @vitest/coverage-v8 包,选 yes 安装即可。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤3:创建测试文件

现在,我们在 src 目录下创建一个名为 main.test.ts 的文件。

在这个 .test 文件里,我们编写针对 MagicLottery 模块的所有测试用例。

至此,你的单元测试环境就应该配置完毕了!接下来在终端项目的根目录下运行 pnpm test,这个命令会实时监听测试用例并给出反馈。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤4:学习测试用例的写法

describe, test, expect, beforeEach, 和 afterEach 是常用的用于测试的函数,源自 Jest 测试框架,也在 Vitest 中得到了支持。

  1. describe 这个函数通常用来组织相关联的测试用例。它接受两个参数:第一个参数是对这个测试组的描述,通常以字符串形式;第二个参数是一个包含所有相关测试案例的函数。

    describe("MagicLottery class", () => {
      test("some function", () => {
        // Test case goes here
      });
    });
    
  2. test 这个函数用来定义单个测试用例。它也接受两个参数,即用于描述的字符串和实现测试的函数。在 Vitest 中,ittest 的别名,你可以互换使用。

    test("should correctly shuffle entries", () => {
      // Test case goes here
    });
    
  3. expect 这个函数用来断言某个值的预期结果。期望提供一串链式方法如 .toBe(value), .toEqual(value), .toBeTruthy() 等等,用于具体断言测试结果是否符合预期。

    const result = someFunction();
    expect(result).toBe(expectedValue);
    
  4. beforeEach 这个函数在每个 test 测试用例执行前都会被调用一次,常用于为每个测试用例准备共享环境,比如创建新的实例或者重置全局状态。

  5. afterEach 类似地,这个函数在每个 test 测试用例执行完毕之后调用一次,通常用于清理操作,例如重置模块状态或还原全局配置。

    let lottery;
    
    beforeEach(() => {
      lottery = new MagicLottery([]);
    });
    
    afterEach(() => {
      lottery = null;
    });
    

3. 实现代码

根据设计好的接口,我们来编写对应的代码。在以下的教程中,我们将逐步实现 MagicLottery 类:

步骤1:构造函数和属性

首先,我们需要定义构造函数及类的属性。根据上面提供的接口,我们知道 MagicLottery 需要一个接受项(entries)和选项(options)的构造函数。为了存储这些值,我们需要在类中定义相应的属性。

// https://github.com/logeast/magic-lottery/blob/101/src/main.ts

class MagicLottery<T> {
  private entries: T[] = [];
  private shuffledEntries: T[] = [];
  private shuffle: (input: T[]) => T[];
  private channelName?: string;
  private replacement: boolean;

  constructor(entries: T[] = [], options: Options<T> = {}) {
    this.entries = entries;

    this.shuffle = options.shuffle || this.defaultShuffle;
    this.shuffledEntries = this.shuffle([...this.entries]);

    this.channelName = options.channelName;
    this.replacement = options.replacement || true;
  }
}

export default MagicLottery;

这里我们还需要添加一个默认的洗牌算法。同时,可以看到我们添加了 tsdoc 的文字注释,你可以在后续的代码中都加上。

// https://github.com/logeast/magic-lottery/blob/101/src/main.ts

class MagicLottery<T> {
  //...constructor and properties...

  /**
   * Implements the Fisher-Yates Shuffle Algorithm as the default shuffling method.
   * This method is used to shuffle the entries of the lottery.
   * See wikipedia at https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle.
   * @param input - The array to shuffle.
   * @returns The shuffled array.
   */
  private defaultShuffle(input: T[]): T[] {
    const array = [...input];
    let currentIndex = array.length;
    let temporaryValue: T;
    let randomIndex: number;

    while (0 !== currentIndex) {
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex -= 1;

      // swap array[currentIndex] with array[randomIndex]
      temporaryValue = array[currentIndex];
      array[currentIndex] = array[randomIndex];
      array[randomIndex] = temporaryValue;
    }

    return array;
  }
}

由于 options 的测试依赖后续方法的实现,这里的测试脚本我们留到最后来实现。

步骤2:抽奖池管理方法

接下来,我们可以添加一些管理抽奖池的方法,包括设置和获取 channelName ,以及 add(entry)remove(entry)hasEntry(entry)size()isEmpty()reset() 方法:

// https://github.com/logeast/magic-lottery/blob/101/src/main.ts

class MagicLottery<T> {
  //...constructor and properties...

  setChannelName(channelName: string): void {
    this.channelName = channelName;
  }

  getChannelName(): string | undefined {
    return this.channelName;
  }

  add(entries: T[]): void {
    this.entries.push(...entries);
    this.shuffledEntries = this.shuffle([...this.entries]);
  }

  remove(entry: T): void {
    const index = this.entries.indexOf(entry);
    if (index > -1) {
      this.entries.splice(index, 1);
      this.shuffledEntries = this.shuffle([...this.entries]);
    }
  }

  hasEntry(entry: T): boolean {
    return this.entries.includes(entry);
  }

  size(): number {
    return this.entries.length;
  }

  isEmpty(): boolean {
    return this.entries.length === 0;
  }

  reset(): void {
    this.entries = [];
    this.shuffledEntries = [];
  }
}

同步的,我们为该步骤书写测试用例。在 main.test.ts 文件中,写入以下用例。

// https://github.com/logeast/magic-lottery/blob/101/src/main.test.ts

import { describe, expect, test, beforeEach, afterEach } from "vitest";
import MagicLottery from "./main";

describe("Magic Lottery Manage Methods", () => {
  let lottery: MagicLottery<number>;

  beforeEach(() => {
    lottery = new MagicLottery([1, 2, 3, 4, 5]);
  });

  afterEach(() => {
    lottery.reset();
  });

  test("setChannelName and getChannelName method handle set and get a channel name", () => {
    expect(lottery.getChannelName()).toBeUndefined();

    lottery.setChannelName("Test Channel");
    expect(lottery.getChannelName()).toBe("Test Channel");
  });

  test("add method adds entries to the lottery", () => {
    lottery.add([6, 7, 8]);
    expect(lottery.size()).toBe(8);
    expect(lottery.drawOriginal()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8]);
  });

  test("add method handles empty array", () => {
    lottery.add([]);
    expect(lottery.size()).toBe(5);
  });

  test("remove method removes an entry from the lottery", () => {
    lottery.remove(1);
    expect(lottery.size()).toBe(4);
    expect(lottery.hasEntry(1)).toBe(false);
  });

  test("remove method handles non-existent entry", () => {
    lottery.remove(6);
    expect(lottery.size()).toBe(5);
  });

  test("hasEntry method checks if an entry is in the lottery", () => {
    expect(lottery.hasEntry(1)).toBe(true);
    expect(lottery.hasEntry(6)).toBe(false);
  });

  test("size method returns the size of the lottery", () => {
    expect(lottery.size()).toBe(5);
  });

  test("isEmpty method checks if the lottery is empty", () => {
    expect(lottery.isEmpty()).toBe(false);
    lottery.reset();
    expect(lottery.isEmpty()).toBe(true);
  });

  test("reset method resets the lottery", () => {
    lottery.reset();
    expect(lottery.size()).toBe(0);
  });
});

查看终端的测试结果,如果都打了对勾,恭喜你,所有的测试都通过了。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤3:洗牌和抽奖方法

现在,我们可以添加洗牌和抽奖的方法。根据传入的 shuffle 函数或者默认的 Fisher-Yates 洗牌算法,将条目进行混乱的排列,然后实现抽取一个或多个获胜者的功能。除此之外,还有获取所有混洗后的条目和原始顺序条目的功能。

// https://github.com/logeast/magic-lottery/blob/101/src/main.ts

class MagicLottery<T> {
  //...constructor, properties and other methods...

  draw(): T[] {
    return [...this.shuffledEntries];
  }

  drawOriginal(): T[] {
    return [...this.entries];
  }

  drawWinner(options: DrawOptions<T> = { replacement: this.replacement }): T {
    const { replacement } = options;

    if (this.shuffledEntries.length > 0) {
      const winner = this.shuffledEntries[0];
      if (!replacement) {
        this.remove(winner);
      }
      return winner;
    } else {
      throw new Error("At least one entry is required.");
    }
  }

  drawWinners(
    num: number,
    options: DrawOptions<T> = { replacement: this.replacement }
  ): T[] {
    const { replacement } = options;

    if (num <= this.shuffledEntries.length) {
      const winners = this.shuffledEntries.slice(0, num);
      if (!replacement) {
        winners.forEach((winner) => this.remove(winner));
      }
      return winners;
    } else {
      throw new Error("Requested number of winners exceeds the total entries.");
    }
  }

  setShuffle(shuffle: (input: T[]) => T[]): void {
    this.shuffle = shuffle;
    this.shuffledEntries = this.shuffle([...this.entries]);
  }

  getShuffle(): (input: T[]) => T[] {
    return this.shuffle;
  }
}

同步的,我们为该步骤书写测试用例。在 main.test.ts 文件中,写入以下用例。

// https://github.com/logeast/magic-lottery/blob/101/src/main.test.ts

import { describe, expect, test, beforeEach, afterEach } from "vitest";
import MagicLottery from "./main";

//...other describes...

describe("Magic Lottery Draw and Shuffle", () => {
  let lottery: MagicLottery<number>;

  beforeEach(() => {
    lottery = new MagicLottery([1, 2, 3, 4, 5]);
  });

  afterEach(() => {
    lottery.reset();
  });

  test("draw method returns shuffled entries", () => {
    const originalEntries = lottery.drawOriginal();
    const drawnEntries = lottery.draw();

    expect(drawnEntries).toHaveLength(originalEntries.length);
    expect(drawnEntries).not.toStrictEqual(originalEntries);

    expect(drawnEntries.sort()).toEqual(originalEntries.sort());
  });

  test("drawOriginal method returns original entries", () => {
    const originalEntries = lottery.drawOriginal();
    expect(originalEntries).toEqual([1, 2, 3, 4, 5]);
  });

  test("drawWinner method returns a winner", () => {
    const winner = lottery.drawWinner();
    expect(winner).toBeDefined();
  });

  test("drawWinner method throws error when lottery is empty", () => {
    lottery.reset();
    expect(() => lottery.drawWinner()).toThrow();
  });

  test("drawWinners method returns specified number of winners", () => {
    const winners = lottery.drawWinners(3);
    expect(winners?.length).toBe(3);
  });

  test("drawWinners method throws error when there are not enough entries", () => {
    lottery.reset();
    lottery.add([1]);
    expect(() => lottery.drawWinners(3)).toThrow();
  });
});

同步查看终端的测试通过情况。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤4:队列和异步方法

最后,对于异步方法,比如按照一定时间间隔宣布下一位获胜者,我们需要利用 Promise 对象和 JavaScript 的 async/await 语法完成。

// https://github.com/logeast/magic-lottery/blob/101/src/main.ts

class MagicLottery<T> {
  //...constructor, properties and other methods...

  /**
   * Draw the next winner from the shuffled entries and optionally remove them from the lottery.
   * @param [options={ replacement: this.replacement }]
   * @returns A promise that resolves with the next winner or rejects if there are no more entries left.
   */
  async nextWinner(
    options: DrawOptions<T> = { replacement: this.replacement }
  ): Promise<T | undefined> {
    const { replacement } = options;

    return new Promise((resolve, reject) => {
      if (this.shuffledEntries.length > 0) {
        const winner = this.shuffledEntries[0];
        if (!replacement) {
          this.remove(winner);
        }
        resolve(winner);
      } else {
        reject("No more entries left.");
      }
    });
  }
}

同步的,我们为该步骤书写测试用例。在 main.test.ts 文件中,写入以下用例。

// https://github.com/logeast/magic-lottery/blob/101/src/main.test.ts

import { describe, expect, test, beforeEach, afterEach } from "vitest";
import MagicLottery from "./main";

//...other describes...

describe("Magic Lottery Draw Async", () => {
  let lottery: MagicLottery<number>;

  beforeEach(() => {
    lottery = new MagicLottery([1, 2, 3, 4, 5]);
  });

  afterEach(() => {
    lottery.reset();
  });

  test("nextWinner method draws the next winner and removes them from the lottery", async () => {
    const winner = await lottery.nextWinner({ replacement: false });
    expect(winner).toBeDefined();
    expect(lottery.size()).toBe(4);

    const winner2 = await lottery.nextWinner();
    expect(winner2).toBeDefined();
    expect(lottery.size()).toBe(4);
  });

  test("nextWinner method should reject when there are no more entries left", async () => {
    lottery.reset();

    try {
      await lottery.nextWinner();
      throw new Error("nextWinner should have thrown an error but did not.");
    } catch (error) {
      expect(error).toBe("No more entries left.");
    }
  });
});

同步查看终端的测试通过情况。如果都打了对勾,表明所有测试都通过了。

步骤5:测试 Options 对象

现在让我们测试 constructor 中 options 的功能是否有效。

main.test.ts 文件中,写入以下用例。

// https://github.com/logeast/magic-lottery/blob/101/src/main.test.ts

describe("Magic Lottery Options", () => {
  let lottery: MagicLottery<string>;

  beforeEach(() => {
    lottery = new MagicLottery(["Alice", "Bob", "Charlie"], {
      channelName: "Test Channel",
      shuffle: (input: string[]) => input.reverse(),
      replacement: true,
    });
  });

  test("should set the channel name", () => {
    expect(lottery.getChannelName()).toBe("Test Channel");
  });

  test("should apply the shuffle function", () => {
    const originalEntries = lottery.drawOriginal();
    const shuffledEntries = lottery.draw();

    expect(shuffledEntries).toEqual(originalEntries.reverse());
  });

  test("drawWinner method returns a winner", () => {
    const winner = lottery.drawWinner();
    expect(winner).toBeDefined();

    const winner2 = lottery.drawWinner({ replacement: false });
    expect(lottery.hasEntry(winner2)).toBe(false);
  });

  test("drawWinners should set the replacement options", () => {
    const initialSize = lottery.size();
    const drawCount = 3;
    lottery.drawWinners(drawCount, { replacement: false });
    const finalSize = lottery.size();

    expect(finalSize).toBe(initialSize - drawCount);
  });

  test("nextWinner should set the replacement option", async () => {
    const winner = (await lottery.nextWinner()) || "";
    expect(lottery.hasEntry(winner)).toBe(true);
  });
});

运行测试用例,得到如下结果,表明测试全部通过了。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤6:查看测试覆盖率

测试覆盖率是我们衡量测试是否全面的重要指标。它可以帮助我们识别出可能未被测试的代码区域。

我们运行一下 pnpm coverage ,会生成一个详细的覆盖率报告,列出了每个文件的语句、分支、函数及行覆盖率。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

这表示我们的代码覆盖率已经达到了较高水平。

如果你发现某些部分的覆盖率低于你预期(比如低于90%),你可能需要添加更多的测试来覆盖这些部分的代码。回顾你的测试用例,思考是否遗漏了某些特定条件或边界情况。

步骤7:提交到 Git

以上就是整个 MagicLottery 类代码的具体实现过程。由于代码量较大,我们省略了部分注释,完整的代码可以参考 tutorial 分支里的源码。

添加一行新的内容 coverage/ 来告知 Git 忽略这个文件夹。这将避免我们把用于测试覆盖率的数据文件推送到GitHub仓库。 你的 .gitignore 文件现在应该包括类似这样的一行:

# https://github.com/logeast/magic-lottery/blob/101/.gitignore

coverage/

完成了上面的步骤之后,提交代码到 Git,并推送到 GitHub。

git add .
git commit -m 'feat: add main class features and test'
git push

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

4. 撰写相关文档

创建并管理开源项目时,撰写详细的文档以及选择合适的许可是非常重要的。以下是关于如何编写和发布文档的基本指南。

步骤1:LICENSE

LICENSE 文件是你的开源软件项目的法律声明。通过此文件,你可以告诉他人他们对你的项目有哪些权利。

这是通过GitHub在线界面创建和提交 LICENSE 文件的步骤:

  1. 在你的浏览器中打开 GitHub,并导航到你的项目仓库。

  2. 在仓库主页上,点击右侧的 "Add file" 按钮并从下拉列表中选择 "Create new file"。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  3. 在新的文件页面,为你的文件命名为 "LICENSE"。当你开始输入"LICEN"时,GitHub 会显示一个 "Choose a license template" 的按钮。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  4. 点击 "Choose a license template" 按钮。这将跳转到一个新的页面,其中包含各种常见开源许可证的模板。

  5. 在这个页面中,阅读每个许可证的简要描述,然后选择一个适合你的项目的许可证。点击许可证名称进入详情页并确认无误后,点击右侧的 "Review and submit" 按钮。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  6. 在下一个页面中,再次审查你所选的许可证。如果它符合你的需求和期望,那么点击 "Commit changes…" 完成。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  7. 现在你已经在 GitHub 中成功地创作了 LICENSE 文件,回到本地代码目录,打开终端或命令行工具,使用以下命令将刚才在远程仓库 Github 更新的内容同步到你的本地仓库:

    git pull
    

步骤2:README

README 是对你程序的第一份解释性文档,通常显示在 GitHub 项目页面的底部。它应包含关于项目的信息,如其功能,安装说明,示例用法,依赖关系,及如何贡献等。

  1. 在项目根目录下创建一个名为 README.md 的文件。 Markdown 是一种文本格式化语言,它能使文档更具可读性。

  2. README 文件中一般包含如下内容。

    • 项目简介:[项目名称] 是一个……(在这里简述你的项目)
    • 项目徽标:展示诸如 build 状态、依赖关系安全性、版本号、下载次数等信息。
    • 安装:描述如何安装项目。
    • 示例用法:具体的代码示例和它们期望的结果。
    • 贡献指南:解释怎样提交问题和开发此项目。
    • 许可:务必包含对 LICENSE 文件的引用。
  3. 下面是 Magic Lottery 的 README.md 文档,可以用作参考。

    <!-- https://github.com/logeast/magic-lottery/blob/101/README.md -->
    
    # Magic Lottery
    
    [![ci][ci-badge]][ci-link]
    ![ts][ts-badge]
    [![download-badge]][download-link]
    [![version][version-badge]][download-link]
    ![commit][commit-badge]
    ![license][license-badge]
    
    Magic Lottery is an intuitive library aimed at simplifying your lottery experiences to make them simpler, more enjoyable, and fair.
    
    Magic Lottery uses the [Fisher-Yates Shuffle Algorithm](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) as the default shuffling method.
    
    ## Installation
    
    To install Magic Lottery, use npm/yarn/pnpm. Execute the following command in your terminal:
    
    ```bash
    npm install magic-lottery
    # or
    yarn add magic-lottery
    # or
    pnpm add magic-lottery
    ```
    
    ## Usage Example
    
    Here's a simple usage case for the Magic Lottery.
    
    ```js
    import MagicLottery from "magic-lottery";
    
    // Create a new MagicLottery instance
    const lottery = new MagicLottery(["Alice", "Bob", "Charlie"]);
    
    // Add more entries to the lottery
    lottery.add(["David", "Eve"]);
    
    // Draw all shuffled entries
    console.log(lottery.draw());
    
    // Reset the lottery
    lottery.reset();
    ```
    
    For more examples, please refer to the [official documentation](https://logeast.github.io/magic-lottery).
    
    ## Contributing
    
    Refer to our [Contributing Guide](https://github.com/logeast/magic-lottery/blob/main/CONTRIBUTING.md).
    
    ## License
    
    [MIT](https://github.com/logeast/magic-lottery/blob/main/LICENSE).
    
    [ci-badge]: https://github.com/logeast/magic-lottery/actions/workflows/ci.yml/badge.svg
    [ci-link]: https://github.com/logeast/magic-lottery/actions/workflows/ci.yml
    [ts-badge]: https://badgen.net/badge/-/TypeScript/blue?icon=typescript&label
    [download-badge]: https://img.shields.io/npm/dm/magic-lottery
    [download-link]: https://www.npmjs.com/search?q=magic-lottery
    [version-badge]: https://img.shields.io/npm/v/magic-lottery.svg
    [commit-badge]: https://img.shields.io/github/commit-activity/m/logeast/magic-lottery
    [license-badge]: https://img.shields.io/github/license/logeast/magic-lottery
    ```
    
    

步骤3:重要信息和关键词

好的,接下来我们要给 npm 包添加详细信息。这些信息有助于其他开发者理解你的包的具体内容以及如何使用。

一些有效的元数据包括主页、仓库信息(让用户知道从哪里获得源代码)和关键词(帮助搜索引擎找到你的包)。

打开 package.json 文件,加入下面的信息。

{
  "name": "magic-lottery",
  "license": "MIT",
  "author": "logeast",
  "description": "A magic library makes your lucky draws simpler.",
  "homepage": "https://github.com/logeast/magic-lottery",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/logeast/magic-lottery.git"
  },
  "keywords": [
    "lottery",
    "Fisher-Yates Shuffle",
    "magic lottery",
    "lucky",
    "random",
    "shuffle",
    "raffle",
    "prize",
    "winner"
  ],
	//...other configs...
}

这会使我们的 npm 包在 npm 官网上显示得更为详细且易懂。最后,不要忘记保存 package.json 文件,并将更改提交到仓库。

第 03 章 持续集成和发布

在完成代码编写和基础的文档撰写后,我们需要设立持续集成(Continuous Integration, CI)流程。

CI 是一种在源代码发生更改的时候自动进行构建和测试的实践。它可以帮助我们及时发现并修复问题,保证代码库的状态始终为可运行状态。

1. Github Action CI/CD 配置

步骤1:创建 Github Action 执行环境

首先在项目根目录下,创建名为 .github 的目录,后续所有的 CI 脚本都会放到这个目录下。GitHub 会自动识别这个目录。

接着新建 actions 目录,并创建 .github/actions/setup-and-cache/action.yml 文件。

该文件描述了执行环境的设置过程,包括安装和缓存 pnpm,设定 node 版本等。

# https://github.com/logeast/magic-lottery/blob/101/.github/actions/setup-and-cache/action.yml

name: Setup and cache
description: Setup for node, pnpm and cache for browser testing binaries
inputs:
  node-version:
    required: false
    description: Node version for setup-node
    default: 18.x

runs:
  using: composite

  steps:
    - name: Install pnpm
      uses: pnpm/action-setup@v2
      with:
        version: 8.6.10

    - name: Set node version to ${{ inputs.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ inputs.node-version }}

步骤2:创建 Workflow 文件

接下来在 .github 目录下创建一个名为 workflows 的目录,工作流相关的脚本都会放到这里。可以在这里定义一组自动运行在特定事件(例如 push 到主分支、向主分支提交 PR)触发时要执行的任务。

进入 workflows 创建一个名为 ci.yml 的文件,写入如下脚本。这个脚本主要完成了代码检查和测试,它按照不同的操作系统和 Node.js 版本进行矩阵测试从而确保代码的健壮性。

# https://github.com/logeast/magic-lottery/blob/101/.github/workflows/ci.yml

name: CI

on:
  push:
    branches:
      - main

  pull_request:
    branches:
      - main

concurrency:
  group: ci-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: ./.github/actions/setup-and-cache

      - name: Install
        run: pnpm i

      - name: Lint
        run: pnpm run lint

  test:
    runs-on: ${{ matrix.os }}

    timeout-minutes: 3

    strategy:
      matrix:
        os: [ubuntu-latest]
        node_version: [16, 18]
        include:
          - os: macos-latest
            node_version: 18
          - os: windows-latest
            node_version: 18
      fail-fast: false

    steps:
      - uses: actions/checkout@v3

      - uses: ./.github/actions/setup-and-cache
        with:
          node-version: ${{ matrix.node_version }}

      - name: Install
        run: pnpm i

      - name: Build
        run: pnpm run build

      - name: Test
        run: pnpm run test

然后创建一个名为 release.yml 的文件,并写入如下脚本,用于在对项目打标签时自动生成 GitHub changelog 并发布新版本。

# https://github.com/logeast/magic-lottery/blob/101/.github/workflows/release.yml

name: Release

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v3
        with:
          node-version: 18.x

      - run: npx changelogithub
        env:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

到这里,我们发布 npm 的准备工作就完成了,下一步是整合上面的脚本来实现自动发布 npm 包。

2. 自动发布 npm 包

我们希望能够在本地触发版本号的更改,以及 npm 的自动发布。因此我们需要完成下面几个步骤。

  1. 创建 npm 账号:首先,我们需要在 npm 官网注册一个账号。记下用户名、密码和邮箱,稍后会用到。

  2. 安装 bumpp:接着,在项目中运行 pnpm add -D bumpp 安装 bumpp。,它会根据语义化版本控制规则,更新我们的 package.json 文件中的版本号。 全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  3. 添加 script 命令:然后在 package.json 里的 scripts 中,添加一条新的命令:

    "scripts": {
      "release": "bumpp package.json --commit --push --tag && git update-ref refs/heads/release refs/heads/main && git push origin release && pnpm publish --access public"
    }
    
    

    这条命令有几个部分:

    • bumpp 用于升级版本号;
    • git 相关命令会更新 git 的 release 分支,并将其推送至远程仓库;
    • pnpm publish --access public 发布包到 npm。
  4. 登录 npm:我们运行 npm adduser,然后按提示输入刚才记录的用户名、密码和邮箱(密码不会显示)。完成验证码后,我们就登录成功了。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  5. 发布 npm 包:现在运行 pnpm release,应该会出现版本选择的界面。选好版本后,npm 包就会成功发布啦!现在访问 www.npmjs.com 查看我们刚刚发布的包吧。

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

  6. 以后每次发布,只需要运行 pnpm release,选择版本号,就会自动发布新版本了。

第 04 章 创建文档站点

到这里,我们的项目已经很完善了。如果你想立即进入社区流程,可以暂时跳过这一节;如果你希望得到一份更完善的文档,请继续阅读。

1. 安装 VitePress

我们将使用 VitePress 来帮助我们创建和管理文档。在项目根目录下运行如下命令进行安装:

pnpm add -D vitepress

接着,运行 npx vitepress init,按照提示操作,生成网站。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

生成之后,你会看到有一个新的 docs/ 目录,并且 package.json 文件也新增了几个脚本。

"scripts": {
  "docs:dev": "vitepress dev docs",
  "docs:build": "vitepress build docs",
  "docs:preview": "vitepress preview docs"
},

在项目根目录下运行 pnpm docs:dev 来启动文档站点,并用浏览器打开 http://localhost:5173/,如果能看到一些默认信息就表示运行成功了。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

接下来我们可以按照自己的需求对网站做一些自定义和内容填充,例如:

  • 填充首页的文案
  • 填充指引文档
  • 填充 API 接口文档
  • ...

详细的配置信息可以访问 VitePress 官方文档 查看,在这里我们就不展开介绍了。

2. 自定义网站主题色

如果我们想修改文档网站的主题色,可以对 VitePress 的默认主题进行一些自定义。以下是一种可能的方式:

  1. 在项目的 docs/.vitepress 下新建一个叫做 theme 的目录。

  2. theme 目录下创建一个名为 custom.css 的文件,并写入以下内容来定义你的颜色变量:

    /* https://github.com/logeast/magic-lottery/blob/101/docs/.vitepress/theme/custom.css */
    
    :root {
      --vp-c-brand-darker: #a11b62;
      --vp-c-brand-dark: #c72c79;
      --vp-c-brand: #ed4192;
      --vp-c-brand-light: #fa6ba9;
      --vp-c-brand-lighter: #ff96c0;
    }
    

    请将上述颜色值替换为你所需要的颜色。

  3. 接着,在 theme 目录下创建一个 index.ts 文件,并导入默认的 VitePress 主题和我们之前创建的 custom.css 文件,如下所示:

    // https://github.com/logeast/magic-lottery/blob/101/docs/.vitepress/theme/index.ts
    
    import DefaultTheme from "vitepress/theme";
    import "./custom.css";
    
    export default DefaultTheme;
    

    保存后,要先停止运行你的 VitePress 开发服务器,然后再重新启动它以便让你的主题更改生效。命令是 pnpm docs:dev

    全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

.vitepress/cache 文件夹存储了 VitePress 在构建过程中的缓存文件。一般来说,我们不希望这些文件被提交到版本控制系统(如 Git)中。因此,将其添加到 .gitignore 文件中是一个好习惯。

在你的项目的根目录下,找到 .gitignore 文件,添加以下行:

docs/.vitepress/cache

然后保存并关闭 .gitignore 文件。

现在,Git 就会忽略 docs/.vitepress/cache 中的所有文件和文件夹。

记得及时提交代码。

3. 配置 GitHub Pages

最后,我们会将生成的 VitePress 文档网站部署到 GitHub Pages 上,发布后的路径为 https://<你的GitHub用户名>.github.io/<你的仓库名>。比如 Magic Lottery 的官网地址是 https://logeast.github.io/magic-lottery/

步骤1:配置基础路径

VitePress 的默认部署路径为 /,因此我们需要把它设置为 /magic-lottery/。找到 docs/.vitepress/config.ts 文件,并加上以下代码:

// https://github.com/logeast/magic-lottery/blob/101/docs/.vitepress/config.ts

export default defineConfig({
	base: "/magic-lottery/"
	//...other configs...
});

根据你的项目实际情况修改其中的 /magic-lottery/ 部分。

步骤2:创建部署脚本

我们同样需要一个 GitHub Actions 工作流文件,来自动构建并部署我们的 VitePress 文档网站到 GitHub Pages 上。

.github/workflows 目录下创建一个名为 deploy-docs.yml 的文件。写入如下脚本,这个脚本将 VitePress 网站部署到 GitHub Pages。当我们在 main 分支上 push 或手动触发此工作流时,它就会开始执行。

# https://github.com/logeast/magic-lottery/blob/101/.github/workflows/deploy-docs.yml

name: Deploy VitePress site to Pages

on:
  push:
    branches: [main]

  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: ./.github/actions/setup-and-cache
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: pnpm
      - name: Setup Pages
        uses: actions/configure-pages@v3
      - name: Install dependencies
        run: pnpm i
      - name: Build with VitePress
        run: pnpm run docs:build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: docs/.vitepress/dist

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2

步骤3:开启 GitHub Pages

打开浏览器进入 GitHub 仓库页面,在 "Settings" 中找到 "Pages" 并点击,“Build and deployment > Source” 下拉菜单中选择 "GitHub Actions"。这样我们推送代码就会自动触发文档站点的发布了。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤4:保存并提交发布

  1. 确认分支:首先确认当前处在 main 分支。

  2. 保存并提交发布:接着在终端运行以下命令进行更改的保存和提交:

    git add .
    git commit -m "feat: add docoument website"
    git push
    
  3. 访问部署完成的网站:稍等片刻,GitHub 处理完推送后,转到 https://logeast.github.io/magic-lottery/(将 logeast 替换为你的用户名,magic-lottery 替换为你的仓库名)。

如果能成功访问,那就说明配置成功了。

第 05 章 社区建设

社区对于开源项目非常重要。在 GitHub 仓库中,我们将添加贡献指南文档,提供代码提交规范和问题报告的模版,以及开通 GitHub Discussions。

1. 贡献指南

一份详尽的贡献指南通常应包含以下内容。

  1. 项目介绍:明确阐述项目的目标和宗旨,帮助新的贡献者了解项目。
  2. 环境设定:提供说明和命令以帮助新的贡献者设置开发环境,包括如何fork和clone仓库,如何安装依赖等。
  3. 进行测试:让参与者知道如何在他们的本地环境下运行和通过测试。
  4. 文档生成:如果需要,告诉贡献者如何生成或查看相关的项目文档。
  5. 提交Pull Request(PR)规则:清晰解释如何创建和提交PR,以及所有需要遵守的规范或限制。
  6. 代码审查过程:让知道他们的PR将被审查,并可能需要进行修改。
  7. 联系方式:给出参与者如何提出问题或获取帮助的方法。
  8. 许可证说明:明确贡献者同意其贡献将根据项目的许可协议进行许可。

在根目录下新建一个 CONTRIBUTING.md 的文件,参考下面的内容书写你自己的贡献指南。

<!-- https://github.com/logeast/magic-lottery/blob/101/CONTRIBUTING.md -->

# Magic Lottery Contributing Guide

Hi! We're really excited that you're interested in contributing to Magic Lottery!

Before submitting your comtribution, please read through the fllowing guide.

## Repo Setup

To develop locally, fork this repository and clone it in your local machine. The package manager used to install and link dependencies must be [pnpm](https://pnpm.io/).

To develop and test the lib:

1. Run `pnpm i` in Magic Lottery's root folder.
2. Run `pnpm build` in Magic Lottery's root folder.

## Running Test

1. Run `pnpm test` in Magic Lottery's root folder.

## Running Document

1. Run `pnpm docs:dev` in Magic Lottery's root folder and open http://127.0.0.1:5173 in your browser.

## Pull Request Guidelines

1. Before you submit your pull request, make sure to review your changes thoroughly. This includes running all tests and checking for any performance issues.
2. In your pull request description, please include a detailed explanation of your changes and why you think they are necessary.
3. Make sure your code adheres to our coding standards and conventions.
4. Your pull request will be reviewed by one of the maintainers. Please be patient and address any feedback you receive.
5. Once your pull request is approved, it will be merged into the main branch.

For any additional questions, please contact logeast@outlook.com.

## License

By contributing to Magic Lottery, you agree that your contributions will be licensed under its MIT license.

2. Issue 模版

接下来,我们将提供一些预设的 Issue 模板,帮助其他开发者以统一的格式提交问题或功能请求。先创建 .github/ISSUE_TEMPLATE 目录。

步骤1:创建 Bug 报告模板

.github/ISSUE_TEMPLATE 文件夹中,新建一个名为 bug_report.yml 的文件,并粘贴以下内容:

这个文件定义了一个 GitHub Issue 模板,用于报告 bug。它会自动添加 "pending triage" 标签,并要求提供必要的 bug 描述、重现方式、系统信息,并选择使用的包管理器。此外,还包括确认前必需进行的验证选项。

# https://github.com/logeast/magic-lottery/blob/101/.github/ISSUE_TEMPLATE/bug_report.yml

name: 🐞 Bug report
description: Report an issue with Magic Lottery
labels: [pending triage]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report!
  - type: textarea
    id: bug-description
    attributes:
      label: Describe the bug
      description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
      placeholder: Bug description
    validations:
      required: true
  - type: textarea
    id: reproduction
    attributes:
      label: Reproduction
      description: Please provide a link to [StackBlitz]() or a github repo that can reproduce the problem you ran into. A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "need reproduction" label. If no reproduction is provided after 3 days, it will be auto-closed.
      placeholder: Reproduction
    validations:
      required: false
  - type: textarea
    id: system-info
    attributes:
      label: System Info
      description: Output of `npx envinfo --system --npmPackages '{magic-lottery}' --binaries --browsers`
      render: shell
      placeholder: System, Binaries, Browsers
    validations:
      required: true
  - type: dropdown
    id: package-manager
    attributes:
      label: Used Package Manager
      description: Select the used package manager
      options:
        - npm
        - yarn
        - pnpm
    validations:
      required: true
  - type: checkboxes
    id: checkboxes
    attributes:
      label: Validations
      description: Before submitting the issue, please make sure you do the following
      options:
        - label: Read the [Contributing Guidelines](https://github.com/logeast/magic-lottery/blob/main/CONTRIBUTING.md).
          required: true
        - label: Read the [docs](https://logeast.github.io/magic-lottery/).
          required: true
        - label: Check that there isn't [already an issue](https://github.com/logeast/magic-lottery/issues) that reports the same bug to avoid creating a duplicate.
          required: true
        - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/logeast/magic-lottery/discussions) or join our [Discord Chat Server](https://discord.gg/xKSsX84P).
          required: true
        - label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.
          required: true

最终我们的模版会展示成这样的表单。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

步骤2:创建特性请求模板

继续在 .github/ISSUE_TEMPLATE 文件夹中,新建一个名为 feature_request.yml 的文件并粘贴以下内容:

这个文件是另一个 GitHub Issue 模板,用于提交新的特性请求。它会自动添加 "enhancement: pending triage" 标签,并收集关于所建议特性的描述、期望解决方案、替代方案和其他相关上下文。

# https://github.com/logeast/magic-lottery/blob/101/.github/ISSUE_TEMPLATE/feature_request.yml

name: 🚀 New feature proposal
description: Propose a new feature to be added to Magic Lottery
labels: ["enhancement: pending triage"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for your interest in the project and taking the time to fill out this feature report!
  - type: textarea
    id: feature-description
    attributes:
      label: Clear and concise description of the problem
      description: "As a developer using Magic Lottery I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!"
    validations:
      required: true
  - type: textarea
    id: suggested-solution
    attributes:
      label: Suggested solution
      description: We could provide following implementation...
    validations:
      required: true
  - type: textarea
    id: alternative
    attributes:
      label: Alternative
      description: Clear and concise description of any alternative solutions or features you've considered.
  - type: textarea
    id: additional-context
    attributes:
      label: Additional context
      description: Any other context or screenshots about the feature request here.
  - type: checkboxes
    id: checkboxes
    attributes:
      label: Validations
      description: Before submitting the issue, please make sure you do the following
      options:
        - label: Read the [Contributing Guidelines](https://github.com/logeast/magic-lottery/blob/main/CONTRIBUTING.md).
          required: true
        - label: Read the [docs](https://logeast.github.io/magic-lottery/).
          required: true
        - label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
          required: true

最终我们的模版会展示成这样的表单。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

3. 开通社区

现在我们来关注一下如何建立社区,方便用户和开发者进行交流

步骤1:启用 Discussions

GitHub Discussions 是 GitHub 提供的一个社区论坛,允许你与项目的其他维护者、贡献者和用户进行长期且开放的同步或异步对话。按照以下步骤开启:

  1. 在你的 GitHub 仓库页面,点击 "Settings"。
  2. 滚动到 "Discussions" 设置。
  3. 将 "Discussions" 选项设置为 "Enabled"。

步骤2:创建 Discord 频道(可选)

Discord是一个适合大规模社区交流的平台,让用户能够即时通过文本、语音或视频聊天。创建 Discord 频道的相关教程可以从其官方网站获取。创建完 Discord 频道后,记住保存频道链接,稍后我们需要用到。

步骤3:创建配置文件

.github/ISSUE_TEMPLATE 目录中,创建一个名为 config.yml 的文件,并输入以下内容:

# https://github.com/logeast/magic-lottery/blob/101/.github/ISSUE_TEMPLATE/config.yml

blank_issues_enabled: false
contact_links:
  - name: Questions & Discussions
    url: https://github.com/logeast/magic-lottery/discussions
    about: Use GitHub discussions for message-board style questions and discussions.
  - name: Discord Chat
    url: https://discord.gg/xKSsX84P
    about: Ask questions and discuss with other Magic Lottery users in real time.

这个 YAML 文件允许我们向 GitHub Issues 添加自定义链接,这样当用户尝试创建新 Issue 时,他们会看到这些链接作为联系选项。这里我们添加了 "discussions" 和 "discord chat" 两个链接,以引导用户前往社区论坛或 Discord 频道进行问题讨论。

最后,记得提交到 Git 并推送到 GitHub。访问 GitHub 仓库里的的 Issues 标签,会发现我们的模版已经都生效了。

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

第 06 章 结束,亦是开始

我们一起走过了构建一个 npm 包的全过程。这个过程包括了如何编写和发布你的代码,以及如何开设和维护用户社区。

以下是我们创建的文件目录,以帮助你更好地理解各部分之间的关系:

全流程教程:TypeScript npm 开源项目开发、发布及生态建设完整指南

我们在教程中使用的开源项目名为魔法抽奖(Magic Lottery),这是一个真实的且一经发布就被大量使用的项目。

"Magic Lottery" 是一个直观且易用的库,旨在简化你的抽奖体验,使之更为简单、愉快和公平。默认使用 Fisher-Yates 洗牌算法作为混洗方法。

该项目的代码托管在 GitHub 上,你可以通过下面的链接访问项目,如果喜欢,请点个 Star(🌟):github.com/logeast/mag…

本教程中的所有代码示例都被整合在了 101 分支下,方便你参考。

101 分支:github.com/logeast/mag…

希望这个全面的指南能够帮助你更好地理解如何从头开始构建、部署并维护一个 npm 包。