Closed25

Doon - Markdown Notes

まさきちまさきち

状態管理 (Jotai) とデータベース (TypeORM) の使い分け

UI状態: ユーザーインターフェースの現在の状態(例: モーダルの開閉、タブの選択状態)、ログイン状態や一時的なエラーメッセージなど、セッションを通じて持続するが、長期間の永続化を必要としない情報を格納する。

永続的なデータ管理: アプリケーションが再起動しても保持されるべきデータを管理する。

まさきちまさきち

tsconfig.jsonを理解する

https://qiita.com/ryokkkke/items/390647a7c26933940470


tsconfig.jsonのオプションについて

https://typescriptbook.jp/reference/tsconfig


オプションについての説明

https://zenn.dev/chida/articles/bdbcd59c90e2e1


TSConfig Basesの@tsconfig/node16が便利

https://www.npmjs.com/package/@tsconfig/node16

{
  "$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.jsontsconfig.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

https://electron-vite.org/guide/introduction



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の使い所
例えば、ヘッダー、フッターの左右と上下の位置は、ページにぴったり配置したいが、親の余白があるために子の配置がうまくいかない場合など。
https://zenn.dev/smartshopping/articles/658ac0e7c064b1


mixinのtips

https://zenn.dev/tak_dcxi/articles/2cc1828e9c1fe2


cssのテクニック

https://www.tak-dcxi.com/article/that-css-technique-you-learned-is-outdated/


CSS入れ子チートシート

https://yoshikawaweb.com/element/

まさきちまさきち

React.ComponentPropsについて

https://zenn.dev/takepepe/articles/atoms-type-definitions

https://zenn.dev/tm35/articles/4bc94d7e6cb314

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を許容しているかを確認できる。

https://kk-web.link/blog/20201023

まさきちまさきち

Component設計



Atomic Designについてあらためて評価する

https://zenn.dev/mutex_inc/articles/beca85dd7fdcae

Atomic Designの場合、コンポーネントの粒度を決定する基準が曖昧になることがある。例えば、所望のコンポーネントをOrganismsに作ったらいいのか、Moleculesに作ったらいいのか迷う場面がある。

Hidden comment
まさきちまさきち

状態管理ライブラリ

https://zenn.dev/kazukix/articles/react-state-management-libraries

https://blog.uhy.ooo/entry/2021-07-24/react-state-management/

▪️ 候補

  • 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の歴史

https://ics.media/entry/200310/



Jotai

https://jotai.org/

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().

まさきちまさきち

MDX Editor

マークダウンの生成にMDX Editorを使用する。
https://mdxeditor.dev/editor/docs/getting-started


ToolbarCode 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を検討する。

https://blog.stin.ink/articles/replace-react-markdown-with-remark

  • react-markdown
  • SimpleMDE (react-simplemde-editor)
  • Draft.js
  • ProseMirror

react-markdownを採用する。
ReactMarkdown 自体は Markdown テキストを解析して表示するためのコンポーネントである為、MDX Editorとは異なり、テキスト入力機能は備えていない。

https://github.com/remarkjs/react-markdown?tab=readme-ov-file

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)が厳格に設定されているため、インラインスクリプトや外部のリソースがブロックされてしまう。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/style-src

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

https://www.electronjs.org/ja/docs/latest/api/frameless-window

まさきちまさきち

ElectronでReact Developer Toolsを使用する場合

https://zenn.dev/todesking/articles/8dc19abc153098


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.

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/script-src-elem

script-src-elemはscriptタグで有効なソースを示す。


公式ドキュメントで以下のdevtoolsも紹介されている。

https://react.dev/learn/react-developer-tools

https://github.com/MarshallOfSound/electron-devtools-installer


devtoolsが表示されない。
ログで"ReferenceError: dragEvent is not defined"のエラーが発生している。
Chrome のアップストリームの問題である模様。

まさきちまさきち

dialogについて

ファイルを開いたり、保存したり、アラートを出したりするために、ネイティブのシステムダイアログを表示する。

https://www.electronjs.org/ja/docs/latest/api/dialog

https://www.electronjs.org/ja/docs/latest/api/frameless-window


fs-extra

標準装備のfsモジュールの場合、階層構造となったフォルダを一気にコピーできない。

https://www.npmjs.com/package/fs-extra


electron-log

https://www.npmjs.com/package/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.

https://www.electronjs.org/docs/latest/tutorial/context-isolation/#usage-with-typescript

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

https://qiita.com/waterada/items/986660d31bc107dbd91c


