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を修正する
dev
、prod
のenv.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_a
DBに対してマイグレーションが実行され、alembic.prod.ini
の場合はsample_b
DBに対して実行されるようになります。
revisionを作成する
revisionを作成する際に以下のことが必要です。
- .iniを指定する。
- modelsから自動的に読み込む。
これを以下のコマンドで実現します。
alembic -c ./alembic.prod.ini revision --autogenerate
実行後にdb/migrate_prod/versions
に2020_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_prod
はversions
に複数のファイルが含まれておらず、きれいなままです。)
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
テーブルが生成されました。
参考
Flaskで共通的なpath変数を検証する方法
/api/<lang_code>/sample
のようなurlが用意されている場合、lang_code
はすべてのurlで利用する場合を想定します。
このとき、controller側でパタメータを取得し、検証する方法もあります。
ただし、すべてのcontrollerで同じ処理を書いてしまうのは避けるべきことです。
また、うっかり実装を忘れてしまうこともあります。
apiを用意する
class based
でapiを用意しています。
本題と逸れる実装については解説を省きます。
今回は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)
このapiをcurl
を使って実行すると以下のようになります。
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_preprocessor
にlanguage_code
という関数を設定しています。
url_value_preprocessor
はFlask.before_request(f)
の後に実行されます。
https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.url_value_preprocessor
language_code関数を用意する
url_value_preprocessor
で登録する関数にはendpoint
とvalues
が必要です。
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
を実行した場合、endpoint
とvalues
にはそれぞれ以下のようになります。
endpoint: SampleApi.sample values: lang_code: ja
※ yamlで書いていますが、yamlが値に入るわけではありません。
endpoint
はurl_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. リファクタリングを行う理由
リファクタリングを行うことで、内部構造を改善して理解や修正が簡単になります。
この考えは長く開発をしている方はよく理解しているかと思います。
私がこれまで経験した一番悪いケースを紹介すると「10年間リファクタリングをしていないため、重複箇所が多く影響調査に1週間かかる」というものでした。
10年間というキーワードが出てきたことでお気づきかと思いますが、古いシステムです。 PHPを使って作ったシステムで、フレームワークも当時のものを使っています。
控えめに言ってやばいです。
これが原因で上(社長等)から「もっと成果を上げろ」と言われます。
成果を上げるためには重複箇所を修正しなければなりません。 しかし、その修正が目に見えるような成果として出ないものなので、上がなかなか納得してくれません。 説得するための資料を作成するという無駄に時間のかかる作業が必要になり、ベテランのエンジニアが転職していきます。 最終的に、プロダクトを深く理解している人がいなくなり誰も触れないプログラムが出来上がりました。
こうなってしまうと、要件定義から始めて作り直す方が良いかもしれません。
より長く製品を維持するためにリファクタリングが必要になってきます。
2. リファクタリングのタイミングについて
同じコードを2回書いた時に行う
3度目の法則というものがあるのですが、私は2回目で修正を行うべきだと考えています。
経験上、2度同じコードを書いたものは、また別の箇所で同じコードを書いてしまいます。 そして、3つ同じコードが書かれることで修正範囲が広くなってしまい、「修正しよう」という意思が萎えます。 そして重複コードが量産され、作り終わって時間が出来たら直そうという考えに至ります。 そして、その未来はやって来ません。 そうならないように2度目で修正を行います。
修正する際には機能追加や機能改善は行わないようにします。 同時に進めてしまうと、結果の検証時に振る舞いが変わっていないかか確認しづらくなります。
プログラムに手を加えた後で行う
これは多くのチームでやっていないことかと思います。これまで4つのチームで開発を行い全てでやっていませんでした。 機能追加や機能改善、バグフィックス等を行ったあとに、リリース作業を行って終了というケースです。
リリース前にリファクタリング期間を入れます。
手を加えたコードに対して重複箇所をなくしたり、仕方がなく冗長な書き方をしている箇所にコメントを入れたり。 デザインテンプレートを適用して、予想できる機能追加に対して手を打っておきます。
これを行うことによって、製品の品質が維持・向上できます。
リリース前に行う
リリース後のリファクタリングには苦労します。
利用者に影響が出ないように慎重に行う必要がありますし、万が一問題が出たら詫び石を配布しなければならないかもしれません。
twitterで謝罪したり、原因調査等行わないとならないです。
正直面倒です。
そうならないように、リリース前に一度リファクタリングします。
3. 全体的な開発の流れ
リファクタリング期間を入れた際の大まかな開発の流れです。
- 設計
- 仮実装(要件は満たす状態)
- 実装(クラス構造を見直したりする)
- テスト作成
- リファクタリング(既存のコードと設計を合わせたりする)
- リリース 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.rules
にpush
していることです。
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の以下の回答を見て解決方法を思いつきました。ありがとうございます。
最後に
ちなみにですが、コマンドを実行する前に以下のコマンドを実行していました。
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.2q
か1.0.2r
がインストールされてるよ!と。
なんという親切さ。
ありがとうございました。