npmパッケージ開発のすゝめ


## 初めに
npmパッケージ開発は簡単にできるよ！と聞いたので試してみました。

今回はBackとFrontで```camelケース(camelCaes)```や```snakeケース(snake_case)```で統一されていない際、発狂するものが現れないよう値やjson Keyの命名ルールを変換するパッケージを開発し、公開してみます。

こんな感じです。
```
変換例1）
testStr ⇔ test_str
```

```
変換例2）
    {
        testStr: "testStr",
        testObj: { testKeyA: "testKeyA", testKeyB: "testKeyB" },
        testArr: [
            { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" },
            { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" },
        ],
    }
    ⇔
    {
        test_str: "testStr",
        test_obj: { test_key_a: "testKeyA", test_key_b: "testKeyB" },
        test_arr: [
            { test_arr_key_a: "testKeyA", test_arr_key_b: "testKeyB" },
            { test_arr_key_a: "testKeyA", test_arr_key_b: "testKeyB" },
        ],
    }
```

そして作成したのが、こちらです。（バグがあったらごめんなさい）

https://www.npmjs.com/package/convert-variable-naming-rules

## 開発環境
os: Windows10
node：18.17.0

## 開発

### 準備をする
1. npmアカウント作成
以下URLにて、npmアカウントを作成します。

https://www.npmjs.com/signup

1. コマンドにてLogin
    アカウントを作成したら、コマンドプロントにて以下コマンドを実行します。
    ```cmd
    npm login
    ```

    ログイン状態の確認は以下コマンドにてできます。
    ```cmd
    npm whoami
    ```

