utamaro’s blog

誰かの役に立つ情報を発信するブログ

alembicで環境別にマイグレーションファイルを分割する

alembicというマイグレーション管理ライブラリを利用したマイグレーションファイルの分割方法について。

実現したいのは、開発環境のrevisionと本番環境用のrevisionを分けたいということです。

分けることで、リリースタイミングですべてのテストが完了したrevisionのみ作成されて、マイグレーションファイルをきれいに管理できるようにするためです。

また、本番リリースのタイミングで開発環境上のマイグレーションを破棄して、本番用のバージョンに置き換えるということもできます。

最終的なディレクトリ構成は以下のようになります。

.
├── alembic.dev.ini
├── alembic.prod.ini
├── app
│   ├── __init__.py
│   └── models
│       ├── __init__.py
│       └── users.py
├── app.py
├── db
│   ├── __init__.py
│   ├── migrate_dev
│   │   ├── README
│   │   ├── env.py
│   │   ├── script.py.mako
│   │   └── versions
│   │       ├── 2020_02_23_123853_.py
│   │       ├── 2020_02_23_125416_.py
│   │       └── __init__.py
│   └── migrate_prod
│       ├── README
│       ├── env.py
│       ├── script.py.mako
│       └── versions
│           ├── 2020_02_23_125959_.py
│           └── __init__.py
└── static

準備

依存ライブラリ

SQLAlchemy==1.3.13
alembic==1.4.0
mysqlclient==1.4.6

pythonのバージョン

Python 3.8.0

環境毎に初期化を行う

※ 一つ一つメモを取りつつ行ったわけではないため、コマンド実行時の結果に差がでる可能性があります。ご参考程度によろしくおねがいします。

initコマンド使用して初期化します。

alembic init db/migrate_dev

このとき、カレントディレクトリ内にalembic.iniというファイルが作成されます。 このファイルをalembic.dev.iniにリネームします。

mv alembic.ini alembic.dev.ini
alembic init db/migrate_prod

こちらも同様にalembic.prod.iniにリネームしてください。

mv alembic.ini alembic.prod.ini

環境別のenvを修正する

devprodenv.pyは同じことを書いてください。

import importlib
import os
import sys
from logging.config import fileConfig

from sqlalchemy import engine_from_config, MetaData
from sqlalchemy import pool

from alembic import context

config = context.config

fileConfig(config.config_file_name)

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MODELS_ROOT = BASE_DIR + '/../app/models'
sys.path.append(MODELS_ROOT)

target_models = [
    "users",
]


class BaseEnv:
    @staticmethod
    def make_target_metadata():
        lst = list(map(lambda x: importlib.import_module(x).Base.metadata, target_models))
        m = MetaData()
        for metadata in lst:
            for t in metadata.tables.values():
                t.tometadata(m)
        return m


