@logseq/libs
Advanced tools
+13
-1
@@ -7,4 +7,16 @@ # Changelog | ||
| ## [0.0.16] | ||
| ### Added | ||
| - Support api of `logseq.UI.queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>` | ||
| - Support api of `logseq.UI.queryElementById: (id: string) => Promise<string | boolean>` | ||
| - Support api of `logseq.UI.checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>` | ||
| - Support api of `logseq.UI.resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<any>` | ||
| - Support api of `logseq.Assets.builtInOpen(path: string): Promise<boolean | undefined>` | ||
| ### Fixed | ||
| - fix Plugin can't register command shortcut with editing mode [#10392](https://github.com/logseq/logseq/issues/10392) | ||
| - fix [Plugin API] [Keymap] Command without keybinding can't be present in Keymap [#10466](https://github.com/logseq/logseq/issues/10466) | ||
| - fix [Possible DATA LOSS] [Plugin API] [Keymap] Any plugin could break the global config.edn [#10465](https://github.com/logseq/logseq/issues/10465) | ||
| ## [0.0.15] | ||
| ### Added | ||
@@ -11,0 +23,0 @@ - Support a plug-in flag for the plugin slash commands item |
@@ -22,9 +22,3 @@ /// <reference types="node" /> | ||
| export declare function isObject(item: any): boolean; | ||
| export declare const deepMerge: { | ||
| <TObject, TSource>(object: TObject, source: TSource): TObject & TSource; | ||
| <TObject_1, TSource1, TSource2>(object: TObject_1, source1: TSource1, source2: TSource2): TObject_1 & TSource1 & TSource2; | ||
| <TObject_2, TSource1_1, TSource2_1, TSource3>(object: TObject_2, source1: TSource1_1, source2: TSource2_1, source3: TSource3): TObject_2 & TSource1_1 & TSource2_1 & TSource3; | ||
| <TObject_3, TSource1_2, TSource2_2, TSource3_1, TSource4>(object: TObject_3, source1: TSource1_2, source2: TSource2_2, source3: TSource3_1, source4: TSource4): TObject_3 & TSource1_2 & TSource2_2 & TSource3_1 & TSource4; | ||
| (object: any, ...otherArgs: any[]): any; | ||
| }; | ||
| export declare function deepMerge<T>(a: Partial<T>, b: Partial<T>): T; | ||
| export declare class PluginLogger extends EventEmitter<'change'> { | ||
@@ -69,3 +63,3 @@ private _tag?; | ||
| float: boolean; | ||
| }) => void): () => void; | ||
| }) => void): false | (() => void); | ||
| export declare function cleanInjectedUI(id: string): void; | ||
@@ -76,1 +70,2 @@ export declare function cleanInjectedScripts(this: PluginLocal): void; | ||
| export declare function mergeSettingsWithSchema(settings: Record<string, any>, schema: Array<SettingSchemaDesc>): Record<string, any>; | ||
| export declare function normalizeKeyStr(s: string): string; |
| import EventEmitter from 'eventemitter3'; | ||
| import { deferred, PluginLogger } from './helpers'; | ||
| import * as pluginHelpers from './helpers'; | ||
| import DOMPurify from 'dompurify'; | ||
| import { LSPluginCaller } from './LSPlugin.caller'; | ||
@@ -9,2 +10,3 @@ import { ILSPluginThemeManager, LegacyTheme, SettingSchemaDesc, Theme, ThemeMode } from './LSPlugin'; | ||
| LSPluginCore: LSPluginCore; | ||
| DOMPurify: typeof DOMPurify; | ||
| } | ||
@@ -11,0 +13,0 @@ } |
+46
-35
@@ -28,3 +28,2 @@ import * as CSS from 'csstype'; | ||
| resizable: boolean; | ||
| [key: string]: any; | ||
| }; | ||
@@ -63,5 +62,15 @@ export declare type UIBaseOptions = { | ||
| icon: string; | ||
| [key: string]: any; | ||
| /** | ||
| * Alternative entrypoint for development. | ||
| */ | ||
| devEntry: unknown; | ||
| /** | ||
| * For legacy themes, do not use. | ||
| */ | ||
| theme: unknown; | ||
| } | ||
| export interface LSPluginBaseInfo { | ||
| /** | ||
| * Must be unique. | ||
| */ | ||
| id: string; | ||
@@ -71,5 +80,12 @@ mode: 'shadow' | 'iframe'; | ||
| disabled: boolean; | ||
| [key: string]: any; | ||
| }; | ||
| [key: string]: any; | ||
| } & Record<string, unknown>; | ||
| effect: boolean; | ||
| /** | ||
| * For internal use only. Indicates if plugin is installed in dot root. | ||
| */ | ||
| iir: boolean; | ||
| /** | ||
| * For internal use only. | ||
| */ | ||
| lsr: string; | ||
| } | ||
@@ -122,3 +138,2 @@ export declare type IHookEvent = { | ||
| enabledJournals: boolean; | ||
| [key: string]: any; | ||
| } | ||
@@ -132,3 +147,2 @@ /** | ||
| path: string; | ||
| [key: string]: any; | ||
| } | ||
@@ -144,3 +158,2 @@ /** | ||
| parent: IEntityID; | ||
| unordered: boolean; | ||
| content: string; | ||
@@ -162,3 +175,3 @@ page: IEntityID; | ||
| title?: Array<any>; | ||
| [key: string]: any; | ||
| marker?: string; | ||
| } | ||
@@ -181,3 +194,2 @@ /** | ||
| updatedAt?: number; | ||
| [key: string]: any; | ||
| } | ||
@@ -200,5 +212,6 @@ export declare type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>; | ||
| }; | ||
| export declare type Keybinding = string | Array<string>; | ||
| export declare type SimpleCommandKeybinding = { | ||
| mode?: 'global' | 'non-editing' | 'editing'; | ||
| binding: string; | ||
| binding: Keybinding; | ||
| mac?: string; | ||
@@ -216,3 +229,3 @@ }; | ||
| }; | ||
| export declare type ExternalCommandType = 'logseq.command/run' | 'logseq.editor/cycle-todo' | 'logseq.editor/down' | 'logseq.editor/up' | 'logseq.editor/expand-block-children' | 'logseq.editor/collapse-block-children' | 'logseq.editor/open-file-in-default-app' | 'logseq.editor/open-file-in-directory' | 'logseq.editor/select-all-blocks' | 'logseq.editor/toggle-open-blocks' | 'logseq.editor/zoom-in' | 'logseq.editor/zoom-out' | 'logseq.editor/indent' | 'logseq.editor/outdent' | 'logseq.editor/copy' | 'logseq.editor/cut' | 'logseq.go/home' | 'logseq.go/journals' | 'logseq.go/keyboard-shortcuts' | 'logseq.go/next-journal' | 'logseq.go/prev-journal' | 'logseq.go/search' | 'logseq.go/search-in-page' | 'logseq.go/tomorrow' | 'logseq.go/backward' | 'logseq.go/forward' | 'logseq.search/re-index' | 'logseq.sidebar/clear' | 'logseq.sidebar/open-today-page' | 'logseq.ui/goto-plugins' | 'logseq.ui/select-theme-color' | 'logseq.ui/toggle-brackets' | 'logseq.ui/toggle-cards' | 'logseq.ui/toggle-contents' | 'logseq.ui/toggle-document-mode' | 'logseq.ui/toggle-help' | 'logseq.ui/toggle-left-sidebar' | 'logseq.ui/toggle-right-sidebar' | 'logseq.ui/toggle-settings' | 'logseq.ui/toggle-theme' | 'logseq.ui/toggle-wide-mode' | 'logseq.command-palette/toggle'; | ||
| export declare type ExternalCommandType = 'logseq.command/run' | 'logseq.editor/cycle-todo' | 'logseq.editor/down' | 'logseq.editor/up' | 'logseq.editor/expand-block-children' | 'logseq.editor/collapse-block-children' | 'logseq.editor/open-file-in-default-app' | 'logseq.editor/open-file-in-directory' | 'logseq.editor/select-all-blocks' | 'logseq.editor/toggle-open-blocks' | 'logseq.editor/zoom-in' | 'logseq.editor/zoom-out' | 'logseq.editor/indent' | 'logseq.editor/outdent' | 'logseq.editor/copy' | 'logseq.editor/cut' | 'logseq.go/home' | 'logseq.go/journals' | 'logseq.go/keyboard-shortcuts' | 'logseq.go/next-journal' | 'logseq.go/prev-journal' | 'logseq.go/search' | 'logseq.go/tomorrow' | 'logseq.go/backward' | 'logseq.go/forward' | 'logseq.search/re-index' | 'logseq.sidebar/clear' | 'logseq.sidebar/open-today-page' | 'logseq.ui/goto-plugins' | 'logseq.ui/select-theme-color' | 'logseq.ui/toggle-brackets' | 'logseq.ui/toggle-contents' | 'logseq.ui/toggle-document-mode' | 'logseq.ui/toggle-help' | 'logseq.ui/toggle-left-sidebar' | 'logseq.ui/toggle-right-sidebar' | 'logseq.ui/toggle-settings' | 'logseq.ui/toggle-theme' | 'logseq.ui/toggle-wide-mode'; | ||
| export declare type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'; | ||
@@ -278,3 +291,8 @@ export declare type SearchIndiceInitStatus = boolean; | ||
| */ | ||
| registerCommandShortcut: (keybinding: SimpleCommandKeybinding, action: SimpleCommandCallback) => void; | ||
| registerCommandShortcut: (keybinding: SimpleCommandKeybinding | string, action: SimpleCommandCallback, opts?: Partial<{ | ||
| key: string; | ||
| label: string; | ||
| desc: string; | ||
| extras: Record<string, any>; | ||
| }>) => void; | ||
| /** | ||
@@ -335,14 +353,2 @@ * Supported all registered palette commands | ||
| insertTemplate: (target: BlockUUID, name: string) => Promise<any>; | ||
| queryElementById: (id: string) => Promise<string | boolean>; | ||
| /** | ||
| * @added 0.0.5 | ||
| * @param selector | ||
| */ | ||
| queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>; | ||
| /** | ||
| * @deprecated Use `logseq.UI.showMsg` instead | ||
| * @param content | ||
| * @param status | ||
| */ | ||
| showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void; | ||
| setZoomFactor: (factor: number) => void; | ||
@@ -378,2 +384,4 @@ setFullScreen: (flag: boolean | 'toggle') => void; | ||
| }>; | ||
| onBeforeCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook; | ||
| onAfterCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook; | ||
| /** | ||
@@ -592,3 +600,3 @@ * provide ui slot to specific block with UUID | ||
| }) => void; | ||
| openInRightSidebar: (uuid: BlockUUID) => void; | ||
| openInRightSidebar: (id: BlockUUID | EntityID) => void; | ||
| /** | ||
@@ -667,11 +675,8 @@ * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator | ||
| export interface IUIProxy { | ||
| /** | ||
| * @added 0.0.2 | ||
| * | ||
| * @param content | ||
| * @param status | ||
| * @param opts | ||
| */ | ||
| showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string, opts?: Partial<UIMsgOptions>) => Promise<UIMsgKey>; | ||
| closeMsg: (key: UIMsgKey) => void; | ||
| queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>; | ||
| queryElementById: (id: string) => Promise<string | boolean>; | ||
| checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>; | ||
| resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<Record<string, string | undefined> | null>; | ||
| } | ||
@@ -705,2 +710,8 @@ /** | ||
| makeUrl(path: string): Promise<string>; | ||
| /** | ||
| * try to open asset type file in Logseq app | ||
| * @added 0.0.16 | ||
| * @param path | ||
| */ | ||
| builtInOpen(path: string): Promise<boolean | undefined>; | ||
| } | ||
@@ -841,4 +852,4 @@ export interface ILSPluginThemeManager { | ||
| resolveResourceFullUrl(filePath: string): string; | ||
| App: IAppProxy & Record<string, any>; | ||
| Editor: IEditorProxy & Record<string, any>; | ||
| App: IAppProxy; | ||
| Editor: IEditorProxy; | ||
| DB: IDBProxy; | ||
@@ -845,0 +856,0 @@ Git: IGitProxy; |
@@ -78,5 +78,4 @@ import { PluginLogger } from './helpers'; | ||
| get settings(): { | ||
| [key: string]: any; | ||
| disabled: boolean; | ||
| }; | ||
| } & Record<string, unknown>; | ||
| get caller(): LSPluginCaller; | ||
@@ -83,0 +82,0 @@ resolveResourceFullUrl(filePath: string): string; |
+2
-1
| { | ||
| "name": "@logseq/libs", | ||
| "version": "0.0.15", | ||
| "version": "0.0.16", | ||
| "description": "Logseq SDK libraries", | ||
@@ -21,2 +21,3 @@ "main": "dist/lsplugin.user.js", | ||
| "debug": "4.3.4", | ||
| "deepmerge": "4.3.1", | ||
| "dompurify": "2.3.8", | ||
@@ -23,0 +24,0 @@ "eventemitter3": "4.0.7", |
| import { safeSnakeCase } from '../helpers'; | ||
| /** | ||
| * WARN: These are some experience features and may be adjusted at any time. | ||
| * These unofficial plugins that use these APIs are temporarily | ||
| * not supported on the Marketplace. | ||
| */ | ||
| export class LSPluginExperiments { | ||
| ctx; | ||
| constructor(ctx) { | ||
| this.ctx = ctx; | ||
| } | ||
| get React() { | ||
| return this.ensureHostScope().React; | ||
| } | ||
| get ReactDOM() { | ||
| return this.ensureHostScope().ReactDOM; | ||
| } | ||
| get pluginLocal() { | ||
| return this.ensureHostScope().LSPluginCore.ensurePlugin(this.ctx.baseInfo.id); | ||
| } | ||
| invokeExperMethod(type, ...args) { | ||
| const host = this.ensureHostScope(); | ||
| type = safeSnakeCase(type)?.toLowerCase(); | ||
| return host.logseq.api['exper_' + type]?.apply(host, args); | ||
| } | ||
| async loadScripts(...scripts) { | ||
| scripts = scripts.map((it) => { | ||
| if (!it?.startsWith('http')) { | ||
| return this.ctx.resolveResourceFullUrl(it); | ||
| } | ||
| return it; | ||
| }); | ||
| scripts.unshift(this.ctx.baseInfo.id); | ||
| await this.invokeExperMethod('loadScripts', ...scripts); | ||
| } | ||
| registerFencedCodeRenderer(type, opts) { | ||
| return this.ensureHostScope().logseq.api.exper_register_fenced_code_renderer(this.ctx.baseInfo.id, type, opts); | ||
| } | ||
| registerExtensionsEnhancer(type, enhancer) { | ||
| const host = this.ensureHostScope(); | ||
| switch (type) { | ||
| case 'katex': | ||
| if (host.katex) { | ||
| enhancer(host.katex).catch(console.error); | ||
| } | ||
| break; | ||
| default: | ||
| } | ||
| return host.logseq.api.exper_register_extensions_enhancer(this.ctx.baseInfo.id, type, enhancer); | ||
| } | ||
| ensureHostScope() { | ||
| if (window === top) { | ||
| throw new Error('Can not access host scope!'); | ||
| } | ||
| return top; | ||
| } | ||
| } |
| import { EventEmitter } from 'eventemitter3'; | ||
| const CLIENT_MSG_CALLBACK = '#lsp#request#callback'; | ||
| const genTaskCallbackType = (id) => `task_callback_${id}`; | ||
| /** | ||
| * Request task | ||
| */ | ||
| export class LSPluginRequestTask { | ||
| _client; | ||
| _requestId; | ||
| _requestOptions; | ||
| _promise; | ||
| _aborted = false; | ||
| constructor(_client, _requestId, _requestOptions = {}) { | ||
| this._client = _client; | ||
| this._requestId = _requestId; | ||
| this._requestOptions = _requestOptions; | ||
| this._promise = new Promise((resolve, reject) => { | ||
| if (!this._requestId) { | ||
| return reject(null); | ||
| } | ||
| // task result listener | ||
| this._client.once(genTaskCallbackType(this._requestId), (e) => { | ||
| if (e && e instanceof Error) { | ||
| reject(e); | ||
| } | ||
| else { | ||
| resolve(e); | ||
| } | ||
| }); | ||
| }); | ||
| const { success, fail, final } = this._requestOptions; | ||
| this._promise | ||
| .then((res) => { | ||
| success?.(res); | ||
| }) | ||
| .catch((e) => { | ||
| fail?.(e); | ||
| }) | ||
| .finally(() => { | ||
| final?.(); | ||
| }); | ||
| } | ||
| abort() { | ||
| if (!this._requestOptions.abortable || | ||
| this._aborted) | ||
| return; | ||
| this._client.ctx._execCallableAPI('http_request_abort', this._requestId); | ||
| this._aborted = true; | ||
| } | ||
| get promise() { | ||
| return this._promise; | ||
| } | ||
| get client() { | ||
| return this._client; | ||
| } | ||
| get requestId() { | ||
| return this._requestId; | ||
| } | ||
| } | ||
| /** | ||
| * A simple request client | ||
| */ | ||
| export class LSPluginRequest extends EventEmitter { | ||
| _ctx; | ||
| constructor(_ctx) { | ||
| super(); | ||
| this._ctx = _ctx; | ||
| // request callback listener | ||
| this.ctx.caller.on(CLIENT_MSG_CALLBACK, (e) => { | ||
| const reqId = e?.requestId; | ||
| if (!reqId) | ||
| return; | ||
| this.emit(genTaskCallbackType(reqId), e?.payload); | ||
| }); | ||
| } | ||
| static createRequestTask(client, requestID, requestOptions) { | ||
| return new LSPluginRequestTask(client, requestID, requestOptions); | ||
| } | ||
| async _request(options) { | ||
| const pid = this.ctx.baseInfo.id; | ||
| const { success, fail, final, ...requestOptions } = options; | ||
| const reqID = this.ctx.Experiments.invokeExperMethod('request', pid, requestOptions); | ||
| const task = LSPluginRequest.createRequestTask(this.ctx.Request, reqID, options); | ||
| if (!requestOptions.abortable) { | ||
| return task.promise; | ||
| } | ||
| return task; | ||
| } | ||
| get ctx() { | ||
| return this._ctx; | ||
| } | ||
| } |
| import { isArray, isFunction, mapKeys } from 'lodash-es'; | ||
| export class LSPluginSearchService { | ||
| ctx; | ||
| serviceHooks; | ||
| /** | ||
| * @param ctx | ||
| * @param serviceHooks | ||
| */ | ||
| constructor(ctx, serviceHooks) { | ||
| this.ctx = ctx; | ||
| this.serviceHooks = serviceHooks; | ||
| ctx._execCallableAPI('register-search-service', ctx.baseInfo.id, serviceHooks.name, serviceHooks.options); | ||
| // hook events TODO: remove listeners | ||
| const wrapHookEvent = (k) => `service:search:${k}:${serviceHooks.name}`; | ||
| Object.entries({ | ||
| query: { | ||
| f: 'onQuery', args: ['graph', 'q', true], reply: true, | ||
| transformOutput: (data) => { | ||
| // TODO: transform keys? | ||
| if (isArray(data?.blocks)) { | ||
| data.blocks = data.blocks.map(it => { | ||
| return it && mapKeys(it, (_, k) => `block/${k}`); | ||
| }); | ||
| } | ||
| return data; | ||
| } | ||
| }, | ||
| rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] }, | ||
| transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] }, | ||
| truncateBlocks: { f: 'onIndiceReset', args: ['graph'] }, | ||
| removeDb: { f: 'onGraph', args: ['graph'] } | ||
| }).forEach(([k, v]) => { | ||
| const hookEvent = wrapHookEvent(k); | ||
| ctx.caller.on(hookEvent, async (payload) => { | ||
| if (isFunction(serviceHooks?.[v.f])) { | ||
| let ret = null; | ||
| try { | ||
| ret = await serviceHooks[v.f].apply(serviceHooks, (v.args || []).map((prop) => { | ||
| if (!payload) | ||
| return; | ||
| if (prop === true) | ||
| return payload; | ||
| if (payload.hasOwnProperty(prop)) { | ||
| const ret = payload[prop]; | ||
| delete payload[prop]; | ||
| return ret; | ||
| } | ||
| })); | ||
| if (v.transformOutput) { | ||
| ret = v.transformOutput(ret); | ||
| } | ||
| } | ||
| catch (e) { | ||
| console.error('[SearchService] ', e); | ||
| ret = e; | ||
| } | ||
| finally { | ||
| if (v.reply) { | ||
| ctx.caller.call(`${hookEvent}:reply`, ret); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| } |
| /** | ||
| * A storage based on local files under specific context | ||
| */ | ||
| class LSPluginFileStorage { | ||
| ctx; | ||
| opts; | ||
| /** | ||
| * @param ctx | ||
| * @param opts | ||
| */ | ||
| constructor(ctx, opts) { | ||
| this.ctx = ctx; | ||
| this.opts = opts; | ||
| } | ||
| /** | ||
| * plugin id | ||
| */ | ||
| get ctxId() { | ||
| return this.ctx.baseInfo.id; | ||
| } | ||
| /** | ||
| * @param key A string as file name that support nested directory | ||
| * @param value Storage value | ||
| */ | ||
| setItem(key, value) { | ||
| return this.ctx.caller.callAsync(`api:call`, { | ||
| method: 'write-plugin-storage-file', | ||
| args: [this.ctxId, key, value, this.opts?.assets], | ||
| }); | ||
| } | ||
| /** | ||
| * @param key | ||
| */ | ||
| getItem(key) { | ||
| return this.ctx.caller.callAsync(`api:call`, { | ||
| method: 'read-plugin-storage-file', | ||
| args: [this.ctxId, key, this.opts?.assets], | ||
| }); | ||
| } | ||
| /** | ||
| * @param key | ||
| */ | ||
| removeItem(key) { | ||
| return this.ctx.caller.call(`api:call`, { | ||
| method: 'unlink-plugin-storage-file', | ||
| args: [this.ctxId, key, this.opts?.assets], | ||
| }); | ||
| } | ||
| /** | ||
| * Get all path file keys | ||
| */ | ||
| allKeys() { | ||
| return this.ctx.caller.callAsync(`api:call`, { | ||
| method: 'list-plugin-storage-files', | ||
| args: [this.ctxId, this.opts?.assets] | ||
| }); | ||
| } | ||
| /** | ||
| * Clears the storage | ||
| */ | ||
| clear() { | ||
| return this.ctx.caller.call(`api:call`, { | ||
| method: 'clear-plugin-storage-files', | ||
| args: [this.ctxId, this.opts?.assets], | ||
| }); | ||
| } | ||
| /** | ||
| * @param key | ||
| */ | ||
| hasItem(key) { | ||
| return this.ctx.caller.callAsync(`api:call`, { | ||
| method: 'exist-plugin-storage-file', | ||
| args: [this.ctxId, key, this.opts?.assets], | ||
| }); | ||
| } | ||
| } | ||
| export { LSPluginFileStorage }; |
| // Fork from https://github.com/dollarshaveclub/postmate | ||
| /** | ||
| * The type of messages our frames our sending | ||
| * @type {String} | ||
| */ | ||
| export const messageType = 'application/x-postmate-v1+json'; | ||
| /** | ||
| * The maximum number of attempts to send a handshake request to the parent | ||
| * @type {Number} | ||
| */ | ||
| export const maxHandshakeRequests = 5; | ||
| /** | ||
| * A unique message ID that is used to ensure responses are sent to the correct requests | ||
| * @type {Number} | ||
| */ | ||
| let _messageId = 0; | ||
| /** | ||
| * Increments and returns a message ID | ||
| * @return {Number} A unique ID for a message | ||
| */ | ||
| export const generateNewMessageId = () => ++_messageId; | ||
| /** | ||
| * Postmate logging function that enables/disables via config | ||
| */ | ||
| export const log = (...args) => (Postmate.debug ? console.log(...args) : null); | ||
| /** | ||
| * Takes a URL and returns the origin | ||
| * @param {String} url The full URL being requested | ||
| * @return {String} The URLs origin | ||
| */ | ||
| export const resolveOrigin = (url) => { | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol; | ||
| const host = a.host.length | ||
| ? a.port === '80' || a.port === '443' | ||
| ? a.hostname | ||
| : a.host | ||
| : window.location.host; | ||
| return a.origin || `${protocol}//${host}`; | ||
| }; | ||
| const messageTypes = { | ||
| handshake: 1, | ||
| 'handshake-reply': 1, | ||
| call: 1, | ||
| emit: 1, | ||
| reply: 1, | ||
| request: 1, | ||
| }; | ||
| /** | ||
| * Ensures that a message is safe to interpret | ||
| * @param {Object} message The postmate message being sent | ||
| * @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check | ||
| * @return {Boolean} | ||
| */ | ||
| export const sanitize = (message, allowedOrigin) => { | ||
| if (typeof allowedOrigin === 'string' && message.origin !== allowedOrigin) | ||
| return false; | ||
| if (!message.data) | ||
| return false; | ||
| if (typeof message.data === 'object' && !('postmate' in message.data)) | ||
| return false; | ||
| if (message.data.type !== messageType) | ||
| return false; | ||
| if (!messageTypes[message.data.postmate]) | ||
| return false; | ||
| return true; | ||
| }; | ||
| /** | ||
| * Takes a model, and searches for a value by the property | ||
| * @param {Object} model The dictionary to search against | ||
| * @param {String} property A path within a dictionary (i.e. 'window.location.href') | ||
| * passed to functions in the child model | ||
| * @return {Promise} | ||
| */ | ||
| export const resolveValue = (model, property, args) => { | ||
| const unwrappedContext = typeof model[property] === 'function' ? model[property].apply(null, args) : model[property]; | ||
| return Promise.resolve(unwrappedContext); | ||
| }; | ||
| /** | ||
| * Composes an API to be used by the parent | ||
| * @param {Object} info Information on the consumer | ||
| */ | ||
| export class ParentAPI { | ||
| parent; | ||
| frame; | ||
| child; | ||
| events = {}; | ||
| childOrigin; | ||
| listener; | ||
| constructor(info) { | ||
| this.parent = info.parent; | ||
| this.frame = info.frame; | ||
| this.child = info.child; | ||
| this.childOrigin = info.childOrigin; | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Registering API'); | ||
| log('Parent: Awaiting messages...'); | ||
| } | ||
| this.listener = (e) => { | ||
| if (!sanitize(e, this.childOrigin)) | ||
| return false; | ||
| /** | ||
| * the assignments below ensures that e, data, and value are all defined | ||
| */ | ||
| const { data, name } = ((e || {}).data || {}).value || {}; | ||
| if (e.data.postmate === 'emit') { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log(`Parent: Received event emission: ${name}`); | ||
| } | ||
| if (name in this.events) { | ||
| this.events[name].forEach((callback) => { | ||
| callback.call(this, data); | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| this.parent.addEventListener('message', this.listener, false); | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Awaiting event emissions from Child'); | ||
| } | ||
| } | ||
| get(property, ...args) { | ||
| return new Promise((resolve) => { | ||
| // Extract data from response and kill listeners | ||
| const uid = generateNewMessageId(); | ||
| const transact = (e) => { | ||
| if (e.data.uid === uid && e.data.postmate === 'reply') { | ||
| this.parent.removeEventListener('message', transact, false); | ||
| resolve(e.data.value); | ||
| } | ||
| }; | ||
| // Prepare for response from Child... | ||
| this.parent.addEventListener('message', transact, false); | ||
| // Then ask child for information | ||
| this.child.postMessage({ | ||
| postmate: 'request', | ||
| type: messageType, | ||
| property, | ||
| args, | ||
| uid, | ||
| }, this.childOrigin); | ||
| }); | ||
| } | ||
| call(property, data) { | ||
| // Send information to the child | ||
| this.child.postMessage({ | ||
| postmate: 'call', | ||
| type: messageType, | ||
| property, | ||
| data, | ||
| }, this.childOrigin); | ||
| } | ||
| on(eventName, callback) { | ||
| if (!this.events[eventName]) { | ||
| this.events[eventName] = []; | ||
| } | ||
| this.events[eventName].push(callback); | ||
| } | ||
| destroy() { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Destroying Postmate instance'); | ||
| } | ||
| window.removeEventListener('message', this.listener, false); | ||
| this.frame.parentNode.removeChild(this.frame); | ||
| } | ||
| } | ||
| /** | ||
| * Composes an API to be used by the child | ||
| * @param {Object} info Information on the consumer | ||
| */ | ||
| export class ChildAPI { | ||
| model; | ||
| parent; | ||
| parentOrigin; | ||
| child; | ||
| constructor(info) { | ||
| this.model = info.model; | ||
| this.parent = info.parent; | ||
| this.parentOrigin = info.parentOrigin; | ||
| this.child = info.child; | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Child: Registering API'); | ||
| log('Child: Awaiting messages...'); | ||
| } | ||
| this.child.addEventListener('message', (e) => { | ||
| if (!sanitize(e, this.parentOrigin)) | ||
| return; | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Child: Received request', e.data); | ||
| } | ||
| const { property, uid, data, args } = e.data; | ||
| if (e.data.postmate === 'call') { | ||
| if (property in this.model && | ||
| typeof this.model[property] === 'function') { | ||
| this.model[property](data); | ||
| } | ||
| return; | ||
| } | ||
| // Reply to Parent | ||
| resolveValue(this.model, property, args).then((value) => { | ||
| ; | ||
| e.source.postMessage({ | ||
| property, | ||
| postmate: 'reply', | ||
| type: messageType, | ||
| uid, | ||
| value, | ||
| }, e.origin); | ||
| }); | ||
| }); | ||
| } | ||
| emit(name, data) { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log(`Child: Emitting Event "${name}"`, data); | ||
| } | ||
| this.parent.postMessage({ | ||
| postmate: 'emit', | ||
| type: messageType, | ||
| value: { | ||
| name, | ||
| data, | ||
| }, | ||
| }, this.parentOrigin); | ||
| } | ||
| } | ||
| /** | ||
| * The entry point of the Parent. | ||
| */ | ||
| export class Postmate { | ||
| static debug = false; // eslint-disable-line no-undef | ||
| container; | ||
| parent; | ||
| frame; | ||
| child; | ||
| childOrigin; | ||
| url; | ||
| model; | ||
| static Model; | ||
| /** | ||
| * @param opts | ||
| */ | ||
| constructor(opts) { | ||
| this.container = opts.container; | ||
| this.url = opts.url; | ||
| this.parent = window; | ||
| this.frame = document.createElement('iframe'); | ||
| if (opts.id) | ||
| this.frame.id = opts.id; | ||
| if (opts.name) | ||
| this.frame.name = opts.name; | ||
| this.frame.classList.add.apply(this.frame.classList, opts.classListArray || []); | ||
| this.container.appendChild(this.frame); | ||
| this.child = this.frame.contentWindow; | ||
| this.model = opts.model || {}; | ||
| } | ||
| /** | ||
| * Begins the handshake strategy | ||
| * @param {String} url The URL to send a handshake request to | ||
| * @return {Promise} Promise that resolves when the handshake is complete | ||
| */ | ||
| sendHandshake(url) { | ||
| url = url || this.url; | ||
| const childOrigin = resolveOrigin(url); | ||
| let attempt = 0; | ||
| let responseInterval; | ||
| return new Promise((resolve, reject) => { | ||
| const reply = (e) => { | ||
| if (!sanitize(e, childOrigin)) | ||
| return false; | ||
| if (e.data.postmate === 'handshake-reply') { | ||
| clearInterval(responseInterval); | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Received handshake reply from Child'); | ||
| } | ||
| this.parent.removeEventListener('message', reply, false); | ||
| this.childOrigin = e.origin; | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Saving Child origin', this.childOrigin); | ||
| } | ||
| return resolve(new ParentAPI(this)); | ||
| } | ||
| // Might need to remove since parent might be receiving different messages | ||
| // from different hosts | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Invalid handshake reply'); | ||
| } | ||
| return reject('Failed handshake'); | ||
| }; | ||
| this.parent.addEventListener('message', reply, false); | ||
| const doSend = () => { | ||
| attempt++; | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin }); | ||
| } | ||
| this.child.postMessage({ | ||
| postmate: 'handshake', | ||
| type: messageType, | ||
| model: this.model, | ||
| }, childOrigin); | ||
| if (attempt === maxHandshakeRequests) { | ||
| clearInterval(responseInterval); | ||
| } | ||
| }; | ||
| const loaded = () => { | ||
| doSend(); | ||
| responseInterval = setInterval(doSend, 500); | ||
| }; | ||
| this.frame.addEventListener('load', loaded); | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Parent: Loading frame', { url }); | ||
| } | ||
| this.frame.src = url; | ||
| }); | ||
| } | ||
| destroy() { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Postmate: Destroying Postmate instance'); | ||
| } | ||
| this.frame.parentNode.removeChild(this.frame); | ||
| } | ||
| } | ||
| /** | ||
| * The entry point of the Child | ||
| */ | ||
| export class Model { | ||
| child; | ||
| model; | ||
| parent; | ||
| parentOrigin; | ||
| /** | ||
| * Initializes the child, model, parent, and responds to the Parents handshake | ||
| * @param {Object} model Hash of values, functions, or promises | ||
| * @return {Promise} The Promise that resolves when the handshake has been received | ||
| */ | ||
| constructor(model) { | ||
| this.child = window; | ||
| this.model = model; | ||
| this.parent = this.child.parent; | ||
| } | ||
| /** | ||
| * Responds to a handshake initiated by the Parent | ||
| * @return {Promise} Resolves an object that exposes an API for the Child | ||
| */ | ||
| sendHandshakeReply() { | ||
| return new Promise((resolve, reject) => { | ||
| const shake = (e) => { | ||
| if (!e.data.postmate) { | ||
| return; | ||
| } | ||
| if (e.data.postmate === 'handshake') { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Child: Received handshake from Parent'); | ||
| } | ||
| this.child.removeEventListener('message', shake, false); | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Child: Sending handshake reply to Parent'); | ||
| } | ||
| ; | ||
| e.source.postMessage({ | ||
| postmate: 'handshake-reply', | ||
| type: messageType, | ||
| }, e.origin); | ||
| this.parentOrigin = e.origin; | ||
| // Extend model with the one provided by the parent | ||
| const defaults = e.data.model; | ||
| if (defaults) { | ||
| Object.keys(defaults).forEach((key) => { | ||
| this.model[key] = defaults[key]; | ||
| }); | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Child: Inherited and extended model from Parent'); | ||
| } | ||
| } | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| log('Child: Saving Parent origin', this.parentOrigin); | ||
| } | ||
| return resolve(new ChildAPI(this)); | ||
| } | ||
| return reject('Handshake Reply Failed'); | ||
| }; | ||
| this.child.addEventListener('message', shake, false); | ||
| }); | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
7
-70.83%155860
-13%9
12.5%21
-19.23%1649
-28.89%+ Added
+ Added