Doon - Markdown Notes
上記動画を参考にアプリを作成する。
使用技術
- Electron
- React
- CSS modules
- jotai
- TypeORM
- react-markdown
残タスク
- TypeORMの実装
- UIの修正
- テストコードの記載
- CI/CDの実装
- haskyの取り込み
状態管理 (Jotai) とデータベース (TypeORM) の使い分け
UI状態: ユーザーインターフェースの現在の状態(例: モーダルの開閉、タブの選択状態)、ログイン状態や一時的なエラーメッセージなど、セッションを通じて持続するが、長期間の永続化を必要としない情報を格納する。
永続的なデータ管理: アプリケーションが再起動しても保持されるべきデータを管理する。
Electron + React + TypeScript の開発環境構築手順
Electron + React + TypeScript の開発環境構築
@quick-start/create-electron
npm: @quick-start/create-electron
electron reload
buildを高速化するライブラリ
tsconfig.jsonを理解する
tsconfig.jsonのオプションについて
オプションについての説明
@tsconfig/node16
が便利
TSConfig Basesの
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 16",
"_version": "16.1.0",
"compilerOptions": {
"lib": ["es2021"],
"module": "node16",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node16"
}
}
Electronの場合、メインプロセス(main)とレンダラープロセス(renderer)で環境が異なるためそれぞれのプロセスのために異なるtsconfig.jsonファイルを設定する。
環境の違い
- メインプロセスはNode.js環境で動作し、ElectronのAPIやNode.jsのAPIにフルアクセス可能
- レンダラープロセスはChromium(Web)環境で動作し、WebAPIへのアクセスがメイン
mainプロセス
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "./dist/main",
"lib": ["esnext"],
"noImplicitAny": true,
"sourceMap": true
},
"include": ["src/main/**/*"],
"exclude": ["src/renderer/**/*"]
}
renderプロセス
{
"compilerOptions": {
"module": "esnext",
"target": "es6",
"outDir": "./dist/renderer",
"lib": ["dom", "esnext"],
"noImplicitAny": true,
"sourceMap": true,
"jsx": "react"
},
"include": ["src/renderer/**/*"],
"exclude": ["src/main/**/*"]
}
package.jsonで調整
"scripts": {
"build:main": "tsc -p tsconfig.main.json",
"build:renderer": "tsc -p tsconfig.renderer.json",
"build": "npm run build:main && npm run build:renderer"
}
tsconfig.node.json
とtsconfig.web.json
が存在する理由→ Electronはメインプロセス(Node.js環境)とレンダラープロセス(Webブラウザ環境)の両方のコードが含まれる為
aliasはelectron.vite.config.ts
で設定すればよかった模様
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'@/main': resolve('src/main'),
'@/lib': resolve('src/main/lib')
}
}
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
assetsInclude: 'src/renderer/assets/**',
resolve: {
alias: {
'@/renderer': path.resolve(__dirname, 'src/renderer'),
'@/components': resolve('src/renderer/src/components'),
'@/hooks': resolve('src/renderer/src/hooks'),
'@/assets': resolve('src/renderer/src/assets'),
'@/store': resolve('src/renderer/src/store'),
'@/mocks': resolve('src/renderer/src/mocks')
}
},
plugins: [react()]
}
})
コンテキストの分離について
コンテキスト分離は、Electronのセキュリティ機能の一つ。レンダラープロセスのグローバルスコープをメインプロセスや他のレンダラープロセスから分離することで、セキュリティ(クロスサイトスクリプティング(XSS)攻撃などから守る)を向上させる。
→ ウェブページのJavaScriptがElectronのAPIやNode.jsの機能に直接アクセスすることを防ぐ。
代わりに、contextBridge APIを使用して、メインプロセスから安全に公開された機能のみをレンダラープロセスで利用できる。
contextBridge.exposeInMainWorld(apiKey, api)
コンテキストが分離されているかどうかを確認する
if(process.contextIsolated) {
...
}
Electron vita
Scssで以下エラーが発生
[plugin:vite:css] [sass] Can't find stylesheet to import.
╷
2 │ @use '@/styles/libs/mixins/index' as *;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
╵
src/renderer/src/styles/_base.scss 2:1 @use
src/renderer/src/styles/styles.scss 1:1 root stylesheet
ViteがScssのpathを解決できていないことが原因である模様
nagative merginの使い所
例えば、ヘッダー、フッターの左右と上下の位置は、ページにぴったり配置したいが、親の余白があるために子の配置がうまくいかない場合など。
mixinのtips
cssのテクニック
CSS入れ子チートシート
React.ComponentPropsについて
Props 型定義を自分で用意した場合、メンテナンスコストが高い為お勧めしない。実装が増えるたびに都度都度値を追加していく必要があるため。
type Props = {
value: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>
onBlur?: React.FocusEventHandler<HTMLInputElement>
}
export const Input = ({ value, onChange, onBlur }: Props) => (
<input
value={value}
onChange={onChange}
onBlur={onBlur}
className={styles.input}
/>
)
React.ComponentProps 型を使う
type Props = React.ComponentProps<'input'>
export const Input = ({ className, ...props }: Props) => (
<input
{...props} // <- className 以外、全ての props を分割代入
className={clsx(className, styles.input)}
/>
)
React.ComponentPropsよりもComponentPropsWithoutRef
を使用することでrefを許容しているかを確認できる。
Component設計
Atomic Designについてあらためて評価する
Atomic Designの場合、コンポーネントの粒度を決定する基準が曖昧になることがある。例えば、所望のコンポーネントをOrganismsに作ったらいいのか、Moleculesに作ったらいいのか迷う場面がある。
ドラッグ可能な領域
electronでドラッグ可能な領域はcssの-webkit-app-region: drag
で設定する。
状態管理ライブラリ
▪️ 候補
- Redux
コードが膨大になりやすい。より簡単に記載できるRedux Toolkitは存在する。
DispatchとReducerが存在し、メンテナンスコストが大きい。
すべてのステートを一つのStoreで管理し、selectorにより必要なデータのみ抽出する。
ルールが厳密で大規模開発向き
- Recoil
AtomとSelector:Atomは一つの状態を保持する(Reduxとは異なりデータのソースとなるAtomが複数存在する)
ReduxのDispatchとReducerによる中間操作が不要
対象のコンポーネントを<RecoilRoot/>
で囲む。Atom を作成する際はkey
を指定する必要あり。
key
の管理をどのようにするか。
- Zustand
Redux に近い
- Jotai
コンポーネントで Atomの使用が可能で、プロバイダーでラップする必要なし。keyも不要
シンプルで直感的に操作できるJotaiを採用する。
Reactの歴史
Jotai
atom
- Read-only atom
- Write-only atom
- Read-Write atom
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
null, // it's a convention to pass `null` for the first argument
(get, set, update) => {
// `update` is any single value we receive for updating this atom
set(priceAtom, get(priceAtom) - update.discount)
// or we can pass a function as the second parameter
// the function will be invoked,
// receiving the atom's current value as its first parameter
set(priceAtom, (price) => price - update.discount)
},
)
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2)
// you can set as many atoms as you want at the same time
},
)
const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(read)
const derivedAtomWithReadWrite = atom(read, write)
const derivedAtomWithWriteOnly = atom(null, write)
unwrap
非同期を同期に変換する。
非同期アトムが同期アトムに変換されると、非同期操作の結果がキャッシュされます。これにより、同じデータが再度必要になった場合に再取得の必要がなくなり、パフォーマンスが向上します。
: フォールバック値を設定することで、非同期操作の結果がまだ取得されていない場合でも、代替の値を使用することができます。
useAtom
The useAtom hook is to read an atom value in the state. The state can be seen as a WeakMap of atom configs and atom values.
The useAtom hook returns the atom value and an update function as a tuple, just like React's useState. It takes an atom config created with atom().
const [value, setValue] = useAtom(anAtom)
useAtomValue
Similar to the useSetAtom hook, useAtomValue allows you to access a read-only atom.
const countAtom = atom(0)
const Counter = () => {
const setCount = useSetAtom(countAtom)
const count = useAtomValue(countAtom)
return (
<>
<div>count: {count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
)
}
useSetAtom
In case you need to update a value of an atom without reading it, you can use useSetAtom().
日付操作ライブラリ
以下の日付操作ライブラリを使用する。
最近出てきて使用してみたかったFromKitを使ってみる。
MDX Editor
マークダウンの生成にMDX Editorを使用する。
Toolbar
やCode blocks
が直感的に記述することができ良さげ
MDXEditorを使用したところ、以下のエラーが発生
Uncaught EvalError: Refused to evaluate a string as JavaScript
because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".
at eval (<anonymous>)
at getGlobal (@mdxeditor_editor.js?v=1930ed33:65343:10)
at @mdxeditor_editor.js?v=1930ed33:65344:2
Content Security Policy(CSP)に関連したエラー:XSS攻撃などから保護するために、どのような外部リソースがWebページに読み込まれることを許可するかをブラウザに指示するためのもの。Electornではunsafe-eval を許可しないようにデフォルト設定されている。
MDXEditorや依存ライブラリがeval()関数を使用しているために、警告文がconsole.logに表示されている模様。
eval() 関数は文字列から JavaScript を実行する関数。
一応手段としては、'unsafe-eval' を CSP ルールに追加することは可能(セキュリティリスクは増加する)。
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';">
代替案
react-markdown
, remark
など別のMDXを検討する。
- react-markdown
- SimpleMDE (react-simplemde-editor)
- Draft.js
- ProseMirror
react-markdown
を採用する。
ReactMarkdown 自体は Markdown テキストを解析して表示するためのコンポーネントである為、MDX Editorとは異なり、テキスト入力機能は備えていない。
export const MarkdownEditor = (): JSX.Element => {
const [text, setText] = useState<string>('')
const handletext = (e) => {
setText(e.target.value)
}
return (
<div>
<textarea
id="markdown"
name="markdown"
rows={50}
cols={33}
onChange={handletext}
value={text}
placeholder="Markdown をここに入力"
></textarea>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeHighlight, rehypeKatex, rehypeRaw]}
>
{text}
</ReactMarkdown>
</div>
)
}
textareaが反応しない不具合
Electronアプリでは、コンテンツセキュリティポリシー(CSP)が厳格に設定されているため、インラインスクリプトや外部のリソースがブロックされてしまう。
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
- script-src 'self': スクリプトは自サイトのもののみ許可。
- style-src 'self' 'unsafe-inline': スタイルは自サイトのものとインラインスタイルを許可。
style-src <source> <source>;
は、スタイルシートの有効なソースを指定する。
インラインスタイル(HTML内に直接書かれたスタイルや、JavaScript経由で動的に適用されるスタイル)を許可するための、 unsafe-inline
を削除した場合、ブロックされるためCSS modulesのStyleが崩れる。
その他の要因
ブラウザーウィンドウのプリロードスクリプト
ElectronでReactアプリケーションを実行する場合、BrowserWindowのwebPreferencesオプションでプリロードスクリプトを設定している。
new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.join(__dirname, 'preload.js')
}
});
ファイルパスの問題
Electronでローカルのファイル(HTMLやJavaScript)をロードする際は、パスが正しいかどうかを再確認
win.loadURL(`file://${__dirname}/index.html`);
bodyタグにドラック可能領域を設定していた為、不具合が発生していた。
-webkit-app-region: drag;
公式ドキュメント
ウインドウ全体をドラッグ可能にした場合、ボタンをドラッグ不可として同時にマークしなければなりません。そうでなければ、ユーザーがボタンをクリックすることができなくなります。
button {
cursor: pointer;
-webkit-app-region: no-drag;
}
textarea {
word-break: break-all;
-webkit-app-region: no-drag;
}
ElectronでReact Developer Toolsを使用する場合
scriptタグにローカルホストを指定すると以下のwarnが発生
Refused to load the script 'http://localhost:8097/'
because it violates the following Content Security Policy directive: "script-src 'self'".
Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
script-src-elem
はscriptタグで有効なソースを示す。
公式ドキュメントで以下のdevtoolsも紹介されている。
devtoolsが表示されない。
ログで"ReferenceError: dragEvent is not defined"
のエラーが発生している。
Chrome のアップストリームの問題である模様。
sass-map
get-mapでutiltyを作成する。
$palettes: (
'blue': (
'default': #0075d1,
'l1': #e6f8ff,
'l2': #23445e
),
'green': (
'default': #1ed535,
'd1': #10b02b
),
);
@function p($group, $val: 'default') {
@return map-get(map-get($palettes, $group), $val);
}
dialogについて
ファイルを開いたり、保存したり、アラートを出したりするために、ネイティブのシステムダイアログを表示する。
fs-extra
標準装備のfsモジュールの場合、階層構造となったフォルダを一気にコピーできない。
electron-log
exposeInMainWorld
Electronでセキュリティを強化しながら、レンダラープロセス(通常はReactや他のフロントエンドフレームワークが実行されるプロセス)とメインプロセス(Node.jsが実行されるバックエンドのロジック)が相互に通信できるようにするための方法。
preload
スクリプト内で使用され、特定のAPIを「安全に」レンダラープロセスに公開することが可能。
ElectronのcontextBridge
は、Node.jsの機能に直接アクセスするのではなく、選択されたAPIだけを公開することで、レンダラープロセスのセキュリティを向上させる。マルウェアなどの攻撃を受けても公開されていないNode.jsのAPIにはアクセスできない。
const writeNote = async (): Promise<WriteNote> => {
return ipcRenderer.invoke('writeNote')
}
try {
// Docs: https://electronjs.org/docs/api/context-bridge
contextBridge.exposeInMainWorld('electron', {
sendPing,
getNote,
createNote,
deleteNote,
readNote,
writeNote
})
} catch (error) {
console.error(error)
}
// Renderer (Main World)
window.electron.writeNote()
Usage with TypeScript
If you're building your Electron app with TypeScript, you'll want to add types to your APIs exposed over the context bridge.
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
export interface IElectronAPI {
loadPreferences: () => Promise<void>,
}
// interface.d.ts
declare global {
interface Window {
electronAPI: IElectronAPI
}
}
// Renderer (Main World)
window.electronAPI.loadPreferences()
lodash の debounce や throttle
負荷対策(mouse move イベント等マウスの座標が動くたびにサーバーに問い合わせするのではなく、少し待ってから同期や問い合わせが走るように実装する)
- debounce: 特定の時間間隔が過ぎるまで、イベントが発生し続ける場合にはそのイベントを無視し、最後のイベントだけを処理
import { debounce } from 'lodash';
const handleMouseMove = debounce((event) => {
// サーバーに問い合わせ
}, 300);
window.addEventListener('mousemove', handleMouseMove);
- throttleは、一定時間に1度しか実行されないようにするためのもの
import { throttle } from 'lodash';
const handleMouseMove = throttle((event) => {
// サーバーに問い合わせ
}, 300);
window.addEventListener('mousemove', handleMouseMove);
その他
- requestAnimationFrame:ブラウザのリペイントタイミングに合わせてイベントを処理する。
- Custom Timeout Handling:自身でカスタムする
let timeoutId = null;
const handleMouseMove = (event) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
// サーバーに問い合わせ
}, 300);
};
window.addEventListener('mousemove', handleMouseMove);
- Observable Libraries (RxJS)
RxJSのライブラリを使用する。
import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
const mouseMove$ = fromEvent(window, 'mousemove')
.pipe(debounceTime(300))
.subscribe(event => {
// サーバーに問い合わせ
});
RxJSとPromiseの違い
useRef
useRef は、レンダー時には不要な値を参照するための React フック
DOM にアクセスする手段として理解しているかもしれません。<div ref={myRef} /> のようにして React に ref オブジェクトを渡した場合、React は DOM ノードに変更があるたびに .current プロパティをその DOM ノードに設定
useStateを利用している場合はstateの変更される度にコンポーネントの再レンダリングが発生しますが、useRefは値が変更になっても、コンポーネントの再レンダリングは発生しない。
Error関連
chunk-BZFZRUQ2.js?v=3d6a074e:521 Warning: Cannot update a component (`MarkdownEditor`) while rendering a different component (`NoteList`). To locate the bad setState() call inside `NoteList`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
コンポーネントから親の状態をレンダー時に変えたらNG。Reactコンポーネントは、他のコンポーネントがレンダー中は副作用、つまりその状態を変えることは許されない。 → 意図しない状態変更から生じるアプリケーションのバグが発生しやすくなるため。
Uncaught Error: Too many re-renders. React limits the number of renders to
prevent an infinite loop.
→ Reactが無限ループ状態となっている。
関数の中に入れて渡すとクリックした時のみ実行される。
/*以下の通り処理を修正する*/
onClick={ handleNoteSelect(i)} → onClick={() => handleNoteSelect(i)}
call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
→ フックはメソッド内の一番外側でしか、呼び出すことができない。フックを使用する場合は、コンポーネントを読み込んだ時点でどのフックが使われるかを明示しなければいけないので、メソッドを実行して途中でフックの数が変わるといった挙動をしてしまうとエラーになる。
export const DeleteNoteButton = ({ ...props }: ActionButtonProps) => {
const deleteNote = useSetAtom(deleteNoteAtom) ← topに移動
const selectedNote = useAtomValue(selectedNoteAtom) ← topに移動
const handleDelete = async () => {
if (!selectedNote) {
return
}
const isDeleted = await window.electron.deleteNote(selectedNote.title)
if (!isDeleted) {
return
}
deleteNote()
}
Error occurred in handler for 'getNote': Error: No handler registered for 'getNote'
at WebContents.<anonymous> (node:electron/js2c/browser_init:2:78073)
at WebContents.emit (node:events:517:28)
getNote
関数をElectronが上手く認識してくれていない模様
ipcMain.handle
を登録してmain.tsにimportしておく必要がある。
ipcMain.handle(channel, listener)
invoke 可能な IPC のハンドラを追加。このハンドラは、レンダラが ipcRenderer.invoke(channel, ...args) を呼び出したとき常に呼び出される。
ipcMain.handle('my-invokable-ipc', async (event, ...args) => {
const result = await somePromise(...args)
return result
})
async () => {
const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2)
// ...
}
メインプロセスに登録したところ以下のエラーが発生
x Build failed in 110ms
ERROR [vite]: Rollup failed to resolve import "@main/utils/handler" from "/Users/takahashi_masaki/Desktop/doon/src/main/ipc/useNotes.ts".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`
viteの名前解決ができていないようなのでvite.configの名前解決の場所を確認する。
開発用拡張機能の読み込み (DevTools Extension, session)について
マイグレーションの実行
マイグレーションファイルについて
マイグレーションとは、データベースのスキーマを新旧のバージョン間で移行させるプロセスのこと。
→ 「DB の変更内容をファイルに記録し、その内容を実行して DB のスキーマを更新していく手法」のこと。
マイグレーションファイルは、データベースのスキーマ変更を管理するためのスクリプトのこと。
TypeORM CLIを使用して、マイグレーションファイルが作成されていないように思われるので、状況を確認する。
ts-nodeをインストールする
npm install ts-node --save-dev
コマンドを追加
"scripts": {
...
"typeorm": "typeorm-ts-node-commonjs"
}
npm run typeorm migration:show
typeorm migration:show
Show all migrations and whether they have been run or not
オプション:
-h, --help ヘルプを表示 [真偽]
-d, --dataSource Path to the file where your DataSource instance is defined.
[必須]
-v, --version バージョンを表示 [真偽]
必須の引数が見つかりません: dataSource
TypeORMのバージョン ^0.3.x からは、CLIコマンドを実行する際に DataSourceを明示的に指定する必要がある。
npx typeorm migration:show -d src/main/database/typeorm.ts
Error during migration show:
Error: Unable to open file:
DataSourceの定義ファイルを指定するが、以前エラーが出力される。TypeScriptのモジュールシステムがNode.jsで直接サポートされていないため発生している?
npxに変更してみる。Cannot use import statement outside a module Error
が発生。
npx typeorm migration:show -d src/main/database/typeorm.ts
Error during migration show:
Error: Unable to open file: "/Users/takahashi_masaki/Desktop/doon/src/main/database/typeorm.ts". Cannot use import statement outside a module
at Function.loadDataSource (/Users/takahashi_masaki/Desktop/doon/node_modules/typeorm/commands/CommandUtils.js:22:19)
at async Object.handler (/Users/takahashi_masaki/Desktop/doon/node_modules/typeorm/commands/MigrationShowCommand.js:27:26)
公式ドキュメントを再度参照
npm run typeorm migration:run -- -d path-to-datasource-config
別のエラーに遭遇
Error: Unable to open file: "/Users/takahashi_masaki/Desktop/doon/src/main/database/typeorm.ts". ⨯ Unable to compile TypeScript:
src/main/database/typeorm.ts:1:8 - error TS1259: Module '"path"' can only be default-imported using the 'esModuleInterop' flag
1 import path from 'path'
~~~~
node_modules/@types/node/path.d.ts:178:5
178 export = path;
~~~~~~~~~~~~~~
This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.
src/main/database/typeorm.ts:3:31 - error TS2307: Cannot find module '@main/database/model/noteInfo' or its corresponding type declarations.
3 import { NoteInfoModel } from '@main/database/model/noteInfo'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main/database/typeorm.ts:4:28 - error TS2307: Cannot find module '@main/utils/index' or its corresponding type declarations.
4 import { getHomeDir } from '@main/utils/index'
~~~~~~~~~~~~~~~~~~~
at Function.loadDataSource (/Users/takahashi_masaki/Desktop/doon/node_modules/src/commands/CommandUtils.ts:22:19)
tsconfig.jsonに以下のFlagを追加する。
"compilerOptions": {
"esModuleInterop": true,
},
TypeScriptののCommonJSなモジュールの実行のエラーは "esModuleInterop": true
で解消可能。
また@main/database/model/noteInfo や @main/utils/index)が解決されない。tsconfig.jsonにpath設定を追加する。
"paths": {
"@renderer/*": [
"src/renderer/src/*"
],
"@main/*": [
"src/main/*"
]
},
},
また、tsconfig-pathsをインストールする。
npm i tsconfig-paths
tsconfig.jsonにさらに設定を追加する。
"ts-node": {
"require": ["tsconfig-paths/register"]
},
Error during migration run:
Error: Unable to open file: "/Users/takahashi_masaki/Desktop/doon/src/main/database/typeorm.ts". ⨯ Unable to compile TypeScript:
src/main/database/model/noteInfo.ts:4:2 - error TS1238: Unable to resolve signature of class decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 1.
4 @Entity('NoteInfoModel')
~~~~~~~~~~~~~~~~~~~~~~~
src/main/database/model/noteInfo.ts:6:4 - error TS1240: Unable to resolve signature of property decorator when called as an expression.
Argument of type 'ClassFieldDecoratorContext<NoteInfoModel, string> & { name: "uuid"; private: false; static: false; }' is not assignable to parameter of type 'string | symbol'.
6 @PrimaryGeneratedColumn('uuid')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main/database/model/noteInfo.ts:9:4 - error TS1240: Unable to resolve signature of property decorator when called as an expression.
Argument of type 'ClassFieldDecoratorContext<NoteInfoModel, string> & { name: "title"; private: false; static: false; }' is not assignable to parameter of type 'string | symbol'.
9 @Column('text', { nullable: false })
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main/database/model/noteInfo.ts:12:4 - error TS1240: Unable to resolve signature of property decorator when called as an expression.
Argument of type 'ClassFieldDecoratorContext<NoteInfoModel, string> & { name: "content"; private: false; static: false; }' is not assignable to parameter of type 'string | symbol'.
12 @Column('text', { nullable: true })
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main/database/model/noteInfo.ts:15:4 - error TS1240: Unable to resolve signature of property decorator when called as an expression.
Argument of type 'ClassFieldDecoratorContext<NoteInfoModel, Date> & { name: "lastEditTime"; private: false; static: false; }' is not assignable to parameter of type 'string | symbol'.
15 @Column('datetime')
~~~~~~~~~~~~~~~~~~
experimentalDecorators
を追加すればOK
"experimentalDecorators": true,
Typeorm立ち上げ時に遭遇したError
EntityMetadataNotFoundError: No metadata for "NoteInfoModel" was found.
TypeORMがNoteInfoModelエンティティのメタデータを見つけられないときに発生する。
import { DataSource } from "typeorm";
import { NoteInfoModel } from "./entity/NoteInfoModel";
export const AppDataSource = new DataSource({
type: "sqlite",
database: "database.sqlite",
synchronize: true,
logging: true,
entities: [NoteInfoModel], // エンティティを登録
migrations: [],
subscribers: [],
});
エンティティを直接登録した場合は上記エラーが発生しない為、パスに問題がある模様。
entities: [__dirname + "/entity/**/*.js"],
tscをしよしてコンパイルすると、srcディレクトリ内のファイルがoutディレクトリなどに移動する。その為、実行時にTypeORMがエンティティを探す際に、srcディレクトリではなくoutディレクトリ内のファイルを参照することになる。
outディレクトリ:TypeScriptをコンパイルした後に生成されるJavaScriptファイルを配置するための出力ディレクトリ
tsconfig.jsonで指定されたoutDirを考慮する(tsconfig.jsonファイルでoutDirオプションを指定することで、コンパイルされたファイルがどのディレクトリに出力されるかを指定できる)。
また、TypeScriptからJavaScriptにコンパイルされた後、ファイルの拡張子が.tsから.jsに変わります。そのため、TypeORMがエンティティを探す際には、*.jsを指定する。
コンパイル後、__dirnameはoutディレクトリを指す。 → path.joinを使用してパスを適切に組み立てることが推奨
entities: [path.join(__dirname, '/database/model/**/*.js')]
error: Error: SQLITE_ERROR: no such table: NoteInfoModel
NoteInfoModel Tableを見つけることができないエラー
synchronize: true,
synchronizeをfalseにした場合、マイグレーションが適切に適用されていないとテーブルが作成されない。
Jotai
ユーティリティunwrapは、 のような非同期アトムを同期アトムに変換する。
※ 読み取り専用の関数となる。
import { atom } from 'jotai'
import { unwrap } from 'jotai/utils'
const countAtom = atom(0)
const delayedCountAtom = atom(async (get) => {
await new Promise((r) => setTimeout(r, 500))
return get(countAtom)
})
const unwrapped1Atom = unwrap(delayedCountAtom)
// The value is `undefined` while pending
const unwrapped2Atom = unwrap(delayedCountAtom, (prev) => prev ?? 0)