def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """

    alembic_config = config.get_section(config.config_ini_section)
    connectable = engine_from_config(
        alembic_config,
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    target_metadata = BaseEnv.make_target_metadata()
    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            compare_type=True,
        )

        with context.begin_transaction():
            context.run_migrations()


run_migrations_online()

今回はrevision --autogenerateで自動的にモデルを読み込むためにBaseEnv.make_target_metadata()を作成しました。

残念ながらenv.pyが実行されるタイミングがalembic revisionが実行されたタイミングなのでapp以下のmodelsを読み込むのが難しいです。

そのために、以下のようにモデルまでのパスを登録しています。

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MODELS_ROOT = BASE_DIR + '/../app/models'
sys.path.append(MODELS_ROOT)

これを行うことで、以下のようにモデルを定義できます。

target_models = [
    "users",
]

最後にメタデータを取得するための関数を通して、target_metadataを作成しています。

usersテーブル用のモデルを作成する

よくある書き方なので詳細については省略します。

from sqlalchemy import DATETIME, Column, Integer, String, BigInteger
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class User(Base):
    __tablename__ = "users"

    id = Column(BigInteger, primary_key=True, nullable=False)
    name = Column(String(255), nullable=False)
    email = Column(String(255), nullable=False)
    created_at = Column(DATETIME, nullable=False)
    updated_at = Column(DATETIME, nullable=False)

.iniファイルを修正する

initで作成されたファイルのうち、[alembic]を以下のように修正します。

[alembic]
script_location = db/migrate_dev
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
sqlalchemy.url = mysql://root:admin@localhost:3306/sample_a

※ sqlalchemyとmysqlclientを利用しているため、sqlalchemy.urlは適宜読み替えてください。

同様にalembic.prod.iniも修正します。

[alembic]
script_location = db/migrate_prod
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
sqlalchemy.url = mysql://root:admin@localhost:3306/sample_b

今回はわかりやすく別スキーマを用意して、マイグレーション対象のDBを分けました。

これで、alembic.dev.iniの場合はsample_aDBに対してマイグレーションが実行され、alembic.prod.iniの場合はsample_bDBに対して実行されるようになります。

revisionを作成する

revisionを作成する際に以下のことが必要です。

  1. .iniを指定する。
  2. modelsから自動的に読み込む。

これを以下のコマンドで実現します。

alembic -c ./alembic.prod.ini revision --autogenerate

実行後にdb/migrate_prod/versions2020_02_23_123612_.pyというファイルが作成されます。

その中が以下のようになっていれば成功です。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.BigInteger(), nullable=False),
    sa.Column('name', sa.String(length=255), nullable=False),
    sa.Column('email', sa.String(length=255), nullable=False),
    sa.Column('created_at', sa.DATETIME(), nullable=False),
    sa.Column('updated_at', sa.DATETIME(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###

同様にdevも実行してみてください。

alembic -c ./alembic.dev.ini revision --autogenerate

DBをマイグレーションする

作成された2020_02_23_123612_.pyをDBに反映させます。

alembic -c alembic.dev.ini upgrade head

これを実行すると、sample_aスキーマusersテーブルが生成され、alembic_versionテーブルにRevision IDが追加されます。

devで作業して、成果物をprodに反映する

モデルを修正して、スキーママイグレーションします。

今回はUserモデルにdeletedを追加します。

deleted = Column(Boolean, default=False)

修正分のリビジョンを作成します。

alembic -c ./alembic.dev.ini revision --autogenerate

2020_02_23_125416_.pyが作成されて、以下のスクリプトが生成されました。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('deleted', sa.Boolean(), nullable=True))
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'deleted')
    # ### end Alembic commands ###

マイグレーションします。

alembic -c alembic.dev.ini upgrade head

これでdeletedカラムが追加されます。

この段階でdb/migrate_dev/versionsの中身は以下のようになっています。

db/migrate_dev
├── README
├── env.py
├── script.py.mako
└── versions
    ├── 2020_02_23_123853_.py
    ├── 2020_02_23_125416_.py
    └── __init__.py

この段階でdb/migrate_prod/versionsの中身は以下のようになっています。

(migrate_prodversionsに複数のファイルが含まれておらず、きれいなままです。)

db/migrate_prod
├── README
├── env.py
├── script.py.mako
└── versions
    └── __init__.py

usersテーブルの成果物を本番環境に反映します。

alembic -c ./alembic.prod.ini revision --autogenerate

以下のようにスクリプトが生成されます。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.BigInteger(), nullable=False),
    sa.Column('name', sa.String(length=255), nullable=False),
    sa.Column('email', sa.String(length=255), nullable=False),
    sa.Column('deleted', sa.Boolean(), nullable=True),
    sa.Column('created_at', sa.DATETIME(), nullable=False),
    sa.Column('updated_at', sa.DATETIME(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###

あとはマイグレーションするだけです。

alembic -c alembic.prod.ini upgrade head

これでsample_bスキーマusersテーブルが生成されました。

参考

https://alembic.sqlalchemy.org/en/latest/tutorial.html

http://okerra.hatenablog.com/entry/2020/02/05/184237

Flaskで共通的なpath変数を検証する方法

/api/<lang_code>/sampleのようなurlが用意されている場合、lang_codeはすべてのurlで利用する場合を想定します。

このとき、controller側でパタメータを取得し、検証する方法もあります。

ただし、すべてのcontrollerで同じ処理を書いてしまうのは避けるべきことです。

また、うっかり実装を忘れてしまうこともあります。

apiを用意する

class basedapiを用意しています。

本題と逸れる実装については解説を省きます。

今回はSampleApiを用意しました。

class SampleApi(ApiResource):
    endpoint = 'SampleApi'
    url_prefix = '/api/<lang_code>/sample'
    methods = ['POST']
    url_rules = [
        UrlRule(
            endpoint='sample',
            rule='index',
            methods=['POST'],
        )
    ]

    def post(self, lang_code):
        body = request.json
        print("called sample api.", body)
        return jsonify(body)

このapicurlを使って実行すると以下のようになります。

curl -X POST -H "Content-Type: application/json" -d '{
    "title": "sample title"
}' http://127.0.0.1:8080/api/ja/sign-in/index

このときの/ja/の部分を取得して、DBからデータを取得するなどできるようにします。

他にも、アプリケーションコンテキスト中にデータを格納できるgに値を入れるなどできます。

url_value_preprocessorの設定を行う

Flaskを拡張して、独自の設定を追加できるclassを作成します。

from app.request_module.pre_processors import language_code

class AppFlask(Flask):
    """Extended version of `Flask` that implements custom config class"""

    def init_url_value_preprocessor(self):
        self.url_value_preprocessor(language_code)

url_value_preprocessorlanguage_codeという関数を設定しています。

url_value_preprocessorFlask.before_request(f)の後に実行されます。

https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.url_value_preprocessor

language_code関数を用意する

url_value_preprocessorで登録する関数にはendpointvaluesが必要です。

def language_code(endpoint, values):
    api_class = url_for(endpoint, lang_code="ja")
    if 'lang_code' in values or not g.lang_code:
        return

今回のSampleApiを実行した場合、endpointvaluesにはそれぞれ以下のようになります。

endpoint: SampleApi.sample
values:
  lang_code: ja

yamlで書いていますが、yamlが値に入るわけではありません。

endpointurl_forで使用可能な値です。

あとはapiを実行するだけで、先程作成したlanguage_code関数が実行されます。

material-iconsをchrome拡張のcontent-scriptで使用する方法

環境を整える

開発環境を整えます。

今回使うライブラリは以下のものを使います。

"dependencies": {
  "css-loader": "^2.1.1",
  "file-loader": "^3.0.1",
  "jquery": "^3.4.1",
  "material-icons": "^0.3.1",
  "webpack": "^4.32.2",
  "sass-loader": "^7.1.0",
  "style-loader": "^0.23.1",
  "ts-loader": "^6.2.1",
  "typescript": "^3.7.3",
  "url-loader": "^1.1.2",
}

他にも依存関係を使っているのですが、細かいところは省いています。

webpackを使ってtypescriptで書かれたスクリプトをビルドして、それを拡張機能として利用します。

また、content-scriptを使って画面を拡張してmaterial-iconsのアイコンを表示します。

webpackの設定を行う

module.exports = {
  module: {
    rules: [
      {
        test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        use: [
          {
            loader: 'url-loader?limit=10000&name=../fonts/[name].[ext]',
          }
        ]
      },
      {
        test: /\.(ttf|eot|svg)(\?[\s\S]+)?$/,
        use: [
          {
            loader: 'file-loader?name=../fonts/[name].[ext]',
          }
        ]
      },
    ]
  }
}

ここで大事なのはloaderの部分です。

nameを指定することで出力時のwoffファイルがfonts/MaterialIcons-Regular.woffとのようになります。

出力時のpathはwebpack.configのあるディレクトリがルートとなります。

これを行わない場合、ファイル名がhashとなります。

そうすると、画面上でwoff等を読み込むのが難しくなります。 ※ build時に再度生成されるため、毎回書き直す必要が出てきます。

manifest.jsonを設定する

{
  "content_scripts": [
    {
      "run_at": "document_end",
      "matches": ["*://*/*"],
      "js": [ 
        "./assets/js/content.bundle.js"
      ]
    }
  ],
  "web_accessible_resources": [
    "assets/fonts/MaterialIcons-Regular.eot",
    "assets/fonts/MaterialIcons-Regular.ttf",
    "assets/fonts/MaterialIcons-Regular.woff",
    "assets/fonts/MaterialIcons-Regular.woff2"
  ],
}

このようになります。

ここで重要なのは、web_accessible_resourcesにフォント用のファイルを書いていることです。

(ワイルドカード*を使っても良いですが、使えるリソースを把握するのにすべて書いたほうが良いと考えてこのようにしています。)

これを行うことで、画面上からリソースを使用することができます。

このパラメータについてはManifest - Web Accessible Resourcesを参照するのが良いです。

リソースを扱う場合は以下のようにurlを書かなければなりません。

chrome-extension://[PACKAGE ID]/[PATH]

web_accessible_resourcesを使わない場合material-iconsで使用するwoffなどが以下のように読み込まれます。

https://[domain]/~.woff

もちろんそのページで.woffを使用していない限り、これはエラー(404)となります。

もう一点重要な点があります。

"assets/fonts/MaterialIcons-Regular.eot",

リソースの書き方です。

最初は以下のように相対パスで実装していました。

{
  "web_accessible_resources": [
    "./assets/fonts/MaterialIcons-Regular.eot",
  ]
}

これは動きませんでした。

原因はわかりませんでした。

予想ではリソースの読み込み時に利用するurlが異なるからです。

画面に@font-faceを埋め込む

manifestの設定時に解説した通り、リソースを利用する場合は以下のように使用する必要があります。

chrome-extension://[PACKAGE ID]/[PATH]

このパスを作成するのに必要なのが、拡張機能のIDです。

IDは開発時と配信時に別のIDが付けられるため、ハードコーディングでは対応できません。

googleはこのurlを作成するための機能を用意しています。

以下を使用します。

chrome.extension.getURL("");

getURL("hoge.png")を利用することでchrome-extension://[PACKAGE ID]/hoge.pngとうurlを取得できます。

以上を踏まえて、画面上に@font-face<style>として埋め込みます。

function makeFontFace() {
  let woff2 = chrome.extension.getURL("assets/fonts/MaterialIcons-Regular.woff2");
  let woff = chrome.extension.getURL("assets/fonts/MaterialIcons-Regular.woff2");
  let ttf = chrome.extension.getURL("assets/fonts/MaterialIcons-Regular.woff2");
  let newStyle = document.createElement('style');
  newStyle.textContent = '\
  @font-face {\
      font-family: "Material Icons TestExtension";\
      font-style: normal;\
      src: url("' + woff2 + '") format("woff2"),\
        url("' + woff + '") format("woff"),\
        url("' + ttf + '") format("truetype");\
  }'
  document.head.appendChild(newStyle);
}

これで画面上からリソースを利用できるようになります。

jquery使ってないというツッコミはなしでお願いします。

ここで重要なのはfont-familyの値として、Material Icons TestExtensionとしていることです。

もし画面上でMaterial Iconsを使っていた場合、コンフリクトが発生する可能性があります。

あとはこの関数を実行するだけです。

cssを追加する

私の場合はscssを使っています。が、sassの場合は読み替えてください。

.test-extension {
  .material-icons {
    font-family: "Material Icons TestExtension";
    font-weight: normal;
    font-style: normal;
    font-size: 24px;
    display: inline-block;
    line-height: 1;
    text-transform: none;
    letter-spacing: normal;
    word-wrap: normal;
    white-space: nowrap;
    direction: ltr;
    /* Support for all WebKit browsers. */
    -webkit-font-smoothing: antialiased;
    /* Support for Safari and Chrome. */
    text-rendering: optimizeLegibility;
    /* Support for Firefox. */
    -moz-osx-font-smoothing: grayscale;
    /* Support for IE. */
    font-feature-settings: 'liga';
  }
}

重要なのは、Material Iconsに書かれているように以下のように使用しないことです。

$material-icons-font-path: '~material-icons/iconfont/';
@import '~material-icons/iconfont/material-icons.scss';

このようにすると、せっかく参照できるようにしたリソースファイルを利用してくれません。

ちなみに、上記のスタイルは以下のファイルを参考にしました。

node_modules/material-icons/iconfont/material-icons.css

まとめ

やることは以下のことです。

  • リソースを登録する
  • @font-faceを埋め込む
  • material-iconsのスタイルを独自に用意する

以上です。

これを解決するのに1日かかってしまいました。

だれかの参考になれば幸いです。

リファクタリングを行うタイミングについて

最近リファクタリングについて考える機会がありました。

今回はリファクタリングを実施するタイミングについて、これまで経験したことを記事にしたいと思います。

記事の流れとしては以下のように書きたいと思います。

  1. リファクタリングを行う理由
  2. リファクタリングのタイミングについて
  3. 全体的な開発の流れ

1. リファクタリングを行う理由

リファクタリングを行うことで、内部構造を改善して理解や修正が簡単になります。

この考えは長く開発をしている方はよく理解しているかと思います。

私がこれまで経験した一番悪いケースを紹介すると「10年間リファクタリングをしていないため、重複箇所が多く影響調査に1週間かかる」というものでした。

10年間というキーワードが出てきたことでお気づきかと思いますが、古いシステムです。 PHPを使って作ったシステムで、フレームワークも当時のものを使っています。

控えめに言ってやばいです。

これが原因で上(社長等)から「もっと成果を上げろ」と言われます。

成果を上げるためには重複箇所を修正しなければなりません。 しかし、その修正が目に見えるような成果として出ないものなので、上がなかなか納得してくれません。 説得するための資料を作成するという無駄に時間のかかる作業が必要になり、ベテランのエンジニアが転職していきます。 最終的に、プロダクトを深く理解している人がいなくなり誰も触れないプログラムが出来上がりました。

こうなってしまうと、要件定義から始めて作り直す方が良いかもしれません。

より長く製品を維持するためにリファクタリングが必要になってきます。

2. リファクタリングのタイミングについて

同じコードを2回書いた時に行う

3度目の法則というものがあるのですが、私は2回目で修正を行うべきだと考えています。

経験上、2度同じコードを書いたものは、また別の箇所で同じコードを書いてしまいます。 そして、3つ同じコードが書かれることで修正範囲が広くなってしまい、「修正しよう」という意思が萎えます。 そして重複コードが量産され、作り終わって時間が出来たら直そうという考えに至ります。 そして、その未来はやって来ません。 そうならないように2度目で修正を行います。

修正する際には機能追加や機能改善は行わないようにします。 同時に進めてしまうと、結果の検証時に振る舞いが変わっていないかか確認しづらくなります。

プログラムに手を加えた後で行う

これは多くのチームでやっていないことかと思います。これまで4つのチームで開発を行い全てでやっていませんでした。 機能追加や機能改善、バグフィックス等を行ったあとに、リリース作業を行って終了というケースです。

リリース前にリファクタリング期間を入れます。

手を加えたコードに対して重複箇所をなくしたり、仕方がなく冗長な書き方をしている箇所にコメントを入れたり。 デザインテンプレートを適用して、予想できる機能追加に対して手を打っておきます。

これを行うことによって、製品の品質が維持・向上できます。

リリース前に行う

リリース後のリファクタリングには苦労します。

利用者に影響が出ないように慎重に行う必要がありますし、万が一問題が出たら詫び石を配布しなければならないかもしれません。

twitterで謝罪したり、原因調査等行わないとならないです。

正直面倒です。

そうならないように、リリース前に一度リファクタリングします。

3. 全体的な開発の流れ

リファクタリング期間を入れた際の大まかな開発の流れです。

  1. 設計
  2. 仮実装(要件は満たす状態)
  3. 実装(クラス構造を見直したりする)
  4. テスト作成
  5. リファクタリング(既存のコードと設計を合わせたりする)
  6. リリース or 次の開発

業務では(上からの圧力により)このような流れでできていないのですが、個人開発ではこのように進めています。

がむしゃらに進むより速度は遅いですが、スムーズに進みます。

イメージとしては3歩進んで1回休憩という感じです。 休憩中に、もっと良い方法など考えます。 歩き方が効率化されて、目的地まで疲れずに辿り着けます。

属性付きセレクタを作成する関数を作った。

属性付きセレクタを作成する関数を作った。

jqueryである属性がついたタグを取得したいと思ったときに、セレクタをハードコーディングすることがあります。

例えば以下のようなものです。

$("div[id='hoge']")

hogeという値が動的に指定できる場合はもう少し面倒になります。

$("div[id='"+ val +"']")

たまに事故が起きるのは'が抜けていたり、"で囲えていなくてビルドがエラーになったりします。

今回はこういうことが無いように便利なutilを作りました。

Attrクラスを作成する

このクラスを使って属性指定します。

export class Attr {
  private key: string
  private val: string

  constructor(key: string, val: string) {
    this.key = key
    this.val = val
  }

  getKey() { return this.key }
  getVal() { return this.val }
}

ちなみに、僕はconstructorに引数が2つの場合はsetterを作らないようにしてます。

3つ以上になったらsetterを作成します。

属性付きセレクタを生成するためのutilを作成する

Attrクラスをimportしています。

今回はmakeAttrSelectorという関数を作りました。

import {Attr} from './Attr'

export var SelectorUtils = {
  makeAttrSelector: function(target: string, attrs?: Array<Attr>) {
    if (attrs == void(0)) {
      return target
    }
    let selector = target + (function(attrs: Array<Attr>): string {
      let result = ""
      attrs.forEach((attr: Attr, index, array) => {
        result += "[" + attr.getKey() + "='" + attr.getVal() + "']"
      })
      return result
    })(attrs)
    return selector
  }
}

属性の指定がない場合にも対応可能なように、attrs引数はオプションとしています。

このメソッドを実行すると以下のようになります。

let selector = SelectorUtils.tagAttrs(
  "#test", 
  [new Attr("A", "A"), new Attr("B", "B")]
)
console.debug(selector)

結果

#test[A='A'][B='B']

new Attr("A", "A")が面倒に思えるかもしれません。 ただし、[{key: A, val: A}, {key: B, val: B}]という方法に変えた場合、タイプミスが発生する可能性が高いです。 このような理由から今回はclassを使いました。

テスト

便利なutilを作成した場合は必ずテストを書きましょう。

多くの場所で使われることが想定できるので、誰かが修正した場合に品質を担保する必要があります。

今回はjestを使用してテストを作成しました。

import {SelectorUtils} from '../SelectorUtils'
import {Attr} from '../Attr'

describe('test', () => {
  let idAttr = "#test"

  test.each([
    [null, idAttr],
    [[new Attr("A", "A")], idAttr + "[A='A']"],
    [[new Attr("A", "A"), new Attr("B", "B")], idAttr + "[A='A'][B='B']"]
  ])("属性付きセレクタの生成テスト", (attrs, expected) => {
    let selector = SelectorUtils.tagAttrs(idAttr, attrs)
    console.log({
      actual: selector,
      expected: expected,
      args: [attrs]
    })
    expect(selector).toEqual(expected)
  })

  test("attrs引数を指定しない場合", () => {
    let selector = SelectorUtils.tagAttrs("#test")
    expect(selector).toEqual("#test")
  })
});

テストにログを出すのはなぜかと聞かれたことがあります。

私の場合は以下の理由から出しています。

  • テスト対象の関数が変更されたときにテストを修正しやすくするため

環境ごとに分けたtsconfigを作成する

環境ごとに分けたtsconfigを設定する

想定するのはdevelop環境とprod環境の2つの設定を分けて管理します。

dev環境では、sourceMap: trueで、prod環境ではsourceMap: falseとします。

製品として出すのに元のソースを出すのはかっこ悪いので。。。

devとprodの設定を作成する

tsconfig.dev.json

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",  // TSはECMAScript 5に変換
    "module": "es2015",  // TSのモジュールはES Modulesとして出力
    "moduleResolution": "node",  // node_modules からライブラリを読み込む
  }
}

tsconfig.prod.json

{
  "compilerOptions": {
    "sourceMap": false,
    "target": "es5",  // TSはECMAScript 5に変換
    "module": "es2015",  // TSのモジュールはES Modulesとして出力
    "moduleResolution": "node",  // node_modules からライブラリを読み込む
  }
}

webpackのdev設定を作成する

これについてはwebpack-mergeを使用して分けます。

一つのファイルで実現する方法が思いつきませんでした。

webpack.config.dev.jsを作成します。

// 開発用のコンフィグファイル

const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.base');
// ↑ baseとなるconfigを読み込みます。

// webpack-base.config.jsに同様の設定値がある場合、こちらが優先される。
// baseの設定ファイルをマージします。マージにはwebpack-mergeというライブラリを使ってます。
const devConfig = merge(baseConfig, {
    devtool: 'inline-source-map',
});
devConfig.module.rules.push({
    // 拡張子 .ts の場合
    test: /\.ts$/,
    // TypeScript をコンパイルする
    loader: "ts-loader",
    options: {
        configFile: 'tsconfig.dev.json'
    }
})

module.exports = devConfig;

ここで大事なのは、devConfig.module.rulespushしていることです。

ruleを上書きしないようにしています。

次に、webpack.config.base.jsを作成します。

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const jsRoot = './js';

function makePath(...paths) {
    return path.resolve(__dirname, paths.join('/'));
}

const baseConfig = {
    // エントリーポイントの設定
    entry: {
        'reset': makePath(jsRoot, 'reset.js'),
    },
    // 出力の設定
    output: {
        // 出力するファイル名
        filename: '[name].bundle.js',
        // 出力パス
        path: `${__dirname}/../static/js`,
        publicPath: '/static/js/'
    },
    module: {
        rules: [
          // いろいろな設定を追加する。
        ]
    },
    resolve: {
        extensions: [".ts", ".js"]
    },
    target: 'electron-main',
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: `${__dirname}/static/js/*`
        }),
    ]
};

module.exports = baseConfig;

package.jsonのscriptを作成する

これについてはwebpackを使っている方にとってはよく見るスクリプトかと思います。

{
  "scripts": {
    "webpack-dev": "webpack --config ./webpack.config.dev.js --mode development --progress --display-error-details",
    "webpack-production": "webpack --config ./webpack.config.production.js --mode production --progress --display-error-details"
  }
}

実行する

npm run webpack-devで実行すると、.ts.jsで出力されます。

当初はtscを使うのかと思ってました。

mysqlclientを使ったらdyldエラーが起きた

from MySQLdb import _mysqlが書かれたpythonファイルを実行したら以下のエラーが発生しました。

Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import MySQLdb as _mysql
  File "/lib/python3.8/site-packages/MySQLdb/__init__.py", line 18, in <module>
    from . import _mysql
ImportError: dlopen(/lib/python3.8/site-packages/MySQLdb/_mysql.cpython-38-darwin.so, 2): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
  Referenced from: /usr/local/opt/mysql/lib/libmysqlclient.21.dylib
  Reason: image not found

このエラーによってプログラミング時間として用意していた夜の3時間と、朝の2時間が溶けました。

なんでこんなエラーが起きたのかわからなかったですし、初めての経験だったのであたふたしました。

原因について

brewでインストールしたopensslをswitchすることで解決したので、brewに関係があると思います。

エラー内容にもある通り、opensslが問題だったのかと思います。

今回はswitchで解決したので、依存関係の問題だったのでしょう。

ちなみに、エラーが起きたときにインストールしていたopensslについては以下のバージョンがインストールされていました。

  • openssl/
  • openssl@1.1/

このときwhichコマンドを使って参照先を確認したところ、以下が出力されました。

/usr/local/opt/openssl@1.1/bin/openssl

そして、openssl@1.1には当然ながらlibssl.1.0.0.dylibはありません。

なので、Library not loaded:と言われたのだと思います。

解決方法

使用しているバージョンに誤りがあると考えたので、以下のコマンドを実行しました。

brew switch openssl 1.0.2r

これだけで解決しました。

悩んでいた時間が5時間で、解決が1分未満という。。。

なんといいますか、気分が上がったあとに、疲れて萎えました。

stackoverflowの以下の回答を見て解決方法を思いつきました。ありがとうございます。

https://stackoverflow.com/questions/59006602/dyld-library-not-loaded-usr-local-opt-openssl-lib-libssl-1-0-0-dylib#answer-59184347

最後に

ちなみにですが、コマンドを実行する前に以下のコマンドを実行していました。

brew switch openssl 1.0

結果は

Error: openssl does not have a version "1.0" in the Cellar.
openssl installed versions: 1.0.2q, 1.0.2r

そんなバージョンは無いよ!と。 1.0.2q1.0.2rがインストールされてるよ!と。

なんという親切さ。

ありがとうございました。