その他

  • 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のライブラリを使用する。

https://rxjs.dev/guide/overview

import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

const mouseMove$ = fromEvent(window, 'mousemove')
  .pipe(debounceTime(300))
  .subscribe(event => {
    // サーバーに問い合わせ
  });

RxJSとPromiseの違い

https://memo.open-code.club/RxJS/はじめに/Promiseとの比較.html

まさきちまさきち

useRef

useRef は、レンダー時には不要な値を参照するための React フック
DOM にアクセスする手段として理解しているかもしれません。<div ref={myRef} /> のようにして React に ref オブジェクトを渡した場合、React は DOM ノードに変更があるたびに .current プロパティをその DOM ノードに設定

useStateを利用している場合はstateの変更される度にコンポーネントの再レンダリングが発生しますが、useRefは値が変更になっても、コンポーネントの再レンダリングは発生しない。

https://zenn.dev/dove/articles/e2d962e9d69e20

https://qiita.com/seira/items/0e6a2d835f1afb50544d

まさきちまさきち

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コンポーネントは、他のコンポーネントがレンダー中は副作用、つまりその状態を変えることは許されない。 → 意図しない状態変更から生じるアプリケーションのバグが発生しやすくなるため。

https://qiita.com/FumioNonaka/items/3fe39911e3f2479128e8#レンダーが済んでから親の状態を変える



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

→ フックはメソッド内の一番外側でしか、呼び出すことができない。フックを使用する場合は、コンポーネントを読み込んだ時点でどのフックが使われるかを明示しなければいけないので、メソッドを実行して途中でフックの数が変わるといった挙動をしてしまうとエラーになる。

https://qiita.com/tatsumin0206/items/4e1076e2deedf20a9485

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しておく必要がある。

https://www.electronjs.org/ja/docs/latest/api/ipc-main

ipcMain.handle(channel, listener)

invoke 可能な IPC のハンドラを追加。このハンドラは、レンダラが ipcRenderer.invoke(channel, ...args) を呼び出したとき常に呼び出される。

Main Process
ipcMain.handle('my-invokable-ipc', async (event, ...args) => {
  const result = await somePromise(...args)
  return result
})
Renderer Process
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の名前解決の場所を確認する。

まさきちまさきち

マイグレーションの実行

マイグレーションファイルについて

マイグレーションとは、データベースのスキーマを新旧のバージョン間で移行させるプロセスのこと。
→ 「DB の変更内容をファイルに記録し、その内容を実行して DB のスキーマを更新していく手法」のこと。
マイグレーションファイルは、データベースのスキーマ変更を管理するためのスクリプトのこと。

https://qiita.com/to3izo/items/7b8d44021cb386de2ef7

TypeORM CLIを使用して、マイグレーションファイルが作成されていないように思われるので、状況を確認する。

https://orkhan.gitbook.io/typeorm/docs/using-cli#installing-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)

https://github.com/TypeStrong/ts-node/issues/1096

tsconfig.jsonに以下のFlagを追加する。

tsconfig.json
  "compilerOptions": {
    "esModuleInterop": true,
  },

TypeScriptののCommonJSなモジュールの実行のエラーは "esModuleInterop": trueで解消可能。

https://www.typescriptlang.org/tsconfig/#esModuleInterop

また@main/database/model/noteInfo や @main/utils/index)が解決されない。tsconfig.jsonにpath設定を追加する。

tsconfig.json
    "paths": {
      "@renderer/*": [
        "src/renderer/src/*"
      ],
      "@main/*": [
        "src/main/*"
      ]
    },
  },


また、tsconfig-pathsをインストールする。

npm i tsconfig-paths

tsconfig.jsonにさらに設定を追加する。

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

tsconfig.json
"experimentalDecorators": true,
まさきちまさきち

Typeorm立ち上げ時に遭遇したError

EntityMetadataNotFoundError: No metadata for "NoteInfoModel" was found.

TypeORMがNoteInfoModelエンティティのメタデータを見つけられないときに発生する。

https://medium.com/@JorgeSantanaDeveloper/troubleshooting-the-no-metadata-was-found-error-in-typeorm-2fab1003b099

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を見つけることができないエラー

src/main/database/typeorm.ts
synchronize: true,

synchronizeをfalseにした場合、マイグレーションが適切に適用されていないとテーブルが作成されない。

https://qiita.com/kss_nm/items/d971eb2e64e7dcc94814



まさきちまさきち

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)
このスクラップは23日前にクローズされました