@silexlabs/silex 3.0.2 → 3.0.4

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.
@@ -18,12 +18,19 @@
18
18
  import { ConnectorId, WebsiteId, WebsiteData, ConnectorUser, ConnectorType, ApiError } from '../../types'
19
19
  import { websiteLoad, websiteSave } from '../api'
20
20
  import { cmdLogin, eventLoggedIn, eventLoggedOut, getCurrentUser, updateUser } from './LoginDialog'
21
- import { addTempDataToPages, addTempDataToAssetUrl, addTempDataToStyles, removeTempDataFromAssetUrl, removeTempDataFromPages, removeTempDataFromStyles } from '../assetUrl'
22
- import { PublicationStatus } from './PublicationManager'
21
+ import { addTempDataToAssetUrl, addTempDataToStyles, removeTempDataFromAssetUrl, removeTempDataFromStyles } from '../assetUrl'
22
+ import { PublicationStatus, PublishableEditor } from './PublicationManager'
23
23
  import { ClientEvent } from '../events'
24
+ import { ProjectData } from 'grapesjs'
24
25
 
25
26
  export const cmdPauseAutoSave = 'pause-auto-save'
26
27
 
28
+ // Wait for the next frame to avoid blocking the main thread
29
+ function nextFrame(): Promise<void> {
30
+ const fn = window.requestIdleCallback ?? window.requestAnimationFrame
31
+ return new Promise(resolve => fn(() => resolve()))
32
+ }
33
+
27
34
  // Mechanism to keep only the last save during publication
28
35
  let lastPendingSaving: WebsiteData = null
29
36
 
@@ -31,7 +38,7 @@ let lastPendingSaving: WebsiteData = null
31
38
  // Note: there is already such a mechanism in grapesjs but it fails when the save is delayed by the publication
32
39
  let isSaving = false
33
40
 
