@pixpilot/chrome-lifecycle 0.10.0 → 0.11.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/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- const e=new Map;let t=!1;function n(){t||(t=!0,chrome.windows.onRemoved.addListener(t=>{let n=e.get(t);n&&(n.forEach(e=>{e()}),e.delete(t))}))}function r(t,r){n();let i=e.get(t);return i||(i=new Set,e.set(t,i)),i.add(r),()=>{let n=e.get(t);n&&(n.delete(r),n.size===0&&e.delete(t))}}const i=250,a=5e3,o=2;let s=!1;function c(e={}){if(s)return console.info(`Side panel state tracker already initialized`),()=>{};s=!0;let{trackDocumentVisibility:t=!0}=e,n,r,i=0,a,o=!1;function c(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(o)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let s=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(o||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**i,5e3);i+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:s,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!o){c();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,i=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(o=!0,c(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(a=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,a)),f(`document-load`)}),()=>{o=!0,c(),a&&document.removeEventListener(`visibilitychange`,a);let e=n;n=void 0,e?.disconnect(),s=!1}}const l=new Map,u=new Map,d=new Set;let f=!1;function p(){f||(f=!0,chrome.action.onClicked.addListener(e=>{let t=l.get(e.windowId);t&&t.state===`visible`?t.port&&(l.delete(e.windowId),t.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:e.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(e=>{e.name===chrome.runtime.id&&(e.onMessage.addListener(t=>{t.type===`side-panel-state-tracker`&&t.state&&g({port:e,state:t.state,reason:t.reason??`unknown`,windowId:t.windowId,type:t.type})}),e.onDisconnect.addListener(e=>{Array.from(l.entries()).forEach(([t,n])=>{n.port&&n.port===e&&g({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(e=>{l.delete(e),u.delete(e)}))}function m(){f||=(p(),!0)}function h(e,t){if(e.type!==`side-panel-state-tracker`)return;let n={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t},r=e.state===t;d.forEach(({listener:e,includeRepeats:t})=>{if(!(r&&!t))try{e(n)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function g(e){let{windowId:t,state:n}=e,r=u.get(t);if(e.type===`side-panel-state-tracker`&&u.set(t,n),n===`hidden`){l.delete(t),h(e,r);return}l.set(t,e),h(e,r)}function _(e){return m(),l.get(e)?.state}function v(e){return m(),_(e)===`visible`}function y(e,t={}){m();let n={listener:e,includeRepeats:t.includeRepeats===!0};return d.add(n),()=>{d.delete(n)}}function b(e){return y(t=>{t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function x(e){return y(t=>{t.state===`hidden`&&t.previousState===`visible`&&e(t)})}exports.getSidePanelStateForWindow=_,exports.initSidePanelStateManager=p,exports.initializeSidePanelStateTracker=c,exports.isWindowSidePanelVisible=v,exports.onSidePanelHidden=x,exports.onSidePanelShown=b,exports.onSidePanelStateChange=y,exports.onWindowClose=r;
1
+ const e=new Map,t=new Map,n=new Set;let r=!1;function i(){r||(r=!0,chrome.action.onClicked.addListener(t=>{let n=e.get(t.windowId);n&&n.state===`visible`?n.port&&(e.delete(t.windowId),n.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:t.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(t=>{t.name===chrome.runtime.id&&(t.onMessage.addListener(e=>{e.type===`side-panel-state-tracker`&&e.state&&s({port:t,state:e.state,reason:e.reason??`unknown`,windowId:e.windowId,type:e.type})}),t.onDisconnect.addListener(t=>{Array.from(e.entries()).forEach(([e,n])=>{n.port&&n.port===t&&s({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:e,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(n=>{e.delete(n),t.delete(n)}))}function a(){r||=(i(),!0)}function o(e,t){if(e.type!==`side-panel-state-tracker`)return;let r={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t},i=e.state===t;n.forEach(({listener:e,includeRepeats:t})=>{if(!(i&&!t))try{e(r)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function s(n){let{windowId:r,state:i}=n,a=t.get(r);if(n.type===`side-panel-state-tracker`&&t.set(r,i),i===`hidden`){e.delete(r),o(n,a);return}e.set(r,n),o(n,a)}function c(t){return a(),e.get(t)?.state}function l(e){return a(),c(e)===`visible`}function u(e,t={}){a();let r={listener:e,includeRepeats:t.includeRepeats===!0};return n.add(r),()=>{n.delete(r)}}function d(e){return u(t=>{t.reason!==`reconnected`&&t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function f(e){return u(t=>{t.reason!==`reconnected`&&t.state===`hidden`&&t.previousState===`visible`&&e(t)})}const p=500;function m(e){let{defaultView:t,debounceMs:n=500}=e,r=new Set,i=t,a,o,s=!1,c;function u(){c&&=(clearTimeout(c),void 0)}async function f(e){await Promise.all([...r].map(async t=>{try{await t(e)}catch(e){console.error(`[browser-change] A change handler failed`,e)}}))}async function p(e,t,n,r={}){if(!s)try{let s=await chrome.tabs.get(e);if(!s.active||n===`page`&&o===e&&a===t)return;let c=o??null;n===`page`&&(o=e,a=t),await f({reason:n,tabId:e,url:t,windowId:s.windowId,tab:s,view:i,previousTabId:c,isPanelReload:r.isPanelReload===!0})}catch(e){console.error(`[browser-change] Failed to read the tab that changed`,e)}}async function m(e,t={}){if(s)return;let r=await chrome.tabs.get(e);if(!r.active||!l(r.windowId))return;let{url:i}=r;if(i!=null){if(u(),t.immediate===!0){await p(e,i,`page`,t);return}c=setTimeout(()=>{c=void 0,p(e,i,`page`,t).catch(console.error)},n)}}function h(e,t={}){chrome.tabs.query({windowId:e,active:!0}).then(async e=>{let n=e[0];n?.id!=null&&await m(n.id,{...t,immediate:!0})}).catch(console.error)}function g(){chrome.tabs.query({active:!0,lastFocusedWindow:!0}).then(async e=>{let t=e[0];t?.id==null||t.url==null||l(t.windowId)&&await p(t.id,t.url,`view`)}).catch(console.error)}function _(e){m(e).catch(console.error)}function v(e){m(e.tabId).catch(()=>{})}chrome.tabs.onUpdated.addListener(_),chrome.tabs.onActivated.addListener(v);let y=d(({reason:e,windowId:n})=>{s||(e===`document-load`&&(i=t),a=void 0,o=void 0,h(n,{isPanelReload:e===`document-load`}))});return{onBrowserChange(e){return r.add(e),()=>{r.delete(e)}},setView(e){s||i===e||(i=e,g())},dispose(){s=!0,u(),chrome.tabs.onUpdated.removeListener(_),chrome.tabs.onActivated.removeListener(v),y(),r.clear()}}}function h(){let e=new Map;return{isDisplaying(t){if(t.isPanelReload)return!1;let n=e.get(t.windowId);return n?.tabId===t.tabId&&n.url===t.url},record(t){e.set(t.windowId,{tabId:t.tabId,url:t.url})},forget(t){e.delete(t)}}}const g=new Map;let _=!1;function v(){_||(_=!0,chrome.windows.onRemoved.addListener(e=>{let t=g.get(e);t&&(t.forEach(e=>{e()}),g.delete(e))}))}function y(e,t){v();let n=g.get(e);return n||(n=new Set,g.set(e,n)),n.add(t),()=>{let n=g.get(e);n&&(n.delete(t),n.size===0&&g.delete(e))}}const b=250,x=5e3,S=2;let C=!1;function w(e={}){if(C)return console.info(`Side panel state tracker already initialized`),()=>{};C=!0;let{trackDocumentVisibility:t=!0}=e,n,r,i=0,a,o=!1;function s(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(o)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let c=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(o||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**i,5e3);i+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:c,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!o){s();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,i=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(o=!0,s(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(a=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,a)),f(`document-load`)}),()=>{o=!0,s(),a&&document.removeEventListener(`visibilitychange`,a);let e=n;n=void 0,e?.disconnect(),C=!1}}exports.createBrowserChangeWatcher=m,exports.createDisplayedPageTracker=h,exports.getSidePanelStateForWindow=c,exports.initSidePanelStateManager=i,exports.initializeSidePanelStateTracker=w,exports.isWindowSidePanelVisible=l,exports.onSidePanelHidden=f,exports.onSidePanelShown=d,exports.onSidePanelStateChange=u,exports.onWindowClose=y;
package/dist/index.d.cts CHANGED
@@ -1,34 +1,96 @@
1
- //#region src/on-window-close.d.ts
1
+ //#region src/types/browser-change.d.ts
2
2
  /**
3
- * Manages callbacks for when specific Chrome windows are closed.
4
- * Useful for triggering alerts, saving data, or cleaning up resources
5
- * when a specific popup or window is removed.
3
+ * What moved under the side panel.
4
+ *
5
+ * - `page` the active tab navigated, the user switched to another tab, or the
6
+ * panel came back on screen and needs to be told what it is looking at.
7
+ * - `view` — the user picked a different view inside the panel; the page is
8
+ * untouched.
6
9
  */
7
- type CloseCallback = () => void;
10
+ type BrowserChangeReason = 'page' | 'view';
8
11
  /**
9
- * Registers a callback to be executed when a specific window is closed.
10
- * @param windowId - The chrome window ID to watch
11
- * @param callback - Function to execute when the window closes
12
- * @returns A function to unsubscribe (remove) this specific listener
13
- * @example
14
- * const unsubscribe = onWindowClose(123, () => {
15
- * console.log("Window 123 was closed!");
16
- * });
12
+ * @typeParam TView - Whatever the extension uses to name the views inside its side
13
+ * panel, usually a union of string literals.
17
14
  */
18
- declare function onWindowClose(windowId: number, callback: CloseCallback): () => void;
19
- //#endregion
20
- //#region src/side-panel-state-tracker.d.ts
21
- interface InitializeSidePanelStateTrackerOptions {
15
+ interface BrowserChangeEvent<TView> {
16
+ reason: BrowserChangeReason;
17
+ /** The active tab the side panel is looking at. */
18
+ tabId: number;
19
+ /** Never empty — a tab with no URL is not announced. */
20
+ url: string;
21
+ windowId: number;
22
+ tab: chrome.tabs.Tab;
23
+ /** The view on screen, so a handler can ignore changes that are not its own. */
24
+ view: TView;
22
25
  /**
23
- * When true, visibilitychange events report document.hidden as hidden state.
24
- * Disable this if you only want open/closed port state.
26
+ * The tab this event moved away from, or `null` for the first event after the
27
+ * panel opens. Handlers that left something behind on a page — markers, injected
28
+ * UI — need it to clean up the tab they are leaving, not the one being arrived
29
+ * at.
25
30
  */
26
- trackDocumentVisibility?: boolean;
31
+ previousTabId: number | null;
32
+ /**
33
+ * True when this is the first event for a side panel document that has just
34
+ * loaded. The panel has rendered nothing yet, so a handler that skips work for a
35
+ * page it believes is already on screen has to treat this as a fresh page — what
36
+ * it drew last time went away with the previous document.
37
+ */
38
+ isPanelReload: boolean;
39
+ }
40
+ type BrowserChangeHandler<TView> = (event: BrowserChangeEvent<TView>) => Promise<void> | void;
41
+ interface BrowserChangeWatcherOptions<TView> {
42
+ /**
43
+ * The view a freshly loaded panel document starts on. A new document has none of
44
+ * the previous one's state, so the watcher resets to this rather than waiting for
45
+ * the panel's first report.
46
+ */
47
+ defaultView: TView;
48
+ /**
49
+ * How long to wait for a navigation to settle before announcing it.
50
+ * `chrome.tabs.onUpdated` fires repeatedly through a single navigation.
51
+ *
52
+ * @default 500
53
+ */
54
+ debounceMs?: number;
55
+ }
56
+ interface BrowserChangeWatcher<TView> {
57
+ /**
58
+ * Registers `handler` for every page or view change under the side panel.
59
+ * Returns a disposer.
60
+ */
61
+ onBrowserChange: (handler: BrowserChangeHandler<TView>) => () => void;
62
+ /**
63
+ * Tells the watcher which view the panel is showing. Call it from wherever the
64
+ * panel reports its view; repeats of the view already recorded are ignored, so it
65
+ * is safe to call on mount as well as on a change.
66
+ */
67
+ setView: (view: TView) => void;
68
+ /** Removes the watcher's browser listeners and drops every handler. */
69
+ dispose: () => void;
70
+ }
71
+ /** The page a window's side panel currently has on screen. */
72
+ interface DisplayedPage {
73
+ tabId: number;
74
+ url: string;
75
+ }
76
+ /** The part of a {@link BrowserChangeEvent} a {@link DisplayedPageTracker} reads. */
77
+ interface DisplayedPageInput {
78
+ windowId: number;
79
+ tabId: number;
80
+ url: string;
81
+ isPanelReload: boolean;
82
+ }
83
+ interface DisplayedPageTracker {
84
+ /**
85
+ * True when the panel in this window already has this exact page on screen, so
86
+ * there is nothing for the user to wait for.
87
+ */
88
+ isDisplaying: (event: DisplayedPageInput) => boolean;
89
+ /** Records the page the panel is now showing. */
90
+ record: (event: DisplayedPageInput) => void;
91
+ /** Drops what is remembered for a window, e.g. when the window closes. */
92
+ forget: (windowId: number) => void;
27
93
  }
28
- /**
29
- * Should only be called inside a Chrome extension side panel page.
30
- */
31
- declare function initializeSidePanelStateTracker(options?: InitializeSidePanelStateTrackerOptions): () => void;
32
94
  //#endregion
33
95
  //#region src/types/side-panel.d.ts
34
96
  type SidePanelState = 'visible' | 'hidden';
@@ -58,6 +120,94 @@ interface SidePanelClientMessage {
58
120
  type: 'close-side-panel';
59
121
  }
60
122
  //#endregion
123
+ //#region src/browser-change-watcher.d.ts
124
+ /**
125
+ * Watches what the side panel is looking at and announces every change.
126
+ *
127
+ * Nothing feature-specific lives here: the browser listeners, the debounce and the
128
+ * "have we already said this" bookkeeping are shared by every consumer, so they are
129
+ * done once and the result handed to whoever registered. Features register a
130
+ * handler instead of wiring their own tab listeners.
131
+ *
132
+ * The watcher does not know how the panel reports its view — call
133
+ * {@link BrowserChangeWatcher.setView} from wherever that arrives.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const { onBrowserChange, setView } = createBrowserChangeWatcher({
138
+ * defaultView: 'insights',
139
+ * });
140
+ *
141
+ * sidePanelTabChange.onMessage(setView);
142
+ * ```
143
+ */
144
+ declare function createBrowserChangeWatcher<TView>(options: BrowserChangeWatcherOptions<TView>): BrowserChangeWatcher<TView>;
145
+ //#endregion
146
+ //#region src/displayed-page-tracker.d.ts
147
+ /**
148
+ * Remembers what each window's side panel currently has on screen, so a handler can
149
+ * tell a real move apart from a page being announced again.
150
+ *
151
+ * The same page reaches a handler more than once — the panel comes back into view,
152
+ * the service worker is recycled, a feature re-reads its data — and putting a
153
+ * loading state up for a page already rendered replaces good content with a spinner,
154
+ * or worse, with an empty state.
155
+ *
156
+ * Keyed by window, not by tab, because a panel shows one page at a time: once it has
157
+ * moved to another tab, coming back is a real change even though that tab had been
158
+ * rendered before. A panel document that has just loaded has drawn nothing, so
159
+ * {@link DisplayedPageTracker.isDisplaying} is always false for it.
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const displayedPage = createDisplayedPageTracker();
164
+ *
165
+ * onBrowserChange(async (event) => {
166
+ * const isAlreadyOnScreen = displayedPage.isDisplaying(event);
167
+ * displayedPage.record(event);
168
+ *
169
+ * if (!isAlreadyOnScreen) {
170
+ * await showLoader(event.windowId);
171
+ * }
172
+ *
173
+ * await render(await loadData(event.tabId), event.windowId);
174
+ * });
175
+ * ```
176
+ */
177
+ declare function createDisplayedPageTracker(): DisplayedPageTracker;
178
+ //#endregion
179
+ //#region src/on-window-close.d.ts
180
+ /**
181
+ * Manages callbacks for when specific Chrome windows are closed.
182
+ * Useful for triggering alerts, saving data, or cleaning up resources
183
+ * when a specific popup or window is removed.
184
+ */
185
+ type CloseCallback = () => void;
186
+ /**
187
+ * Registers a callback to be executed when a specific window is closed.
188
+ * @param windowId - The chrome window ID to watch
189
+ * @param callback - Function to execute when the window closes
190
+ * @returns A function to unsubscribe (remove) this specific listener
191
+ * @example
192
+ * const unsubscribe = onWindowClose(123, () => {
193
+ * console.log("Window 123 was closed!");
194
+ * });
195
+ */
196
+ declare function onWindowClose(windowId: number, callback: CloseCallback): () => void;
197
+ //#endregion
198
+ //#region src/side-panel-state-tracker.d.ts
199
+ interface InitializeSidePanelStateTrackerOptions {
200
+ /**
201
+ * When true, visibilitychange events report document.hidden as hidden state.
202
+ * Disable this if you only want open/closed port state.
203
+ */
204
+ trackDocumentVisibility?: boolean;
205
+ }
206
+ /**
207
+ * Should only be called inside a Chrome extension side panel page.
208
+ */
209
+ declare function initializeSidePanelStateTracker(options?: InitializeSidePanelStateTrackerOptions): () => void;
210
+ //#endregion
61
211
  //#region src/sidepanel-state-manager.d.ts
62
212
  type SidePanelStateListener = (data: SidePanelStateChangeData) => void;
63
213
  interface SidePanelStateChangeOptions {
@@ -104,11 +254,16 @@ declare function onSidePanelStateChange(listener: SidePanelStateListener, option
104
254
  *
105
255
  * A narrowed {@link onSidePanelStateChange} for code that only cares about one
106
256
  * direction. This is the event to use for "the panel is back on screen, resync
107
- * it". It covers
108
- * a freshly loaded document (`reason: 'document-load'`), a panel Chrome had cached
109
- * while another extension's side panel took over the slot
110
- * (`reason: 'visibility-change'`), and the first report after a service worker
111
- * restart (`reason: 'reconnected'`).
257
+ * it". It covers a freshly loaded document (`reason: 'document-load'`) and a panel
258
+ * Chrome had cached while another extension's side panel took over the slot
259
+ * (`reason: 'visibility-change'`).
260
+ *
261
+ * Reconnects (`reason: 'reconnected'`) are not shows and never fire this. The
262
+ * tracker reconnects when the service worker is torn down and restarted, and the
263
+ * panel document lives through that untouched — nothing appeared, so nothing needs
264
+ * resyncing. It has to be excluded explicitly because a restarted worker has no
265
+ * record of the window, leaving `previousState` empty, which would otherwise make
266
+ * every recycle look like a fresh show.
112
267
  *
113
268
  * @param listener - Callback function that receives state change data
114
269
  * @returns Unsubscribe function to remove the listener
@@ -120,11 +275,13 @@ declare function onSidePanelShown(listener: SidePanelStateListener): () => void;
120
275
  * A narrowed {@link onSidePanelStateChange} for code that only cares about one
121
276
  * direction. Nothing fires for a window that was never seen visible, so a service worker
122
277
  * restart followed by a port disconnect stays quiet instead of reporting a close
123
- * that the listener never saw open.
278
+ * that the listener never saw open. Reconnects are excluded for the same reason
279
+ * they are in {@link onSidePanelShown}: they report the state the panel is already
280
+ * in, not a move into it.
124
281
  *
125
282
  * @param listener - Callback function that receives state change data
126
283
  * @returns Unsubscribe function to remove the listener
127
284
  */
128
285
  declare function onSidePanelHidden(listener: SidePanelStateListener): () => void;
129
286
  //#endregion
130
- export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
287
+ export { BrowserChangeEvent, BrowserChangeHandler, BrowserChangeReason, BrowserChangeWatcher, BrowserChangeWatcherOptions, DisplayedPage, DisplayedPageInput, DisplayedPageTracker, InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, createBrowserChangeWatcher, createDisplayedPageTracker, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
package/dist/index.d.ts CHANGED
@@ -1,34 +1,96 @@
1
- //#region src/on-window-close.d.ts
1
+ //#region src/types/browser-change.d.ts
2
2
  /**
3
- * Manages callbacks for when specific Chrome windows are closed.
4
- * Useful for triggering alerts, saving data, or cleaning up resources
5
- * when a specific popup or window is removed.
3
+ * What moved under the side panel.
4
+ *
5
+ * - `page` the active tab navigated, the user switched to another tab, or the
6
+ * panel came back on screen and needs to be told what it is looking at.
7
+ * - `view` — the user picked a different view inside the panel; the page is
8
+ * untouched.
6
9
  */
7
- type CloseCallback = () => void;
10
+ type BrowserChangeReason = 'page' | 'view';
8
11
  /**
9
- * Registers a callback to be executed when a specific window is closed.
10
- * @param windowId - The chrome window ID to watch
11
- * @param callback - Function to execute when the window closes
12
- * @returns A function to unsubscribe (remove) this specific listener
13
- * @example
14
- * const unsubscribe = onWindowClose(123, () => {
15
- * console.log("Window 123 was closed!");
16
- * });
12
+ * @typeParam TView - Whatever the extension uses to name the views inside its side
13
+ * panel, usually a union of string literals.
17
14
  */
18
- declare function onWindowClose(windowId: number, callback: CloseCallback): () => void;
19
- //#endregion
20
- //#region src/side-panel-state-tracker.d.ts
21
- interface InitializeSidePanelStateTrackerOptions {
15
+ interface BrowserChangeEvent<TView> {
16
+ reason: BrowserChangeReason;
17
+ /** The active tab the side panel is looking at. */
18
+ tabId: number;
19
+ /** Never empty — a tab with no URL is not announced. */
20
+ url: string;
21
+ windowId: number;
22
+ tab: chrome.tabs.Tab;
23
+ /** The view on screen, so a handler can ignore changes that are not its own. */
24
+ view: TView;
22
25
  /**
23
- * When true, visibilitychange events report document.hidden as hidden state.
24
- * Disable this if you only want open/closed port state.
26
+ * The tab this event moved away from, or `null` for the first event after the
27
+ * panel opens. Handlers that left something behind on a page — markers, injected
28
+ * UI — need it to clean up the tab they are leaving, not the one being arrived
29
+ * at.
25
30
  */
26
- trackDocumentVisibility?: boolean;
31
+ previousTabId: number | null;
32
+ /**
33
+ * True when this is the first event for a side panel document that has just
34
+ * loaded. The panel has rendered nothing yet, so a handler that skips work for a
35
+ * page it believes is already on screen has to treat this as a fresh page — what
36
+ * it drew last time went away with the previous document.
37
+ */
38
+ isPanelReload: boolean;
39
+ }
40
+ type BrowserChangeHandler<TView> = (event: BrowserChangeEvent<TView>) => Promise<void> | void;
41
+ interface BrowserChangeWatcherOptions<TView> {
42
+ /**
43
+ * The view a freshly loaded panel document starts on. A new document has none of
44
+ * the previous one's state, so the watcher resets to this rather than waiting for
45
+ * the panel's first report.
46
+ */
47
+ defaultView: TView;
48
+ /**
49
+ * How long to wait for a navigation to settle before announcing it.
50
+ * `chrome.tabs.onUpdated` fires repeatedly through a single navigation.
51
+ *
52
+ * @default 500
53
+ */
54
+ debounceMs?: number;
55
+ }
56
+ interface BrowserChangeWatcher<TView> {
57
+ /**
58
+ * Registers `handler` for every page or view change under the side panel.
59
+ * Returns a disposer.
60
+ */
61
+ onBrowserChange: (handler: BrowserChangeHandler<TView>) => () => void;
62
+ /**
63
+ * Tells the watcher which view the panel is showing. Call it from wherever the
64
+ * panel reports its view; repeats of the view already recorded are ignored, so it
65
+ * is safe to call on mount as well as on a change.
66
+ */
67
+ setView: (view: TView) => void;
68
+ /** Removes the watcher's browser listeners and drops every handler. */
69
+ dispose: () => void;
70
+ }
71
+ /** The page a window's side panel currently has on screen. */
72
+ interface DisplayedPage {
73
+ tabId: number;
74
+ url: string;
75
+ }
76
+ /** The part of a {@link BrowserChangeEvent} a {@link DisplayedPageTracker} reads. */
77
+ interface DisplayedPageInput {
78
+ windowId: number;
79
+ tabId: number;
80
+ url: string;
81
+ isPanelReload: boolean;
82
+ }
83
+ interface DisplayedPageTracker {
84
+ /**
85
+ * True when the panel in this window already has this exact page on screen, so
86
+ * there is nothing for the user to wait for.
87
+ */
88
+ isDisplaying: (event: DisplayedPageInput) => boolean;
89
+ /** Records the page the panel is now showing. */
90
+ record: (event: DisplayedPageInput) => void;
91
+ /** Drops what is remembered for a window, e.g. when the window closes. */
92
+ forget: (windowId: number) => void;
27
93
  }
28
- /**
29
- * Should only be called inside a Chrome extension side panel page.
30
- */
31
- declare function initializeSidePanelStateTracker(options?: InitializeSidePanelStateTrackerOptions): () => void;
32
94
  //#endregion
33
95
  //#region src/types/side-panel.d.ts
34
96
  type SidePanelState = 'visible' | 'hidden';
@@ -58,6 +120,94 @@ interface SidePanelClientMessage {
58
120
  type: 'close-side-panel';
59
121
  }
60
122
  //#endregion
123
+ //#region src/browser-change-watcher.d.ts
124
+ /**
125
+ * Watches what the side panel is looking at and announces every change.
126
+ *
127
+ * Nothing feature-specific lives here: the browser listeners, the debounce and the
128
+ * "have we already said this" bookkeeping are shared by every consumer, so they are
129
+ * done once and the result handed to whoever registered. Features register a
130
+ * handler instead of wiring their own tab listeners.
131
+ *
132
+ * The watcher does not know how the panel reports its view — call
133
+ * {@link BrowserChangeWatcher.setView} from wherever that arrives.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const { onBrowserChange, setView } = createBrowserChangeWatcher({
138
+ * defaultView: 'insights',
139
+ * });
140
+ *
141
+ * sidePanelTabChange.onMessage(setView);
142
+ * ```
143
+ */
144
+ declare function createBrowserChangeWatcher<TView>(options: BrowserChangeWatcherOptions<TView>): BrowserChangeWatcher<TView>;
145
+ //#endregion
146
+ //#region src/displayed-page-tracker.d.ts
147
+ /**
148
+ * Remembers what each window's side panel currently has on screen, so a handler can
149
+ * tell a real move apart from a page being announced again.
150
+ *
151
+ * The same page reaches a handler more than once — the panel comes back into view,
152
+ * the service worker is recycled, a feature re-reads its data — and putting a
153
+ * loading state up for a page already rendered replaces good content with a spinner,
154
+ * or worse, with an empty state.
155
+ *
156
+ * Keyed by window, not by tab, because a panel shows one page at a time: once it has
157
+ * moved to another tab, coming back is a real change even though that tab had been
158
+ * rendered before. A panel document that has just loaded has drawn nothing, so
159
+ * {@link DisplayedPageTracker.isDisplaying} is always false for it.
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const displayedPage = createDisplayedPageTracker();
164
+ *
165
+ * onBrowserChange(async (event) => {
166
+ * const isAlreadyOnScreen = displayedPage.isDisplaying(event);
167
+ * displayedPage.record(event);
168
+ *
169
+ * if (!isAlreadyOnScreen) {
170
+ * await showLoader(event.windowId);
171
+ * }
172
+ *
173
+ * await render(await loadData(event.tabId), event.windowId);
174
+ * });
175
+ * ```
176
+ */
177
+ declare function createDisplayedPageTracker(): DisplayedPageTracker;
178
+ //#endregion
179
+ //#region src/on-window-close.d.ts
180
+ /**
181
+ * Manages callbacks for when specific Chrome windows are closed.
182
+ * Useful for triggering alerts, saving data, or cleaning up resources
183
+ * when a specific popup or window is removed.
184
+ */
185
+ type CloseCallback = () => void;
186
+ /**
187
+ * Registers a callback to be executed when a specific window is closed.
188
+ * @param windowId - The chrome window ID to watch
189
+ * @param callback - Function to execute when the window closes
190
+ * @returns A function to unsubscribe (remove) this specific listener
191
+ * @example
192
+ * const unsubscribe = onWindowClose(123, () => {
193
+ * console.log("Window 123 was closed!");
194
+ * });
195
+ */
196
+ declare function onWindowClose(windowId: number, callback: CloseCallback): () => void;
197
+ //#endregion
198
+ //#region src/side-panel-state-tracker.d.ts
199
+ interface InitializeSidePanelStateTrackerOptions {
200
+ /**
201
+ * When true, visibilitychange events report document.hidden as hidden state.
202
+ * Disable this if you only want open/closed port state.
203
+ */
204
+ trackDocumentVisibility?: boolean;
205
+ }
206
+ /**
207
+ * Should only be called inside a Chrome extension side panel page.
208
+ */
209
+ declare function initializeSidePanelStateTracker(options?: InitializeSidePanelStateTrackerOptions): () => void;
210
+ //#endregion
61
211
  //#region src/sidepanel-state-manager.d.ts
62
212
  type SidePanelStateListener = (data: SidePanelStateChangeData) => void;
63
213
  interface SidePanelStateChangeOptions {
@@ -104,11 +254,16 @@ declare function onSidePanelStateChange(listener: SidePanelStateListener, option
104
254
  *
105
255
  * A narrowed {@link onSidePanelStateChange} for code that only cares about one
106
256
  * direction. This is the event to use for "the panel is back on screen, resync
107
- * it". It covers
108
- * a freshly loaded document (`reason: 'document-load'`), a panel Chrome had cached
109
- * while another extension's side panel took over the slot
110
- * (`reason: 'visibility-change'`), and the first report after a service worker
111
- * restart (`reason: 'reconnected'`).
257
+ * it". It covers a freshly loaded document (`reason: 'document-load'`) and a panel
258
+ * Chrome had cached while another extension's side panel took over the slot
259
+ * (`reason: 'visibility-change'`).
260
+ *
261
+ * Reconnects (`reason: 'reconnected'`) are not shows and never fire this. The
262
+ * tracker reconnects when the service worker is torn down and restarted, and the
263
+ * panel document lives through that untouched — nothing appeared, so nothing needs
264
+ * resyncing. It has to be excluded explicitly because a restarted worker has no
265
+ * record of the window, leaving `previousState` empty, which would otherwise make
266
+ * every recycle look like a fresh show.
112
267
  *
113
268
  * @param listener - Callback function that receives state change data
114
269
  * @returns Unsubscribe function to remove the listener
@@ -120,11 +275,13 @@ declare function onSidePanelShown(listener: SidePanelStateListener): () => void;
120
275
  * A narrowed {@link onSidePanelStateChange} for code that only cares about one
121
276
  * direction. Nothing fires for a window that was never seen visible, so a service worker
122
277
  * restart followed by a port disconnect stays quiet instead of reporting a close
123
- * that the listener never saw open.
278
+ * that the listener never saw open. Reconnects are excluded for the same reason
279
+ * they are in {@link onSidePanelShown}: they report the state the panel is already
280
+ * in, not a move into it.
124
281
  *
125
282
  * @param listener - Callback function that receives state change data
126
283
  * @returns Unsubscribe function to remove the listener
127
284
  */
128
285
  declare function onSidePanelHidden(listener: SidePanelStateListener): () => void;
129
286
  //#endregion
130
- export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
287
+ export { BrowserChangeEvent, BrowserChangeHandler, BrowserChangeReason, BrowserChangeWatcher, BrowserChangeWatcherOptions, DisplayedPage, DisplayedPageInput, DisplayedPageTracker, InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, createBrowserChangeWatcher, createDisplayedPageTracker, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- const e=new Map;let t=!1;function n(){t||(t=!0,chrome.windows.onRemoved.addListener(t=>{let n=e.get(t);n&&(n.forEach(e=>{e()}),e.delete(t))}))}function r(t,r){n();let i=e.get(t);return i||(i=new Set,e.set(t,i)),i.add(r),()=>{let n=e.get(t);n&&(n.delete(r),n.size===0&&e.delete(t))}}let i=!1;function a(e={}){if(i)return console.info(`Side panel state tracker already initialized`),()=>{};i=!0;let{trackDocumentVisibility:t=!0}=e,n,r,a=0,o,s=!1;function c(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(s)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let i=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(s||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**a,5e3);a+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:i,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!s){c();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,a=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(s=!0,c(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(o=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,o)),f(`document-load`)}),()=>{s=!0,c(),o&&document.removeEventListener(`visibilitychange`,o);let e=n;n=void 0,e?.disconnect(),i=!1}}const o=new Map,s=new Map,c=new Set;let l=!1;function u(){l||(l=!0,chrome.action.onClicked.addListener(e=>{let t=o.get(e.windowId);t&&t.state===`visible`?t.port&&(o.delete(e.windowId),t.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:e.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(e=>{e.name===chrome.runtime.id&&(e.onMessage.addListener(t=>{t.type===`side-panel-state-tracker`&&t.state&&p({port:e,state:t.state,reason:t.reason??`unknown`,windowId:t.windowId,type:t.type})}),e.onDisconnect.addListener(e=>{Array.from(o.entries()).forEach(([t,n])=>{n.port&&n.port===e&&p({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(e=>{o.delete(e),s.delete(e)}))}function d(){l||=(u(),!0)}function f(e,t){if(e.type!==`side-panel-state-tracker`)return;let n={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t},r=e.state===t;c.forEach(({listener:e,includeRepeats:t})=>{if(!(r&&!t))try{e(n)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function p(e){let{windowId:t,state:n}=e,r=s.get(t);if(e.type===`side-panel-state-tracker`&&s.set(t,n),n===`hidden`){o.delete(t),f(e,r);return}o.set(t,e),f(e,r)}function m(e){return d(),o.get(e)?.state}function h(e){return d(),m(e)===`visible`}function g(e,t={}){d();let n={listener:e,includeRepeats:t.includeRepeats===!0};return c.add(n),()=>{c.delete(n)}}function _(e){return g(t=>{t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function v(e){return g(t=>{t.state===`hidden`&&t.previousState===`visible`&&e(t)})}export{m as getSidePanelStateForWindow,u as initSidePanelStateManager,a as initializeSidePanelStateTracker,h as isWindowSidePanelVisible,v as onSidePanelHidden,_ as onSidePanelShown,g as onSidePanelStateChange,r as onWindowClose};
1
+ const e=new Map,t=new Map,n=new Set;let r=!1;function i(){r||(r=!0,chrome.action.onClicked.addListener(t=>{let n=e.get(t.windowId);n&&n.state===`visible`?n.port&&(e.delete(t.windowId),n.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:t.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(t=>{t.name===chrome.runtime.id&&(t.onMessage.addListener(e=>{e.type===`side-panel-state-tracker`&&e.state&&s({port:t,state:e.state,reason:e.reason??`unknown`,windowId:e.windowId,type:e.type})}),t.onDisconnect.addListener(t=>{Array.from(e.entries()).forEach(([e,n])=>{n.port&&n.port===t&&s({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:e,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(n=>{e.delete(n),t.delete(n)}))}function a(){r||=(i(),!0)}function o(e,t){if(e.type!==`side-panel-state-tracker`)return;let r={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t},i=e.state===t;n.forEach(({listener:e,includeRepeats:t})=>{if(!(i&&!t))try{e(r)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function s(n){let{windowId:r,state:i}=n,a=t.get(r);if(n.type===`side-panel-state-tracker`&&t.set(r,i),i===`hidden`){e.delete(r),o(n,a);return}e.set(r,n),o(n,a)}function c(t){return a(),e.get(t)?.state}function l(e){return a(),c(e)===`visible`}function u(e,t={}){a();let r={listener:e,includeRepeats:t.includeRepeats===!0};return n.add(r),()=>{n.delete(r)}}function d(e){return u(t=>{t.reason!==`reconnected`&&t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function f(e){return u(t=>{t.reason!==`reconnected`&&t.state===`hidden`&&t.previousState===`visible`&&e(t)})}function p(e){let{defaultView:t,debounceMs:n=500}=e,r=new Set,i=t,a,o,s=!1,c;function u(){c&&=(clearTimeout(c),void 0)}async function f(e){await Promise.all([...r].map(async t=>{try{await t(e)}catch(e){console.error(`[browser-change] A change handler failed`,e)}}))}async function p(e,t,n,r={}){if(!s)try{let s=await chrome.tabs.get(e);if(!s.active||n===`page`&&o===e&&a===t)return;let c=o??null;n===`page`&&(o=e,a=t),await f({reason:n,tabId:e,url:t,windowId:s.windowId,tab:s,view:i,previousTabId:c,isPanelReload:r.isPanelReload===!0})}catch(e){console.error(`[browser-change] Failed to read the tab that changed`,e)}}async function m(e,t={}){if(s)return;let r=await chrome.tabs.get(e);if(!r.active||!l(r.windowId))return;let{url:i}=r;if(i!=null){if(u(),t.immediate===!0){await p(e,i,`page`,t);return}c=setTimeout(()=>{c=void 0,p(e,i,`page`,t).catch(console.error)},n)}}function h(e,t={}){chrome.tabs.query({windowId:e,active:!0}).then(async e=>{let n=e[0];n?.id!=null&&await m(n.id,{...t,immediate:!0})}).catch(console.error)}function g(){chrome.tabs.query({active:!0,lastFocusedWindow:!0}).then(async e=>{let t=e[0];t?.id==null||t.url==null||l(t.windowId)&&await p(t.id,t.url,`view`)}).catch(console.error)}function _(e){m(e).catch(console.error)}function v(e){m(e.tabId).catch(()=>{})}chrome.tabs.onUpdated.addListener(_),chrome.tabs.onActivated.addListener(v);let y=d(({reason:e,windowId:n})=>{s||(e===`document-load`&&(i=t),a=void 0,o=void 0,h(n,{isPanelReload:e===`document-load`}))});return{onBrowserChange(e){return r.add(e),()=>{r.delete(e)}},setView(e){s||i===e||(i=e,g())},dispose(){s=!0,u(),chrome.tabs.onUpdated.removeListener(_),chrome.tabs.onActivated.removeListener(v),y(),r.clear()}}}function m(){let e=new Map;return{isDisplaying(t){if(t.isPanelReload)return!1;let n=e.get(t.windowId);return n?.tabId===t.tabId&&n.url===t.url},record(t){e.set(t.windowId,{tabId:t.tabId,url:t.url})},forget(t){e.delete(t)}}}const h=new Map;let g=!1;function _(){g||(g=!0,chrome.windows.onRemoved.addListener(e=>{let t=h.get(e);t&&(t.forEach(e=>{e()}),h.delete(e))}))}function v(e,t){_();let n=h.get(e);return n||(n=new Set,h.set(e,n)),n.add(t),()=>{let n=h.get(e);n&&(n.delete(t),n.size===0&&h.delete(e))}}let y=!1;function b(e={}){if(y)return console.info(`Side panel state tracker already initialized`),()=>{};y=!0;let{trackDocumentVisibility:t=!0}=e,n,r,i=0,a,o=!1;function s(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(o)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let c=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(o||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**i,5e3);i+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:c,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!o){s();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,i=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(o=!0,s(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(a=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,a)),f(`document-load`)}),()=>{o=!0,s(),a&&document.removeEventListener(`visibilitychange`,a);let e=n;n=void 0,e?.disconnect(),y=!1}}export{p as createBrowserChangeWatcher,m as createDisplayedPageTracker,c as getSidePanelStateForWindow,i as initSidePanelStateManager,b as initializeSidePanelStateTracker,l as isWindowSidePanelVisible,f as onSidePanelHidden,d as onSidePanelShown,u as onSidePanelStateChange,v as onWindowClose};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pixpilot/chrome-lifecycle",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
5
  "description": "Lifecycle management utilities for Chrome extensions.",
6
6
  "author": "m.doaie <m.doaie@hotmail.com>",
7
7
  "license": "MIT",
@@ -29,11 +29,11 @@
29
29
  "eslint": "^9.38.0",
30
30
  "tsdown": "^0.15.9",
31
31
  "typescript": "^5.9.3",
32
+ "@internal/eslint-config": "0.3.0",
32
33
  "@internal/prettier-config": "0.1.0",
33
- "@internal/tsconfig": "0.1.0",
34
- "@internal/tsdown-config": "0.1.0",
35
34
  "@internal/vitest-config": "0.1.0",
36
- "@internal/eslint-config": "0.3.0"
35
+ "@internal/tsconfig": "0.1.0",
36
+ "@internal/tsdown-config": "0.1.0"
37
37
  },
38
38
  "prettier": "@internal/prettier-config",
39
39
  "scripts": {