typed-msg 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 vrcalpha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # typed-msg
2
+
3
+ [![npm version](https://badge.fury.io/js/typed-msg.svg)](https://badge.fury.io/js/typed-msg)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ TypeScriptで書かれた、Chrome拡張機能のSW⇔Content Scripts間の通信を型安全にするラッパーライブラリです。
8
+
9
+ ## 例
10
+
11
+ この例では、ストレージ操作とタブ操作の2つのスコープを持つメッセージインタフェイスを定義します。
12
+
13
+ ### 1. メッセージインターフェース(型)定義
14
+
15
+ まず、送受信するメッセージの型を定義します。
16
+
17
+ ```ts
18
+ // types/messages.ts
19
+ import {
20
+ MessageDefinitions,
21
+ MergeMessageDefinitions,
22
+ MessageResponse,
23
+ } from 'typed-msg';
24
+
25
+ // ストレージ関連のメッセージ
26
+ type StorageMessages = MessageDefinitions<{
27
+ // 設定を保存
28
+ setSettings: {
29
+ req: { theme: 'light' | 'dark'; language: string };
30
+ res: MessageResponse;
31
+ };
32
+ // 設定を取得
33
+ getSettings: {
34
+ req: void;
35
+ res: MessageResponse<{ theme: 'light' | 'dark'; language: string }>;
36
+ };
37
+ }>;
38
+
39
+ // タブ関連のメッセージ
40
+ type TabMessages = MessageDefinitions<{
41
+ // 現在のタブ情報を取得
42
+ getCurrentTab: {
43
+ req: void;
44
+ res: MessageResponse<{ id: number; url: string; title: string }>;
45
+ };
46
+ // 新しいタブを開く
47
+ openTab: {
48
+ req: { url: string };
49
+ res: MessageResponse<{ tabId: number }>;
50
+ };
51
+ }>;
52
+
53
+ // すべてのスコープを統合
54
+ export type Messages = MergeMessageDefinitions<{
55
+ storage: StorageMessages;
56
+ tabs: TabMessages;
57
+ }>;
58
+ ```
59
+
60
+ ### 2. 受信側(Service Worker)
61
+
62
+ Service Worker側では `receive` を使ってスコープごとにハンドラーを登録します。
63
+
64
+ ハンドラーは、スコープごとにファイルを分けても動作します。
65
+
66
+ ```ts
67
+ // background.ts
68
+ import type { Messages } from './types/messages';
69
+ import { receive } from 'typed-msg';
70
+
71
+ // ストレージ関連のハンドラー
72
+ const storageReceiver = receive<Messages>('storage');
73
+
74
+ storageReceiver.on('setSettings', async (req) => {
75
+ await chrome.storage.local.set({ settings: req });
76
+ return { success: true };
77
+ });
78
+
79
+ storageReceiver.on('getSettings', async () => {
80
+ const data = await chrome.storage.local.get('settings');
81
+ return { success: true, message: data.settings };
82
+ });
83
+
84
+ // タブ関連のハンドラー
85
+ const tabsReceiver = receive<Messages>('tabs');
86
+
87
+ tabsReceiver.on('getCurrentTab', async (_, sender) => {
88
+ const tab = sender.tab;
89
+ if (!tab?.id || !tab.url || !tab.title) {
90
+ return { success: false, message: 'タブ情報を取得できませんでした' };
91
+ }
92
+ return { success: true, message: { id: tab.id, url: tab.url, title: tab.title } };
93
+ });
94
+
95
+ tabsReceiver.on('openTab', async (req) => {
96
+ const tab = await chrome.tabs.create({ url: req.url });
97
+ if (!tab.id) {
98
+ return { success: false, message: 'タブを開けませんでした' };
99
+ }
100
+ return { success: true, message: { tabId: tab.id } };
101
+ });
102
+ ```
103
+
104
+ ### 3. 送信側(Content Scripts)
105
+
106
+ Content Scripts側では `connect` を使ってメッセージを送信します。
107
+ スコープごとに別々の送信インターフェースを作成できます。
108
+
109
+ ```ts
110
+ // content.ts (Content Script)
111
+ import type { Messages } from './types/messages';
112
+ import { connect } from 'typed-msg';
113
+
114
+ // スコープごとに送信インターフェースを作成
115
+ const storage = connect<Messages>('storage');
116
+ const tabs = connect<Messages>('tabs');
117
+
118
+ // 設定を保存
119
+ const saveResult = await storage.setSettings({ theme: 'dark', language: 'ja' });
120
+ if (saveResult.success) {
121
+ console.log('設定を保存しました');
122
+ }
123
+
124
+ // 設定を取得
125
+ const settingsResult = await storage.getSettings();
126
+ if (settingsResult.success) {
127
+ console.log('テーマ:', settingsResult.message.theme);
128
+ console.log('言語:', settingsResult.message.language);
129
+ }
130
+
131
+ // 現在のタブ情報を取得
132
+ const tabResult = await tabs.getCurrentTab();
133
+ if (tabResult.success) {
134
+ console.log('タブID:', tabResult.message.id);
135
+ console.log('URL:', tabResult.message.url);
136
+ }
137
+
138
+ // 新しいタブを開く
139
+ const openResult = await tabs.openTab({ url: 'https://example.com' });
140
+ if (openResult.success) {
141
+ console.log('新しいタブを開きました:', openResult.message.tabId);
142
+ }
143
+ ```
144
+
145
+ ## インストール
146
+
147
+ ### npm
148
+
149
+ ```bash
150
+ npm install typed-msg
151
+ ```
152
+
153
+ ### yarn
154
+
155
+ ```bash
156
+ yarn add typed-msg
157
+ ```
158
+
159
+ ### pnpm
160
+
161
+ ```bash
162
+ pnpm add typed-msg
163
+ ```
164
+
165
+ ## 貢献
166
+
167
+ プロジェクトへの貢献を歓迎します!以下のルールに従うと,あなたの貢献がスムーズになります!
168
+
169
+ ### Issue / PR
170
+
171
+ Issueを立てる際は,バグ報告・機能要望のどちらかを明記してください。
172
+ PRの説明には,目的・変更点・影響範囲・サンプルコードがあるとありがたいです。
173
+
174
+ ## ライセンス
175
+
176
+ MIT License
177
+
178
+ 詳細は[LICENSE](./LICENSE)ファイルを参照してください。
179
+
180
+ ## 変更履歴・リリース情報
181
+
182
+ ### v1.0.0 (2026-02-08)
183
+
184
+ - 初回リリース
package/dist/main.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ declare type AsyncOrSync<T> = T | Promise<T>;
2
+
3
+ /**
4
+ * メッセージリスナーとの通信を行うための送信用インターフェースを作成します。
5
+ *
6
+ * @template T - メッセージ定義の型。`MergeMessageDefinitions` で作成した型を指定します。
7
+ * @param scope - メッセージのスコープ名。受信側(`receive`)と一致させる必要があります。
8
+ * @returns メッセージ送信用のProxyオブジェクト。定義したメッセージ名をメソッドとして呼び出せます。
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * type Messages = MergeMessageDefinitions<{ remote: RemoteMessages }>;
13
+ *
14
+ * const sender = connect<Messages>('remote');
15
+ * const result = await sender.addRepository({ url: 'https://...' });
16
+ * ```
17
+ */
18
+ export declare function connect<T extends MergedMessageDefinitions>(scope: keyof T & string): MessageInterface<T[keyof T & string]>;
19
+
20
+ /**
21
+ * 失敗時のレスポンス型です。
22
+ *
23
+ * エラーメッセージを `message` に格納して返します。
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const errorResponse: FailureMessageResponse = {
28
+ * success: false,
29
+ * message: 'リポジトリが見つかりません',
30
+ * };
31
+ * ```
32
+ */
33
+ export declare type FailureMessageResponse = {
34
+ success: false;
35
+ message: string;
36
+ };
37
+
38
+ declare type JsonArray = JsonValue[];
39
+
40
+ declare type JsonObject = {
41
+ [key: string]: JsonValue;
42
+ };
43
+
44
+ declare type JsonPrimitive = string | number | boolean | null;
45
+
46
+ declare type JsonValue = JsonPrimitive | JsonArray | JsonObject;
47
+
48
+ declare type MergedMessageDefinitions = Record<string, ValidatedMessageDefinitions>;
49
+
50
+ /**
51
+ * 複数のメッセージ定義をスコープごとに統合するための型エイリアスです。
52
+ *
53
+ * `MessageDefinitions` で作成した個別の定義を、スコープ名をキーとしてまとめます。
54
+ * `connect` と `receive` はこの型を型引数として受け取ります。
55
+ *
56
+ * @template T - スコープ名をキー、`MessageDefinitions` を値とするオブジェクト型
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * type RemoteMessages = MessageDefinitions<{ ... }>;
61
+ * type BackgroundMessages = MessageDefinitions<{ ... }>;
62
+ *
63
+ * type Messages = MergeMessageDefinitions<{
64
+ * remote: RemoteMessages;
65
+ * background: BackgroundMessages;
66
+ * }>;
67
+ *
68
+ * // 使用時:
69
+ * const sender = connect<Messages>('remote');
70
+ * const receiver = receive<Messages>('remote');
71
+ * ```
72
+ */
73
+ export declare type MergeMessageDefinitions<T extends MergedMessageDefinitions> = T;
74
+
75
+ /**
76
+ * 個別のメッセージ定義を作成するための型エイリアスです。
77
+ *
78
+ * 各メッセージは `req`(リクエスト)と `res`(レスポンス)を持つオブジェクトとして定義します。
79
+ * この型で定義したメッセージ群は、`MergeMessageDefinitions` で統合する必要があります。
80
+ *
81
+ * @template T - メッセージ名をキー、`{ req?, res? }` を値とするオブジェクト型
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * type RemoteMessages = MessageDefinitions<{
86
+ * addRepository: {
87
+ * req: { url: string };
88
+ * res: MessageResponse;
89
+ * };
90
+ * getRepository: {
91
+ * req: { id: number };
92
+ * res: MessageResponse<{ name: string; url: string }>;
93
+ * };
94
+ * }>;
95
+ * ```
96
+ */
97
+ export declare type MessageDefinitions<T extends ValidatedMessageDefinitions> = T;
98
+
99
+ declare type MessageHandler<T extends ValidatedMessageDefinitions, K extends string> = K extends keyof T ? (req: SafeExtract<T[K], 'req'>, sender: chrome.runtime.MessageSender) => AsyncOrSync<SafeExtract<T[K], 'res'>> : never;
100
+
101
+ declare type MessageInterface<T extends ValidatedMessageDefinitions> = {
102
+ [K in keyof T]: T[K] extends MessageShape ? (req: SafeExtract<T[K], 'req'>) => Promise<SafeExtract<T[K], 'res'>> : never;
103
+ };
104
+
105
+ /**
106
+ * メッセージハンドラーのレスポンス型で、成功か失敗かを表すユーティリティ型です。
107
+ *
108
+ * 成功時は `{ success: true }` または `{ success: true, message: T }` を、
109
+ * 失敗時は `{ success: false, message: string }` を返します。
110
+ *
111
+ * @template T - 成功時に返すデータの型。省略時は `void` です。
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * // データなしの成功レスポンス
116
+ * type SimpleRes = MessageResponse;
117
+ * // { success: true } | { success: false, message: string }
118
+ *
119
+ * // データありの成功レスポンス
120
+ * type DataRes = MessageResponse<User>;
121
+ * // { success: true, message: User } | { success: false, message: string }
122
+ * ```
123
+ */
124
+ export declare type MessageResponse<T = void> = SuccessMessageResponse<T> | FailureMessageResponse;
125
+
126
+ declare type MessageShape = {
127
+ req?: JsonValue;
128
+ res?: JsonValue;
129
+ };
130
+
131
+ /**
132
+ * メッセージを受信するためのレシーバーを作成します。
133
+ *
134
+ * @template T - メッセージ定義の型。`MergeMessageDefinitions` で作成した型を指定します。
135
+ * @param scope - 受信するメッセージのスコープ名。送信側(`connect`)と一致させる必要があります。
136
+ * @returns メッセージハンドラーを登録するための `Receiver` インスタンスです。
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * type Messages = MergeMessageDefinitions<{ remote: RemoteMessages }>;
141
+ *
142
+ * const receiver = receive<Messages>('remote');
143
+ * receiver.on('addRepository', (req, sender) => {
144
+ * console.log(req.url);
145
+ * return { success: true };
146
+ * });
147
+ * ```
148
+ */
149
+ export declare function receive<T extends MergedMessageDefinitions>(scope: keyof T & string): Receiver<T, keyof T & string>;
150
+
151
+ declare class Receiver<T extends MergedMessageDefinitions, K extends keyof T & string> {
152
+ private handlers;
153
+ constructor(scope: K);
154
+ /**
155
+ * 指定したメッセージ名に対するハンドラーを登録します。
156
+ *
157
+ * 同じスコープ、同じ名前で複数のハンドラーは登録できず、エラーを投げます。
158
+ *
159
+ * @param name - 処理するメッセージの名前。`MessageDefinitions` で定義した名前(キー)を指定します。
160
+ * @param handler - メッセージを処理するコールバック関数。引数は以下の通りです:
161
+ * - `req`: 送信側から送られたリクエストデータ
162
+ * - `sender`: `chrome.runtime.MessageSender` オブジェクト(タブ情報等)
163
+ * @returns 送信側に返すレスポンス。Promiseを返すと自動的に解決されます。
164
+ * @throws 同じスコープ、同じ名前のハンドラーが既に登録されている場合
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const receiver = receive<Message>("remote");
169
+ *
170
+ * receiver.on('addRepository', (req, sender) => {
171
+ * console.log(req.url);
172
+ * console.log(sender.tab?.id);
173
+ * return { success: true };
174
+ * });
175
+ *
176
+ * // 非同期のハンドラーも指定可能
177
+ * receiver.on('fetchData', async (req) => {
178
+ * const data = await someAsyncOperation(req);
179
+ * return { success: true, message: data };
180
+ * });
181
+ * ```
182
+ */
183
+ on<V extends keyof T[K] & string>(name: V, handler: MessageHandler<T[K], V>): void;
184
+ private _typed;
185
+ }
186
+
187
+ declare type SafeExtract<T, K> = K extends keyof T ? T[K] : void;
188
+
189
+ /**
190
+ * 成功時のレスポンス型です。
191
+ *
192
+ * - `T` が `void` の場合: `{ success: true }`
193
+ * - `T` が `void` 以外の場合: `{ success: true, message: T }`
194
+ *
195
+ * @template T - 成功時に返すデータの型。省略時は `void` です。
196
+ */
197
+ export declare type SuccessMessageResponse<T = void> = T extends void ? {
198
+ success: true;
199
+ } : {
200
+ success: true;
201
+ message: T;
202
+ };
203
+
204
+ declare type ValidatedMessageDefinitions = Record<string, MessageShape>;
205
+
206
+ export { }
package/dist/main.js ADDED
@@ -0,0 +1,72 @@
1
+ class c {
2
+ handlers = {};
3
+ constructor(r) {
4
+ chrome.runtime.onMessage.addListener((e, o, i) => {
5
+ if (this._typed(e) && e.scope === r)
6
+ return (async () => {
7
+ const t = this.handlers[e.name];
8
+ if (!t)
9
+ throw new Error(
10
+ `scope: "${r}", name: "${e.name}" のハンドラーが未定義です。`
11
+ );
12
+ const s = await Promise.resolve(t(e.req, o));
13
+ i(s);
14
+ })(), !0;
15
+ });
16
+ }
17
+ /**
18
+ * 指定したメッセージ名に対するハンドラーを登録します。
19
+ *
20
+ * 同じスコープ、同じ名前で複数のハンドラーは登録できず、エラーを投げます。
21
+ *
22
+ * @param name - 処理するメッセージの名前。`MessageDefinitions` で定義した名前(キー)を指定します。
23
+ * @param handler - メッセージを処理するコールバック関数。引数は以下の通りです:
24
+ * - `req`: 送信側から送られたリクエストデータ
25
+ * - `sender`: `chrome.runtime.MessageSender` オブジェクト(タブ情報等)
26
+ * @returns 送信側に返すレスポンス。Promiseを返すと自動的に解決されます。
27
+ * @throws 同じスコープ、同じ名前のハンドラーが既に登録されている場合
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const receiver = receive<Message>("remote");
32
+ *
33
+ * receiver.on('addRepository', (req, sender) => {
34
+ * console.log(req.url);
35
+ * console.log(sender.tab?.id);
36
+ * return { success: true };
37
+ * });
38
+ *
39
+ * // 非同期のハンドラーも指定可能
40
+ * receiver.on('fetchData', async (req) => {
41
+ * const data = await someAsyncOperation(req);
42
+ * return { success: true, message: data };
43
+ * });
44
+ * ```
45
+ */
46
+ on(r, e) {
47
+ if (r in this.handlers)
48
+ throw new Error("同じ名前のメッセージリスナーは複数登録できません");
49
+ this.handlers[r] = e;
50
+ }
51
+ _typed(r) {
52
+ return typeof r?.scope == "string" && typeof r?.name == "string";
53
+ }
54
+ }
55
+ function h(n) {
56
+ return new Proxy({}, {
57
+ get(r, e) {
58
+ return (o) => new Promise(
59
+ (i, t) => chrome.runtime.sendMessage({ scope: n, name: e, req: o }, (s) => {
60
+ chrome.runtime.lastError ? t(chrome.runtime.lastError) : i(s);
61
+ })
62
+ );
63
+ }
64
+ });
65
+ }
66
+ function u(n) {
67
+ return new c(n);
68
+ }
69
+ export {
70
+ h as connect,
71
+ u as receive
72
+ };
@@ -0,0 +1 @@
1
+ (function(r,t){typeof exports=="object"&&typeof module<"u"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(r=typeof globalThis<"u"?globalThis:r||self,t(r.main={}))})(this,(function(r){"use strict";class t{handlers={};constructor(e){chrome.runtime.onMessage.addListener((n,s,c)=>{if(this._typed(n)&&n.scope===e)return(async()=>{const o=this.handlers[n.name];if(!o)throw new Error(`scope: "${e}", name: "${n.name}" のハンドラーが未定義です。`);const u=await Promise.resolve(o(n.req,s));c(u)})(),!0})}on(e,n){if(e in this.handlers)throw new Error("同じ名前のメッセージリスナーは複数登録できません");this.handlers[e]=n}_typed(e){return typeof e?.scope=="string"&&typeof e?.name=="string"}}function d(i){return new Proxy({},{get(e,n){return s=>new Promise((c,o)=>chrome.runtime.sendMessage({scope:i,name:n,req:s},u=>{chrome.runtime.lastError?o(chrome.runtime.lastError):c(u)}))}})}function f(i){return new t(i)}r.connect=d,r.receive=f,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "typed-msg",
3
+ "version": "1.0.0",
4
+ "description": "Chrome拡張機能のSW⇔Content Scripts間の通信を型安全にするライブラリ",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "main": "dist/main.js",
9
+ "types": "dist/main.d.ts",
10
+ "scripts": {
11
+ "dev": "vite",
12
+ "build": "tsc && vite build",
13
+ "format": "prettier --write \"src/**/*.ts\" --log-level warn",
14
+ "docs": "npx typedoc"
15
+ },
16
+ "author": "vrcalphabet",
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "devDependencies": {
20
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
21
+ "@types/chrome": "^0.1.36",
22
+ "@types/node": "^25.2.1",
23
+ "typedoc": "^0.28.16",
24
+ "typescript": "^5.8.2",
25
+ "vite": "^7.3.1",
26
+ "vite-plugin-dts": "^4.5.4"
27
+ }
28
+ }