@getdraft/plugin 1.13.0 → 1.15.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/src/plugin.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { LOCAL_ENDPOINT, PROD_ENDPOINT } from './routes';
2
2
  import {
3
+ DraftError,
3
4
  EditorMenu,
4
5
  ExitParams,
5
6
  OnInitParams,
@@ -11,6 +12,8 @@ import {
11
12
  UploadImageParams,
12
13
  ViewMode,
13
14
  HandlerParams,
15
+ MessageData,
16
+ ExportAsFileParams,
14
17
  } from './types';
15
18
  import {
16
19
  getImageParamsFromFileUrl,
@@ -28,12 +31,8 @@ const getDeployLink = () => {
28
31
  return LOCAL_ENDPOINT;
29
32
  }
30
33
 
31
- if (
32
- value === 'stage' ||
33
- value?.includes('https://') ||
34
- window.location.pathname.includes('netlify')
35
- ) {
36
- return value?.includes('https://') ? value : `${PROD_ENDPOINT}/stage`;
34
+ if (value?.startsWith('https://')) {
35
+ return value;
37
36
  }
38
37
 
39
38
  const [major, mid] = pkg.version.split('.');
@@ -55,17 +54,19 @@ const verifyConfig = ({ token, pluginId, apiUrl }: PluginConfig) => {
55
54
  }
56
55
  };
57
56
 
58
- const createIframe = () => {
57
+ const createIframe = (parentOrigin: string) => {
59
58
  const iframe = document.createElement('iframe');
60
- iframe.src = getDeployLink();
59
+ const url = new URL(getDeployLink());
60
+ url.searchParams.set('parentOrigin', parentOrigin);
61
+ iframe.src = url.toString();
61
62
  iframe.setAttribute(
62
63
  'style',
63
- 'height:100%;width:100%;min-width:960px;border:0px',
64
+ 'height:100%;width:100%;min-width:960px;border:0px;',
64
65
  );
65
66
  iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
66
67
  iframe.setAttribute(
67
68
  'sandbox',
68
- 'allow-scripts allow-same-origin allow-forms allow-popups',
69
+ 'allow-scripts allow-same-origin allow-forms allow-popups allow-downloads',
69
70
  );
70
71
 
71
72
  return iframe;
@@ -74,6 +75,7 @@ const createIframe = () => {
74
75
  export class PluginInstance {
75
76
  public iframe: HTMLIFrameElement | null = null;
76
77
  private hotkeysAttached = false;
78
+ private iframeOrigin: string | null = null;
77
79
  constructor(public config: PluginConfig) {}
78
80
 
79
81
  private captureHotkeysEnabled() {
@@ -116,35 +118,69 @@ export class PluginInstance {
116
118
  });
117
119
  };
118
120
 
121
+ private static HEARTBEAT_TIMEOUT = 4000;
122
+
119
123
  private promisedIframeMethod<T>(eventName: string) {
120
124
  let timer: ReturnType<typeof setTimeout> | null = null;
125
+ let listener: ((e: MessageEvent<MessageData>) => void) | null = null;
126
+
127
+ const cleanup = () => {
128
+ if (timer) {
129
+ clearTimeout(timer);
130
+ timer = null;
131
+ }
132
+
133
+ if (listener) {
134
+ window.removeEventListener('message', listener, true);
135
+ listener = null;
136
+ }
137
+ };
121
138
 
122
139
  const res = new Promise<T>((resolve, reject) => {
123
- const onEvent = (
124
- e: MessageEvent<{ source: string; event: keyof PluginConfig['on'] }>,
125
- ) => {
126
- const {
127
- data: { source, event, ...data },
128
- } = e;
129
- if (source === 'DraftPlugin' && event === eventName) {
140
+ const resetTimer = () => {
141
+ if (timer) clearTimeout(timer);
142
+ timer = setTimeout(() => {
143
+ cleanup();
144
+ reject(
145
+ new DraftError('timeout_error', `Action "${eventName}" timed out`),
146
+ );
147
+ }, PluginInstance.HEARTBEAT_TIMEOUT);
148
+ };
149
+
150
+ listener = (e: MessageEvent<MessageData>) => {
151
+ if (e.source !== this.iframe?.contentWindow) return;
152
+
153
+ const { data } = e;
154
+ if (data.source !== 'DraftPlugin') return;
155
+
156
+ if (data.event === 'actionPing' && data.action === eventName) {
130
157
  e.stopPropagation();
158
+ resetTimer();
131
159
 
132
- if (timer) {
133
- clearTimeout(timer);
134
- timer = null;
160
+ return;
161
+ }
162
+
163
+ if (data.event === eventName) {
164
+ e.stopPropagation();
165
+ cleanup();
166
+
167
+ const { source: _1, event: _2, ok, code, message, ...rest } = data;
168
+
169
+ if (ok === false) {
170
+ reject(
171
+ new DraftError(
172
+ code || 'unknown_error',
173
+ message || 'Unknown error',
174
+ ),
175
+ );
176
+ } else {
177
+ resolve(rest as T);
135
178
  }
136
- resolve(data as T);
137
- window.removeEventListener('message', onEvent, true);
138
179
  }
139
180
  };
140
181
 
141
- window.addEventListener('message', onEvent, true);
142
-
143
- timer = setTimeout(() => {
144
- window.removeEventListener('message', onEvent, true);
145
- reject('Request timed out');
146
- timer = null;
147
- }, 2000);
182
+ window.addEventListener('message', listener, true);
183
+ resetTimer();
148
184
  });
149
185
 
150
186
  this.postEvent(eventName);
@@ -153,15 +189,17 @@ export class PluginInstance {
153
189
  }
154
190
 
155
191
  private postEvent(event: string, data: object = {}) {
156
- if (this.iframe && this.iframe.contentWindow) {
192
+ if (this.iframe && this.iframe.contentWindow && this.iframeOrigin) {
157
193
  this.iframe.contentWindow.postMessage(
158
194
  {
159
195
  ...data,
160
196
  event,
161
197
  source: 'DraftPlugin',
162
198
  },
163
- '*',
199
+ this.iframeOrigin,
164
200
  );
201
+ } else if (!this.iframeOrigin) {
202
+ throw new Error('Iframe origin is not initialized');
165
203
  }
166
204
  }
167
205
 
@@ -232,6 +270,10 @@ export class PluginInstance {
232
270
  private onMessage = (
233
271
  ev: MessageEvent<{ source: string; event: keyof Handlers }>,
234
272
  ) => {
273
+ if (ev.source !== this.iframe?.contentWindow) {
274
+ return;
275
+ }
276
+
235
277
  const {
236
278
  data: { source, event, ...data },
237
279
  } = ev;
@@ -258,7 +300,7 @@ export class PluginInstance {
258
300
  uid,
259
301
  }: {
260
302
  template: string | TemplateJSON;
261
- uid: string;
303
+ uid: string | number;
262
304
  templateName?: string;
263
305
  }) {
264
306
  const containerEl = document.querySelector(this.config.container);
@@ -266,8 +308,15 @@ export class PluginInstance {
266
308
  if (!containerEl) {
267
309
  throw new Error('Specified container not found');
268
310
  } else {
269
- const onInit = ({ data: { source, event } }: OnInitParams) => {
311
+ const onInit = (msg: OnInitParams) => {
312
+ const {
313
+ data: { source, event },
314
+ } = msg;
270
315
  if (source === 'DraftPlugin' && event === 'loaded') {
316
+ if (msg.source !== this.iframe?.contentWindow) return;
317
+
318
+ this.iframeOrigin = msg.origin;
319
+
271
320
  const {
272
321
  on,
273
322
  container,
@@ -276,6 +325,7 @@ export class PluginInstance {
276
325
  token,
277
326
  apiUrl,
278
327
  pluginId,
328
+ authData,
279
329
  __config,
280
330
  } = this.config;
281
331
 
@@ -289,6 +339,7 @@ export class PluginInstance {
289
339
  autosave,
290
340
  token,
291
341
  pluginId,
342
+ authData,
292
343
  apiUrl,
293
344
  __config,
294
345
  enabledListeners: Object.keys(on),
@@ -302,7 +353,7 @@ export class PluginInstance {
302
353
  }
303
354
  };
304
355
 
305
- this.iframe = createIframe();
356
+ this.iframe = createIframe(window.location.origin);
306
357
  window.addEventListener('message', onInit);
307
358
  containerEl.appendChild(this.iframe);
308
359
 
@@ -369,4 +420,8 @@ export class PluginInstance {
369
420
  exportContent() {
370
421
  return this.promisedIframeMethod<SaveParams>('exportContent');
371
422
  }
423
+
424
+ exportAsFile() {
425
+ return this.promisedIframeMethod<ExportAsFileParams>('exportAsFile');
426
+ }
372
427
  }
package/src/types.ts CHANGED
@@ -2,6 +2,30 @@ export type EventHandler<T> = (data: T) => void;
2
2
 
3
3
  export type AsyncEventHandler<P, R> = (params: P) => Promise<R>;
4
4
 
5
+ export class DraftError extends Error {
6
+ public readonly code: string;
7
+
8
+ constructor(code: string, message: string) {
9
+ super(message);
10
+ this.name = 'DraftError';
11
+ this.code = code;
12
+ }
13
+ }
14
+
15
+ export type MessageData = {
16
+ source: string;
17
+ event: string;
18
+ ok?: boolean;
19
+ code?: string;
20
+ message?: string;
21
+ action?: string;
22
+ };
23
+
24
+ export interface ExportAsFileParams {
25
+ blob: Blob;
26
+ fileName: string;
27
+ }
28
+
5
29
  export interface SaveParams {
6
30
  json: TemplateJSON;
7
31
  html: string;
@@ -34,6 +58,7 @@ export interface ReadyParams extends SaveParams {
34
58
  export interface ErrorParams {
35
59
  message: string;
36
60
  code?: string;
61
+ details?: unknown;
37
62
  }
38
63
 
39
64
  export interface ExitParams extends SaveParams {}
@@ -239,6 +264,10 @@ export interface PluginConfig {
239
264
  interval: number;
240
265
  };
241
266
  token: string;
267
+ authData?: {
268
+ orgId: number;
269
+ projectId?: number;
270
+ };
242
271
  pluginId: string;
243
272
  apiUrl: string;
244
273
  disableHotkeysPassing?: boolean;
package/vite.config.ts CHANGED
@@ -9,8 +9,8 @@ export default defineConfig({
9
9
  sourcemap: true,
10
10
  lib: {
11
11
  entry: path.resolve(__dirname, 'src/index.ts'),
12
- name: 'seplugin',
13
- formats: ['es', 'cjs'],
12
+ name: 'DraftPlugin',
13
+ formats: ['es', 'cjs', 'iife'],
14
14
  fileName: (format) => `index.${format}.js`,
15
15
  },
16
16
  },
@@ -1,22 +0,0 @@
1
-
2
- > @letty-email/plugin@1.12.1 build /Users/nipanasovich/Documents/Work/draft/packages/plugin
3
- > vite build
4
-
5
- The CJS build of Vite's Node API is deprecated. See https://vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
6
- vite v5.4.20 building for production...
7
- transforming...
8
- ✓ 6 modules transformed.
9
- rendering chunks...
10
-
11
- [vite:dts] Start generate declaration files...
12
- computing gzip size...
13
- dist/index.es.js 7.69 kB │ gzip: 2.73 kB │ map: 21.92 kB
14
-
15
- [vite:dts] The resolved path of type entry is not ending with '.d.ts'.
16
-
17
- [vite:dts] Outside emitted: /Users/nipanasovich/Documents/Work/draft/packages/plugin/src.d.ts
18
- [vite:dts] Declaration files built in 8574ms.
19
-
20
- Entry module "src/index.ts" is using named and default exports together. Consumers of your bundle will have to use `seplugin.default` to access the default export, which may not be what you want. Use `output.exports: "named"` to disable this warning.
21
- dist/index.cjs.js 5.69 kB │ gzip: 2.41 kB │ map: 21.10 kB
22
- ✓ built in 9.44s
@@ -1,4 +0,0 @@
1
-
2
- > @getdraft/plugin@1.13.0-beta.0 eslint /Users/nipanasovich/Documents/Work/draft/packages/plugin
3
- > npx eslint src "--quiet" "--fix"
4
-