34
- export const storagePlugin = (editor) => {
41
+ export const storagePlugin = (editor: PublishableEditor) => {
35
42
  // Add useful commands
36
43
  editor.Commands.add(cmdPauseAutoSave, {
37
44
  run: () => {
@@ -46,25 +53,24 @@ export const storagePlugin = (editor) => {
46
53
  })
47
54
  // Add the storage connector
48
55
  editor.Storage.add('connector', {
49
- async load(options: { id: WebsiteId, connectorId: ConnectorId }): Promise<WebsiteData> {
56
+ async load(options: { id: WebsiteId, connectorId: ConnectorId }): Promise<ProjectData> {
50
57
  try {
51
58
  const user: ConnectorUser = await getCurrentUser(editor) ?? await updateUser(editor, ConnectorType.STORAGE, options.connectorId)
52
59
  if (!user) throw new ApiError('Not logged in', 401)
53
60
  const data = await websiteLoad({ websiteId: options.id, connectorId: user.storage.connectorId }) as WebsiteData
54
61
  if (data.assets) data.assets = addTempDataToAssetUrl(data.assets, options.id, user.storage.connectorId)
55
- if (data.pages) data.pages = addTempDataToPages(data.pages, options.id, user.storage.connectorId)
56
62
  if (data.styles) data.styles = addTempDataToStyles(data.styles, options.id, user.storage.connectorId)
57
- return data
58
-
63
+ //setTimeout(() => progressiveLoadPages(editor, data))
64
+ await progressiveLoadPages(editor, data)
65
+ return {}
59
66
  } catch (err) {
60
67
  editor.UndoManager.clear()
61
68
  if (err.httpStatusCode === 401) {
62
69
  editor.once(eventLoggedIn, async () => {
63
70
  try {
64
71
  editor.runCommand(cmdPauseAutoSave)
65
- const data = await editor.Storage.load(options)
72
+ await editor.Storage.load(options)
66
73
  editor.once('canvas:frame:load', () => editor.stopCommand(cmdPauseAutoSave))
67
- editor.loadProjectData(data)
68
74
  } catch (err) {
69
75
  console.error('connectorPlugin load error', err)
70
76
  throw err
@@ -94,7 +100,7 @@ export const storagePlugin = (editor) => {
94
100
  },
95
101
 
96
102
  async doStore(data, options) {
97
- if (editor.PublicationManager?.status === PublicationStatus.STATUS_PENDING) {
103
+ if (editor.PublicationManager.status === PublicationStatus.STATUS_PENDING) {
98
104
  // Publication is pending, save is delayed
99
105
  return new Promise((resolve) => {
100
106
  editor.once(ClientEvent.PUBLISH_END, () => {
@@ -115,7 +121,6 @@ export const storagePlugin = (editor) => {
115
121
  const user = await getCurrentUser(editor)
116
122
  if (user) {
117
123
  data.assets = removeTempDataFromAssetUrl(data.assets)
118
- data.pages = removeTempDataFromPages(data.pages)
119
124
  data.styles = removeTempDataFromStyles(data.styles)
120
125
  await websiteSave({ websiteId: options.id, connectorId: user.storage.connectorId, data })
121
126
  isSaving = false
@@ -123,7 +128,7 @@ export const storagePlugin = (editor) => {
123
128
  // Not logged in save is delayed
124
129
  editor.once(eventLoggedIn, () => {
125
130
  isSaving = false
126
- return editor.Storage.store()
131
+ return editor.Storage.store(data, options)
127
132
  })
128
133
  editor.Commands.run(cmdLogin)
129
134
  }
@@ -185,3 +190,24 @@ export const storagePlugin = (editor) => {
185
190
  }
186
191
  }
187
192
  }
193
+
194
+ async function progressiveLoadPages(editor: PublishableEditor, data: ProjectData) {
195
+ editor.Pages.getAll().forEach(page => editor.Pages.remove(page))
196
+ let i = 0
197
+ for (const page of data.pages) {
198
+ document.querySelector('.silex-loader').innerHTML = `Loading page ${++i} / ${data.pages.length}`
199
+ await nextFrame()
200
+ const newPage = editor.Pages.add({
201
+ ...page,
202
+ })
203
+ }
204
+
205
+ // Charger les styles, assets, etc. après les pages
206
+ document.querySelector('.silex-loader').innerHTML = 'Loading styles and assets'
207
+ if (data.styles) editor.setStyle(data.styles)
208
+ if (data.assets) editor.AssetManager.add(data.assets)
209
+
210
+ // Sélectionner la première page
211
+ const firstPage = editor.Pages.getAll()[0]
212
+ if (firstPage) editor.Pages.select(firstPage)
213
+ }
@@ -21,11 +21,13 @@
21
21
  *
22
22
  */
23
23
 
24
+ import { Component, ComponentView } from 'grapesjs'
24
25
  import { DEV_MESSAGE } from '../constants'
25
26
  import { ClientConfig } from './config'
26
27
  import { ClientEvent } from './events'
27
28
  import { initEditor, getEditor } from './grapesjs/index'
28
29
  import { cmdPauseAutoSave } from './grapesjs/storage'
30
+ import { displayedToStored, storedToDisplayed } from './assetUrl'
29
31
 
30
32
  // Expose API to calling app as window.silex
31
33
  export * from './expose'
@@ -83,6 +85,42 @@ export async function start(options = {}): Promise<void> {
83
85
  // Add default plugins
84
86
  await config.addDefaultPlugins()
85
87
 
88
+ const domc = editor.DomComponents
89
+ const imgType = domc.getType('image')
90
+ domc.addType('image', {
91
+ ...imgType,
92
+ view: {
93
+ ...imgType.view,
94
+ onRender() {
95
+ const view = this as ComponentView
96
+ imgType.view.onRender && imgType.view.onRender.call(this) // call original
97
+ const el = view.el
98
+ const elImg = view.el as HTMLImageElement
99
+ const src = this.model.getAttributes().src
100
+ if (src && !src.startsWith('http')) {
101
+ elImg.src = storedToDisplayed(src, config.websiteId, config.storageId)
102
+ }
103
+ },
104
+ },
105
+ model: {
106
+ ...imgType.model,
107
+ init() {
108
+ const model = this as Component
109
+ imgType.model.init && imgType.model.init.call(this) // call original
110
+ // Handle changes by the AssetManager
111
+ model.on('change', () => {
112
+ if(model.get('src')) {
113
+ const newSrc = displayedToStored(model.get('src'))
114
+ if(newSrc !== model.get('src')) {
115
+ model.set('src', newSrc)
116
+ model.view.render()
117
+ }
118
+ }
119
+ })
120
+ },
121
+ },
122
+ })
123
+
86
124
  // Load the site
87
125
  try {
88
126
  editor.runCommand(cmdPauseAutoSave)
@@ -139,26 +139,26 @@ describe('publication-transformers', () => {
139
139
  })
140
140
 
141
141
  it('should transform a style rule with background image', () => {
142
- expect(transformBgImage(editor, { 'background-image': 'url(test.png)' })).toEqual({ 'background-image': 'url(test.png)' })
142
+ expect(transformBgImage(editor, { 'background-image': 'url(test.png)' })).toEqual({ 'background-image': 'url("test.png")' })
143
143
  const transformPermalink = transformer.transformPermalink as Mock
144
144
  const transformedSrc = 'transformed.png'
145
145
  const transformedSrc2 = 'transformed2.png'
146
146
  transformPermalink.mockImplementation((url, type) => (url as string).replace('test.png', transformedSrc).replace('test2.png', transformedSrc2))
147
- expect(transformBgImage(editor, { 'background-image': 'url(test.png)' })).toEqual({ 'background-image': `url(${transformedSrc})` })
148
- expect(transformBgImage(editor, { 'background-image': 'url("test.png")' })).toEqual({ 'background-image': `url(${transformedSrc})` })
147
+ expect(transformBgImage(editor, { 'background-image': 'url(test.png)' })).toEqual({ 'background-image': `url("${transformedSrc}")` })
148
+ expect(transformBgImage(editor, { 'background-image': 'url("test.png")' })).toEqual({ 'background-image': `url("${transformedSrc}")` })
149
149
  expect(transformBgImage(editor, {
150
150
  'background-image': 'linear-gradient(to right, rgb(30 75 115 / 100%), url(test.png), linear-gradient(to left, rgb(30 75 115 / 10%), url(test2.png), rgb(255 255 255 / 0%));',
151
151
  })).toEqual({
152
- 'background-image': `linear-gradient(to right, rgb(30 75 115 / 100%), url(${transformedSrc}), linear-gradient(to left, rgb(30 75 115 / 10%), url(${transformedSrc2}), rgb(255 255 255 / 0%));`,
152
+ 'background-image': `linear-gradient(to right, rgb(30 75 115 / 100%), url("${transformedSrc}"), linear-gradient(to left, rgb(30 75 115 / 10%), url("${transformedSrc2}"), rgb(255 255 255 / 0%));`,
153
153
  })
154
154
  })
155
155
 
156
156
  it('should transform permalinks of background images in inline css', () => {
157
157
  const el = document.createElement('div')
158
158
  const [comp] = editor.addComponents(el)
159
- comp.setStyle({ 'background-image': 'url(test.png)' })
159
+ comp.setStyle({ 'background-image': 'url("test.png")' })
160
160
  //editor.addComponents(`
161
- // <div style="color: black; background-image: url(test.png);"></div>
161
+ // <div style="color: black; background-image: url("test.png");"></div>
162
162
  //`)
163
163
  const transformPermalink = transformer.transformPermalink as Mock
164
164
  const transformedSrc = 'transformed.png'
@@ -168,7 +168,7 @@ describe('publication-transformers', () => {
168
168
  expect(transformPermalink).toBeCalledTimes(1)
169
169
  expect(transformPermalink.mock.calls[0][0]).toBe('test.png')
170
170
  expect(transformPermalink.mock.calls[0][1]).toBe('asset')
171
- expect(css).toContain('url(test.png)')
171
+ expect(css).toContain('url("test.png")')
172
172
  })
173
173
  it('should transform permalinks of background images in styles', () => {
174
174
  editor.addComponents(`
@@ -176,7 +176,7 @@ describe('publication-transformers', () => {
176
176
  `)
177
177
  editor.addStyle(`
178
178
  .test {
179
- background-image: url(test.png);
179
+ background-image: url("test.png");
180
180
  }
181
181
  `)
182
182
  const transformPermalink = transformer.transformPermalink as Mock
@@ -187,7 +187,7 @@ describe('publication-transformers', () => {
187
187
  expect(transformPermalink).toBeCalledTimes(1)
188
188
  expect(transformPermalink.mock.calls[0][0]).toBe('test.png')
189
189
  expect(transformPermalink.mock.calls[0][1]).toBe('asset')
190
- expect(css).toContain(`url(${transformedSrc})`)
190
+ expect(css).toContain(`url("${transformedSrc}")`)
191
191
  })
192
192
  it('should transform links to pages to match permalinks ', () => {
193
193
  editor.addComponents(`
@@ -188,7 +188,7 @@ function doTransformPermalink(editor: Editor, cssValue: string): string {
188
188
  // Transform URLs
189
189
  const newUrl = transformPermalink(editor, cleanUrl, ClientSideFileType.ASSET, Initiator.CSS)
190
190
  // Return the new URL with url keyword
191
- return `url(${newUrl})`
191
+ return `url("${newUrl}")`
192
192
  })
193
193
  }
194
194
 
@@ -198,7 +198,18 @@ function doTransformPermalink(editor: Editor, cssValue: string): string {
198
198
  export function transformBgImage(editor: Editor, style: StyleProps): StyleProps {
199
199
  const cssValue = style['background-image']
200
200
  if (cssValue) {
201
- const newCssValue = typeof cssValue === 'string' ? doTransformPermalink(editor, cssValue) : cssValue.map(value => doTransformPermalink(editor, value))
201
+ let newCssValue
202
+ if (Array.isArray(cssValue)) {
203
+ newCssValue = cssValue
204
+ .map(value => doTransformPermalink(editor, value))
205
+ .join(', ')
206
+ } else if (typeof cssValue === 'string') {
207
+ newCssValue = doTransformPermalink(editor, cssValue)
208
+ } else if (newCssValue === cssValue) {
209
+ // No change
210
+ return style
211
+ }
212
+ // Set the new value
202
213
  return {
203
214
  ...style,
204
215
  'background-image': newCssValue,
@@ -22,6 +22,7 @@ import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, Website
22
22
  import fetch from 'node-fetch'
23
23
  import crypto, { createHash } from 'crypto'
24
24
  import { join } from 'path'
25
+ import { Agent } from 'https'
25
26
 
26
27
  /**
27
28
  * Gitlab connector
@@ -247,6 +248,7 @@ export default class GitlabConnector implements StorageConnector {
247
248
  // Call the API
248
249
  const url = `${this.options.domain}/api/v4/projects/${websiteId}/repository/files/${safePath}?ref=${this.options.branch}&access_token=${this.getSessionToken(session).token?.access_token}`
249
250
  const response = await fetch(url, {
251
+ agent: this.getAgent(),
250
252
  method: 'GET',
251
253
  headers: {
252
254
  'Content-Type': 'application/json',
@@ -305,10 +307,12 @@ export default class GitlabConnector implements StorageConnector {
305
307
  let response
306
308
  try {
307
309
  response = await fetch(url, body && method !== 'GET' ? {
310
+ agent: this.getAgent(),
308
311
  method,
309
312
  headers,
310
313
  body: body ? JSON.stringify(body) : undefined
311
314
  } : {
315
+ agent: this.getAgent(),
312
316
  method,
313
317
  headers,
314
318
  })
@@ -332,6 +336,7 @@ export default class GitlabConnector implements StorageConnector {
332
336
  client_secret: this.options.clientSecret,
333
337
  }
334
338
  const response = await fetch(this.options.domain + '/oauth/token', {
339
+ agent: this.getAgent(),
335
340
  method: 'POST',
336
341
  headers: {
337
342
  'Content-Type': 'application/json',
@@ -399,6 +404,16 @@ export default class GitlabConnector implements StorageConnector {
399
404
  return `${this.config.url}${API_PATH}${API_CONNECTOR_PATH}${API_CONNECTOR_LOGIN_CALLBACK}?${params}`
400
405
  }
401
406
 
407
+ // Force IPv4 when running locally
408
+ private getAgent(): Agent | undefined {
409
+ if (this.config.url.startsWith('http://localhost')) {
410
+ return new Agent({
411
+ family: 4,
412
+ })
413
+ }
414
+ return undefined
415
+ }
416
+
402
417
  /**
403
418
  * Get the OAuth URL to redirect the user to
404
419
  * The URL should look like
@@ -471,6 +486,7 @@ export default class GitlabConnector implements StorageConnector {
471
486
  }
472
487
 
473
488
  const response = await fetch(this.options.domain + '/oauth/token', {
489
+ agent: this.getAgent(),
474
490
  method: 'POST',
475
491
  headers: {
476
492
  'Content-Type': 'application/json',
@@ -489,8 +505,8 @@ export default class GitlabConnector implements StorageConnector {
489
505
 
490
506
  // Store the token in the session
491
507
  this.setSessionToken(session, {
492
- token,
493
508
  ...this.getSessionToken(session),
509
+ token,
494
510
  })
495
511
 
496
512
  // We need to get the user ID for listWebsites
@@ -49,17 +49,24 @@ export default function(config: ServerConfig) {
49
49
  * Method to validate HTTP status codes
50
50
  * Returns a suitable error code if it is not valid
51
51
  */
52
- function validateStatus(status: number, _default = 500): number {
52
+ function validateStatus(status: number | string, _default = 500): number {
53
53
  if(!status) {
54
54
  console.warn(`Status code is undefined, returning default ${_default}`)
55
55
  return _default
56
56
  }
57
- // Make sure it is a string
58
- status = parseInt(status.toString())
57
+ switch (status) {
58
+ case 'ETIMEDOUT':
59
+ case 'ECONNREFUSED':
60
+ case 'ECONNRESET':
61
+ console.warn(`Connection error ${status}, returning default ${_default}`)
62
+ return _default
63
+ }
64
+ // Make sure it is a number
65
+ const statusNum = parseInt(status.toString())
59
66
  // Check the status code
60
- if (status >= 100 && status < 600) return status
67
+ if (statusNum >= 100 && statusNum < 600) return statusNum
61
68
  // Invalid status code
62
- console.warn(`Invalid status code ${status}, returning default ${_default}}`)
69
+ console.warn(`Invalid status code ${statusNum} (${status.toString()}, ${status}), returning default ${_default}`)
63
70
  return _default
64
71
  }
65
72
 
@@ -241,7 +248,7 @@ async function routeLogin(req: Request, res: Response) {
241
248
  async function routeLoginSuccess(req: Request, res: Response) {
242
249
  try {
243
250
  const query = req.query as ApiConnectorLoginCbkQuery
244
- if(query.error) throw new Error(query.error)
251
+ if (query.error) throw new Error(query.error)
245
252
 
246
253
  const body = req.body as ApiConnectorLoginCbkBody
247
254
  const session = requiredParam(req['session'], 'Session object')
@@ -265,9 +272,10 @@ async function routeLoginSuccess(req: Request, res: Response) {
265
272
  // End the auth flow
266
273
  res.send(getEndAuthHtml('Logged in', false, connectorId, type, connector.getOptions(body)))
267
274
  } catch (error) {
275
+ console.error('Error in the login callback', error, error?.code, error?.httpStatusCode)
268
276
  res
269
277
  .status(validateStatus(error?.code ?? error?.httpStatusCode, 500))
270
- .send(getEndAuthHtml(error.message, true, req.query.connectorId as ConnectorId, req.query.type as ConnectorType))
278
+ .send(getEndAuthHtml(`${error?.message} ${error?.code ?? error?.httpStatusCode}`, true, req.query.connectorId as ConnectorId, req.query.type as ConnectorType))
271
279
  }
272
280
  }
273
281