6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TDCソフト株式会社Advent Calendar 2023

Day 17

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

Last updated at Posted at 2023-12-17

初めに

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" },
        ],
    }

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

開発環境

os: Windows10
node:18.17.0

開発

準備をする

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

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

    npm login
    

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

    npm whoami
    

Typescriptの設定をする

  1. プロジェクトフォルダを作成し、npm初期化を行う

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

    $ mkdir convert-variable-naming-rules
    $ cd ./convert-variable-naming-rules    
    
    $ 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

    package.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に追加してしまいます。

    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についてまとめる

     

  2. typescriptの設定を行う
    今回はtypescriptで実装するため、typescriptのインストールと、tsconfig.jsonの設定をしていきます。

    npm install typescript
    npx tsc --init
    
    tsconfig.json
    {
      "compilerOptions": {
        "target": "es2016",
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "declaration": true,   // 追記: 型定義ファイルの出力
        "sourceMap": true,     // 追記: ソースマップの出力
        "outDir": "./dist",    // 追記: 出力先を指定
      },
      "include": [
        "src/**/*.ts"          // 追記: コンパイル対象を指定
      ]
    }
    

     

  3. 必要なファイルやフォルダを作成

    mkdir src
    touch .npmignore
    

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

    .vscode
    /node_modules
    /src
    /test
    tsconfig.json
    

開発する

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

mkdir src
touch ./src/index.ts
touch ./src/ToCamelCase.ts
touch ./src/ToSnakeCase.ts
index.ts
export * from "./ToSnakeCase";
export * from "./ToCamelCase";
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;
  };
}

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;
  };
}

一応テストもする

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

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
test/const.ts
export * from "./ToSnakeCase";
export * from "./ToCamelCase";
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);
});
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);
});

テスト結果

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はつけていないとエラーになる報告がありましたが、私はつけないでも大丈夫でした。
でもここに記述するものには一応つけておきます。保険です。

npm publish --access public

ちなみに、2回目以降に公開する際、package.jsonversionを更新してあげる必要があります。
更新してあげないとこんなエラーが出て拗ねます。

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

失敗した!って時の非公開コマンドはこちらです。

npm unpublish <パッケージ名>

パッケージの動作確認する

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

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

const toSnakeCase = new ToSnakeCase();
console.log(toSnakeCase.convertCamelWordToSnake("camelCase"));

実行します。

npx ts-node index.ts

実行結果

camel_case

OK!

最後に

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

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

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

参考サイト・書籍

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?