### Typescriptの設定をする
1. プロジェクトフォルダを作成し、npm初期化を行う

    開発準備をします。
    以後出てくる```convert-variable-naming-rules```は公開するパッケージ名となります。
    使用されていないパッケージ名を調べる際は、以下で検索してみましょう。
    未使用だったら404となります。早い者勝ちです。使いましょう。
    https://www.npmjs.com/package/＜使用したいパッケージ名＞

    ```cmd
    $ mkdir convert-variable-naming-rules
    $ cd ./convert-variable-naming-rules    
    ```
    ```cmd
    $ npm init

    package name: (convert-variable-naming-rules)
    version: (1.0.0) 
    description: Convert camel case to snake case. And vice versa
    entry point: (index.js)
    test command:
    git repository:
    keywords:
    author: Tomoki Horiuchi
    license: (ISC) MIT
    ```
    
    ```npm init```の選択肢は以下項目以外は初期値で```package.json```が作成します。
    - ```description```：Convert camel case to snake case. And vice versa
        - 説明分です。
    - ```author```：Tomoki Horiuchi
        - 作者です。私の事です。
    - ```license```：MIT
        - このパッケージのライセンスです。MITについては↓
    [wiki：MIT_License](https://en.wikipedia.org/wiki/MIT_License)


    ```package.json```が生成されました。
    ```json: package.json
	{
	  "name": "convert-variable-naming-rules",
	  "version": "1.0.0",
	  "description": "Convert camel case to snake case. And vice versa",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
	  "author": "Tomoki Horiuchi",
	  "license": "MIT",
	}
    ```
    後ほど```typescript```の設定をしますが、先に必要な項目を```package.json```に追加してしまいます。
    ```json: package.json
	{
	  "name": "convert-variable-naming-rules",
	  "version": "1.0.0",
	  "description": "Convert camel case to snake case. And vice versa",
      "main": "dist/index.js",      // 追記: 最初に呼ばれるスクリプトファイルの指定
      "types": "dist/index.d.ts",   // 追記: 型定義ファイルの指定
      "type": "commonjs",           // 追記: これは必要なのか…？後に記載
      "scripts": {
        "build": "tsc",             // 追記: Buildコマンド
        "test": "jest"              // 追記: Testコマンド
      },
	  "author": "Tomoki Horiuchi",
	  "license": "MIT",
	}
    ```

    ※```"type": "commonjs",           // 追記: これは必要なのか…？後に記載```について
    ```"type": "commonjs"```なしでBuild⇒Publishにてパッケージ公開をした際、他プロジェクトにて使用した際、以下のエラーが出力されました。
    ```
    Error [ERR_REQUIRE_ESM]: require() of ES Module C:\01_repository\0_tech_campus\npm\test\node_modules\test-horiuchi\dist\index.js from C:\01_repository\0_tech_campus\npm\test\index.ts not supported.
    index.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules.
    Instead rename index.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in C:\01_repository\0_tech_campus\npm\test\node_modules\test-horiuchi\package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).
    ```
    どうやらモジュールの仕様として、互換性がない旨のエラーの様です。
    詳しい方がいたので載せておきます。
    **[CommonJSとES Modulesについてまとめる](https://zenn.dev/yodaka/articles/596f441acf1cf3)**

    　
1. ```typescript```の設定を行う
    今回はtypescriptで実装するため、```typescript```のインストールと、```tsconfig.json```の設定をしていきます。
    
    ```cmd
    npm install typescript
    npx tsc --init
    ```

    ```json: tsconfig.json
    {
      "compilerOptions": {
        "target": "es2016",
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "declaration": true,   // 追記: 型定義ファイルの出力
        "sourceMap": true,     // 追記: ソースマップの出力
        "outDir": "./dist",    // 追記: 出力先を指定
      },
      "include": [
        "src/**/*.ts"          // 追記: コンパイル対象を指定
      ]
    }
    ```
    　
    
1. 必要なファイルやフォルダを作成
    ```cmd
    mkdir src
    touch .npmignore
    ```

    ```.npmignore```にはパッケージ公開の対象外を定義できます。
    今回はコンパイル済みの```dist```の中身をライブラリ本体とするので、とりあえずこんなこところです。
    ```txt
    .vscode
    /node_modules
    /src
    /test
    tsconfig.json
    ```

### 開発する
今回は前述していたように、```Strgin値```と```jsonのKey```を```CamelCase```⇔```SnakeCase```に変換するパッケージを開発してみます。
特に説明することもないので、簡単にコードだけ貼っておきます。

```cmd
mkdir src
touch ./src/index.ts
touch ./src/ToCamelCase.ts
touch ./src/ToSnakeCase.ts
```

```typescript: index.ts
export * from "./ToSnakeCase";
export * from "./ToCamelCase";
```

```typescript: ToCamelCase.ts
export class ToCamelCase {
  /**
   * camelCase word to snake_case word
   *
   * example:
   * camel_case => camelCase
   * @param snakeStr
   * @returns snake case word
   */
  convertSnakeWordToCamel = (snakeStr: string) => {
    return snakeStr.replace(/_([a-z])/g, (_, group) => {
      return group.toUpperCase();
    });
  };

  /**
   * object`s camelCase key to snake_case
   *
   * example:
   * {
   *   test_str: 'testStr',
   *   test_obj: { test_key_a: 'testKeyA', test_key_b: 'testKeyB' },
   *   test_arr: [
   *     { test_arr_key_a: 'testKeyA', test_arr_key_b: 'testKeyB' },
   *     { test_arr_key_a: 'testKeyA', test_arr_key_b: 'testKeyB' }
   *   ]
   * }
   * =>
   * {
   *   testStr: "testStr",
   *   testObj: { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" },
   *   testArr: [
   *     { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" },
   *     { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" }
   *   ],
   * }
   * @param snakeObj
   * @returns camel case key object
   */
  convertSnakeObjKeyToCamel: any = (snakeObj: any) => {
    if (typeof snakeObj !== "object" || snakeObj === null) {
      return snakeObj;
    }

    if (Array.isArray(snakeObj)) {
      return snakeObj.map((item: object) =>
        this.convertSnakeObjKeyToCamel(item)
      );
    }

    const camelObj: any = {};
    for (const key in snakeObj) {
      if (snakeObj.hasOwnProperty(key)) {
        const snakeKey = this.convertSnakeWordToCamel(key);
        camelObj[snakeKey] = this.convertSnakeObjKeyToCamel(snakeObj[key]);
      }
    }

    return camelObj;
  };
}

```

```typescript: ToSnakeCase.ts
export class ToSnakeCase {
  /**
   * camelCase word to snake_case word
   *
   * example:
   * camelCase => camel_case
   * @param camelStr
   * @returns snake_case word
   */
  convertCamelWordToSnake = (camelStr: string) => {
    return camelStr.replace(/([A-Z])/g, "_$1").toLowerCase();
  };

  /**
   * object`s camelCase key to snake_case
   *
   * example:
   * {
   *   testStr: "testStr",
   *   testObj: { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" },
   *   testArr: [
   *     { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" },
   *     { testArrKeyA: "testKeyA", testArrKeyB: "testKeyB" }
   *   ],
   * }
   * =>
   * {
   *   test_str: 'testStr',
   *   test_obj: { test_key_a: 'testKeyA', test_key_b: 'testKeyB' },
   *   test_arr: [
   *     { test_arr_key_a: 'testKeyA', test_arr_key_b: 'testKeyB' },
   *     { test_arr_key_a: 'testKeyA', test_arr_key_b: 'testKeyB' }
   *   ]
   * }
   * @param camelObj
   * @returns snake_case object
   */
  convertCamelObjKeyToSnake: any = (camelObj: any) => {
    if (typeof camelObj !== "object" || camelObj === null) {
      return camelObj;
    }

    if (Array.isArray(camelObj)) {
      return camelObj.map((item: object) =>
        this.convertCamelObjKeyToSnake(item)
      );
    }

    const snakeObj: any = {};
    for (const key in camelObj) {
      if (camelObj.hasOwnProperty(key)) {
        const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
        snakeObj[snakeKey] = this.convertCamelObjKeyToSnake(camelObj[key]);
      }
    }

    return snakeObj;
  };
}
```


### 一応テストもする
一応パッケージ公開するものなのでテストもしておきましょう。
せっかく作ったので供養させてください。読み飛ばして大丈夫です。
こちらも特に説明することもないので、簡単にコードだけ貼っておきます。

```cmd
npm i -D jest ts-jest @types/jest
npx ts-jest config:init

mkdir test
touch ./test/const.ts
touch ./test/ToCamelCase.test.ts
touch ./test/ToSnakeCase.test.ts
```

```typescript: test/const.ts
export * from "./ToSnakeCase";
export * from "./ToCamelCase";
```

```typescript: test/ToSnakeCase.test.ts
import { ToCamelCase } from "../src/ToCamelCase";
import { camelStr, snakeStr, camelObj, snakeObj } from "./const";

test("test convertSnakeWordToCamel", () => {
  const value = new ToCamelCase().convertSnakeWordToCamel(snakeStr);
  expect(value).toBe(camelStr);
});

test("test convertSnakeObjKeyToCamel", () => {
  const value = new ToCamelCase().convertSnakeObjKeyToCamel(snakeObj);
  expect(value).toEqual(camelObj);
});
```

```typescript: test/ToCamelCase.test.ts
import { ToSnakeCase } from "../src/ToSnakeCase";
import { camelStr, snakeStr, camelObj, snakeObj } from "./const";

test("test convertCamelWordToSnake", () => {
  const value = new ToSnakeCase().convertCamelWordToSnake(camelStr);
  expect(value).toBe(snakeStr);
});

test("test convertCamelObjKeyToSnake", () => {
  const value = new ToSnakeCase().convertCamelObjKeyToSnake(camelObj);
  expect(value).toEqual(snakeObj);
});
```

テスト結果
```cmd
PASS  test/ToCamelCase.test.ts
PASS  test/ToSnakeCase.test.ts

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        3.514 s
Ran all test suites.
```
✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌


### パッケージを公開する
いよいよ自分のパッケージが世に放たれます。
以下コマンドでパッケージの公開ができます。
```--access public```はつけていないとエラーになる報告がありましたが、私はつけないでも大丈夫でした。
でもここに記述するものには一応つけておきます。保険です。
```cmd
npm publish --access public
```

ちなみに、２回目以降に公開する際、```package.json```の```version```を更新してあげる必要があります。
更新してあげないとこんなエラーが出て拗ねます。
```cmd
npm ERR! code E403
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/convert-variable-naming-rules - You cannot publish over the previously published versions: 1.0.36.
npm ERR! 403 In most cases, you or one of your dependencies are requesting
npm ERR! 403 a package version that is forbidden by your security policy, or
npm ERR! 403 on a server you do not have access to.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\...\_logs\2023-12-17T06_37_04_446Z-debug-0.log
```

失敗した！って時の非公開コマンドはこちらです。
```cmd    
npm unpublish ＜パッケージ名＞
```

### パッケージの動作確認する
早速公開したパッケージの動作確認してみましょう。
こちらに関してはすぐです。
新しいプロジェクトを作成し、今回開発したパッケージを呼び出すだけです。

```cmd
mkdir plugin-test
cd ./plugin-test
npm init
npm i -D ts-node typescript
npm i convert-variable-naming-rules
touch index.ts
```
```typescript: index.ts
import { ToSnakeCase } from "convert-variable-naming-rules";

const toSnakeCase = new ToSnakeCase();
console.log(toSnakeCase.convertCamelWordToSnake("camelCase"));
```
実行します。
```cmd
npx ts-node index.ts
```
実行結果
```cmd
camel_case
```

OK！

## 最後に

普段開発する手順に加え、
**npmアカウント作成&ログイン**+αをするだけでnpmパッケージの公開ができてしまいました。
今回は対象にしていま線が、グローバルインストール用のパッケージも簡単に作れるそうです。

是非皆さんも開発し、便利に楽しく開発で切る環境を構築していきましょう。
個人的には、ネタパッケージが増えることを渇望しております。

**記事の内容に間違いがありましたらそっと連絡ください**

## 参考サイト・書籍
- [npm パッケージを TypeScript で自作して公開、別モジュールからインストールして使うまでの手順](https://zenn.dev/k0kishima/articles/d75f4dc5bd1a26#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%A7%E7%9B%AE%E6%8C%87%E3%81%99%E3%81%93%E3%81%A8)
- [CommonJSとES Modulesについてまとめる](https://zenn.dev/yodaka/articles/596f441acf1cf3)
- [nodejs > packages > type](https://nodejs.org/api/packages.html#type)