@silexlabs/silex 3.3.1-4 → 3.3.1-6
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/README.md +27 -2
- package/dist/client/index.html +2 -2
- package/dist/client/js/main.js +214 -214
- package/dist/client/js/main.js.LICENSE.txt +1 -14
- package/dist/plugins/server/plugins/server/FtpConnector.js +53 -8
- package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
- package/dist/plugins/server/plugins/server/GitlabConnector.js +21 -66
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/plugins/server/server/connectors/FsStorage.js +50 -4
- package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
- package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +20 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js +127 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -0
- package/dist/server/page.js +14 -0
- package/dist/server/server/connectors/FsStorage.js +40 -4
- package/dist/server/server/utils/websiteDataSerialization.js +85 -0
- package/package.json +12 -15
- package/src/ts/client/grapesjs/PublicationUi.ts +1 -1
- package/src/ts/client/grapesjs/index.ts +9 -11
- package/src/ts/client/grapesjs/storage.ts +6 -9
- package/src/ts/client/index.ts +0 -2
- package/src/ts/plugins/server/FtpConnector.test.ts +2 -1
- package/src/ts/plugins/server/FtpConnector.ts +61 -9
- package/src/ts/plugins/server/GitlabConnector.ts +22 -80
- package/src/ts/server/connectors/FsStorage.test.ts +6 -2
- package/src/ts/server/connectors/FsStorage.ts +60 -5
- package/src/ts/server/utils/websiteDataSerialization.test.ts +59 -0
- package/src/ts/server/utils/websiteDataSerialization.ts +143 -0
- package/src/ts/client/grapesjs/dynamic-plugins.ts +0 -63
- package/src/ts/client/grapesjs/plugin-options.ts +0 -243
|
@@ -27,6 +27,7 @@ import { getPageSlug } from '../../page'
|
|
|
27
27
|
import e from 'express'
|
|
28
28
|
import { fork } from 'child_process'
|
|
29
29
|
import { Page } from 'grapesjs'
|
|
30
|
+
import { stringify, split, merge } from '../../server/utils/websiteDataSerialization'
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Gitlab connector
|
|
@@ -121,16 +122,6 @@ interface GitlabFetchCommits {
|
|
|
121
122
|
since: string
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
interface GitlabPage {
|
|
125
|
-
id: string
|
|
126
|
-
name: string
|
|
127
|
-
isFile: true
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
interface GitlabWebsiteData {
|
|
131
|
-
fileFormatVersion: string
|
|
132
|
-
pages: GitlabPage[]
|
|
133
|
-
}
|
|
134
125
|
|
|
135
126
|
// interface MetaRepoFileContent {
|
|
136
127
|
// websites: {
|
|
@@ -221,9 +212,6 @@ function sanitizeGitlabPath(name: string): string {
|
|
|
221
212
|
.toLowerCase()
|
|
222
213
|
}
|
|
223
214
|
|
|
224
|
-
function getPageName(page: Page | GitlabPage) {
|
|
225
|
-
return (page as any).getName ? (page as Page).getName() : (page as GitlabPage).name
|
|
226
|
-
}
|
|
227
215
|
|
|
228
216
|
export default class GitlabConnector implements StorageConnector {
|
|
229
217
|
connectorId = 'gitlab'
|
|
@@ -739,40 +727,15 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
739
727
|
*/
|
|
740
728
|
async readWebsite(session: GitlabSession, websiteId: string): Promise<WebsiteData> {
|
|
741
729
|
const websiteDataBuf = await this.downloadRawFile(session, websiteId, WEBSITE_DATA_FILE)
|
|
742
|
-
const
|
|
743
|
-
const { fileFormatVersion, ...websiteData } = websiteDataOb
|
|
730
|
+
const websiteDataContent = websiteDataBuf.toString('utf8')
|
|
744
731
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
732
|
+
// Use the common merge function to reconstruct website data
|
|
733
|
+
const pageLoader = async (pagePath: string): Promise<string> => {
|
|
734
|
+
const pageBuffer = await this.downloadRawFile(session, websiteId, pagePath)
|
|
735
|
+
return pageBuffer.toString('utf8')
|
|
749
736
|
}
|
|
750
737
|
|
|
751
|
-
|
|
752
|
-
// Let grapesjs create the pages in the frontend
|
|
753
|
-
if (!websiteData.pages) {
|
|
754
|
-
return websiteData as WebsiteData
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Load each page in parallel
|
|
758
|
-
const pages = await Promise.all(websiteData.pages.map(async (page: GitlabPage | Page) => {
|
|
759
|
-
if ((page as GitlabPage).isFile) {
|
|
760
|
-
const name = getPageName(page)
|
|
761
|
-
const slug = getPageSlug(name)
|
|
762
|
-
const fileName = (`${slug}-${page.id}`)
|
|
763
|
-
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}.json`
|
|
764
|
-
const pageContent = await this.downloadRawFile(session, websiteId, filePath)
|
|
765
|
-
const res = JSON.parse(pageContent.toString('utf8')) as Page
|
|
766
|
-
return res
|
|
767
|
-
}
|
|
768
|
-
return page as Page
|
|
769
|
-
}))
|
|
770
|
-
|
|
771
|
-
// Read each page file if needed
|
|
772
|
-
return {
|
|
773
|
-
...websiteData,
|
|
774
|
-
pages,
|
|
775
|
-
} as WebsiteData
|
|
738
|
+
return await merge(websiteDataContent, pageLoader)
|
|
776
739
|
}
|
|
777
740
|
|
|
778
741
|
/**
|
|
@@ -787,7 +750,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
787
750
|
name: this.options.repoPrefix + websiteMeta.name,
|
|
788
751
|
}
|
|
789
752
|
}) as any
|
|
790
|
-
await this.createFile(session, project.id, WEBSITE_DATA_FILE,
|
|
753
|
+
await this.createFile(session, project.id, WEBSITE_DATA_FILE, stringify({} as WebsiteData))
|
|
791
754
|
//await this.createFile(session, project.id, WEBSITE_META_DATA_FILE, JSON.stringify(websiteMeta))
|
|
792
755
|
//await this.updateWebsite(session, project.id, {} as WebsiteData)
|
|
793
756
|
//await this.setWebsiteMeta(session, project.id, websiteMeta)
|
|
@@ -812,17 +775,19 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
812
775
|
})
|
|
813
776
|
|
|
814
777
|
// **
|
|
815
|
-
//
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
778
|
+
// Use the common split function to create files
|
|
779
|
+
const filesToWrite = split(websiteData)
|
|
780
|
+
|
|
781
|
+
// **
|
|
782
|
+
// Process each file to create/update
|
|
783
|
+
for (const file of filesToWrite) {
|
|
784
|
+
const filePath = file.path
|
|
785
|
+
const content = file.content
|
|
821
786
|
const newSha = computeGitBlobSha(content, false)
|
|
822
787
|
|
|
823
|
-
const existingSha = existingFiles.get(filePath)
|
|
788
|
+
const existingSha = existingFiles.get(filePath) || (filePath === WEBSITE_DATA_FILE ? 'always-update' : undefined)
|
|
824
789
|
|
|
825
|
-
if (existingSha) {
|
|
790
|
+
if (existingSha && existingSha !== 'always-update') {
|
|
826
791
|
if (existingSha !== newSha) {
|
|
827
792
|
batchActions.push({
|
|
828
793
|
action: 'update',
|
|
@@ -832,26 +797,18 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
832
797
|
} // else: skip unchanged file
|
|
833
798
|
} else {
|
|
834
799
|
batchActions.push({
|
|
835
|
-
action: 'create',
|
|
800
|
+
action: file.path === WEBSITE_DATA_FILE ? 'update' : 'create',
|
|
836
801
|
file_path: filePath,
|
|
837
802
|
content,
|
|
838
803
|
})
|
|
839
804
|
}
|
|
840
|
-
|
|
841
|
-
return {
|
|
842
|
-
name: getPageName(page),
|
|
843
|
-
id: page.id,
|
|
844
|
-
isFile: true,
|
|
845
|
-
}
|
|
846
|
-
})
|
|
805
|
+
}
|
|
847
806
|
|
|
848
807
|
// **
|
|
849
808
|
// Delete pages that are not in the new website data
|
|
809
|
+
const newPageFiles = new Set(filesToWrite.filter(f => f.path.startsWith(WEBSITE_PAGES_FOLDER)).map(f => f.path))
|
|
850
810
|
for (const filePath of existingFiles.keys()) {
|
|
851
|
-
|
|
852
|
-
const pageId = pageName.split('-').pop()
|
|
853
|
-
const page = websiteData.pages.find((p: Page) => p.id === pageId)
|
|
854
|
-
if (!page) {
|
|
811
|
+
if (!newPageFiles.has(filePath)) {
|
|
855
812
|
batchActions.push({
|
|
856
813
|
action: 'delete',
|
|
857
814
|
file_path: filePath,
|
|
@@ -859,21 +816,6 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
859
816
|
}
|
|
860
817
|
}
|
|
861
818
|
|
|
862
|
-
// **
|
|
863
|
-
// Add the main website data file
|
|
864
|
-
const websiteDataWithGitlabPages = {
|
|
865
|
-
...websiteData,
|
|
866
|
-
fileFormatVersion: WEBSITE_DATA_FILE_FORMAT_VERSION,
|
|
867
|
-
pages,
|
|
868
|
-
} as GitlabWebsiteData
|
|
869
|
-
|
|
870
|
-
const websiteJsonContent = JSON.stringify(websiteDataWithGitlabPages)
|
|
871
|
-
batchActions.push({
|
|
872
|
-
action: 'update',
|
|
873
|
-
file_path: WEBSITE_DATA_FILE,
|
|
874
|
-
content: websiteJsonContent,
|
|
875
|
-
})
|
|
876
|
-
|
|
877
819
|
// **
|
|
878
820
|
// Perform a batch commit
|
|
879
821
|
return this.callApi({
|
|
@@ -3,6 +3,7 @@ import { ServerConfig } from '../config'
|
|
|
3
3
|
import { FsStorage } from './FsStorage'
|
|
4
4
|
import { readFileSync, rmdirSync, statSync } from 'fs'
|
|
5
5
|
import { join } from 'path'
|
|
6
|
+
import { WEBSITE_DATA_FILE } from '../../constants'
|
|
6
7
|
|
|
7
8
|
const storageRootPath = '/tmp/silex-tests'
|
|
8
9
|
const assetsFolder = 'assets'
|
|
@@ -45,7 +46,10 @@ describe('FsStorage website', () => {
|
|
|
45
46
|
})
|
|
46
47
|
const id = await connector.createWebsite(dummySession, { name: 'dummy name', connectorUserSettings: {} })
|
|
47
48
|
expect(id).toBeDefined()
|
|
48
|
-
expect(() => statSync(join(storageRootPath, id,
|
|
49
|
-
|
|
49
|
+
expect(() => statSync(join(storageRootPath, id, WEBSITE_DATA_FILE))).not.toThrow()
|
|
50
|
+
const content = readFileSync(join(storageRootPath, id, WEBSITE_DATA_FILE), 'utf8')
|
|
51
|
+
const parsed = JSON.parse(content)
|
|
52
|
+
expect(parsed).toHaveProperty('fileFormatVersion', '1.0.0')
|
|
53
|
+
expect(parsed).toHaveProperty('pages', [])
|
|
50
54
|
})
|
|
51
55
|
})
|
|
@@ -23,10 +23,11 @@ import { ConnectorUser, WebsiteMeta, JobStatus, WebsiteId, ConnectorType, Websit
|
|
|
23
23
|
import { userInfo } from 'os'
|
|
24
24
|
import { requiredParam } from '../utils/validation'
|
|
25
25
|
import { ServerConfig } from '../config'
|
|
26
|
-
import { DEFAULT_WEBSITE_ID, WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE } from '../../constants'
|
|
26
|
+
import { DEFAULT_WEBSITE_ID, WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE, WEBSITE_PAGES_FOLDER } from '../../constants'
|
|
27
27
|
import { Readable } from 'stream'
|
|
28
28
|
import { v4 as uuid } from 'uuid'
|
|
29
29
|
import { fileURLToPath } from 'url'
|
|
30
|
+
import { stringify, split, merge } from '../utils/websiteDataSerialization'
|
|
30
31
|
|
|
31
32
|
// Variables needed for jest tests
|
|
32
33
|
if(!globalThis.__dirname) {
|
|
@@ -145,7 +146,7 @@ export class FsStorage implements StorageConnector<FsSession> {
|
|
|
145
146
|
|
|
146
147
|
async setWebsiteMeta(session: any, id: string, data: WebsiteMetaFileContent): Promise<void> {
|
|
147
148
|
const websiteId = requiredParam<WebsiteId>(id, 'website id')
|
|
148
|
-
const content =
|
|
149
|
+
const content = stringify(data)
|
|
149
150
|
const path = join(this.options.path, id, WEBSITE_META_DATA_FILE)
|
|
150
151
|
await fs.writeFile(path, content)
|
|
151
152
|
}
|
|
@@ -183,14 +184,68 @@ export class FsStorage implements StorageConnector<FsSession> {
|
|
|
183
184
|
async readWebsite(session: FsSession, websiteId: WebsiteId): Promise<WebsiteData> {
|
|
184
185
|
const id = requiredParam<WebsiteId>(websiteId, 'website id')
|
|
185
186
|
const path = join(this.options.path, id, WEBSITE_DATA_FILE)
|
|
187
|
+
|
|
186
188
|
const content = await fs.readFile(path)
|
|
187
|
-
|
|
189
|
+
const websiteDataContent = content.toString()
|
|
190
|
+
|
|
191
|
+
// Check if this is the new split format
|
|
192
|
+
const parsedData = JSON.parse(websiteDataContent)
|
|
193
|
+
if (parsedData.fileFormatVersion === '1.0.0') {
|
|
194
|
+
// Use the merge function to reconstruct website data
|
|
195
|
+
const pageLoader = async (pagePath: string): Promise<string> => {
|
|
196
|
+
const fullPath = join(this.options.path, id, pagePath)
|
|
197
|
+
const pageContent = await fs.readFile(fullPath)
|
|
198
|
+
return pageContent.toString()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return await merge(websiteDataContent, pageLoader)
|
|
202
|
+
} else {
|
|
203
|
+
// Legacy format, return as is
|
|
204
|
+
return parsedData
|
|
205
|
+
}
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
async updateWebsite(session: FsSession, websiteId: WebsiteId, data: WebsiteData): Promise<void> {
|
|
191
209
|
const id = requiredParam<WebsiteId>(websiteId, 'website id')
|
|
192
|
-
const
|
|
193
|
-
|
|
210
|
+
const websitePath = join(this.options.path, id)
|
|
211
|
+
|
|
212
|
+
// Use the split function to create separate files for pages
|
|
213
|
+
const filesToWrite = split(data)
|
|
214
|
+
|
|
215
|
+
// Ensure the website directory exists
|
|
216
|
+
await fs.mkdir(websitePath, { recursive: true })
|
|
217
|
+
|
|
218
|
+
// Ensure the pages directory exists if we have page files
|
|
219
|
+
const hasPageFiles = filesToWrite.some(f => f.path.startsWith(WEBSITE_PAGES_FOLDER))
|
|
220
|
+
if (hasPageFiles) {
|
|
221
|
+
await fs.mkdir(join(websitePath, WEBSITE_PAGES_FOLDER), { recursive: true })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// **
|
|
225
|
+
// Delete pages that are not in the new website data
|
|
226
|
+
const pagesPath = join(websitePath, WEBSITE_PAGES_FOLDER)
|
|
227
|
+
try {
|
|
228
|
+
const existingPageFiles = await fs.readdir(pagesPath)
|
|
229
|
+
const newPageFiles = new Set(
|
|
230
|
+
filesToWrite
|
|
231
|
+
.filter(f => f.path.startsWith(WEBSITE_PAGES_FOLDER))
|
|
232
|
+
.map(f => f.path.replace(`${WEBSITE_PAGES_FOLDER}/`, ''))
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
for (const existingFile of existingPageFiles) {
|
|
236
|
+
if (existingFile.endsWith('.json') && !newPageFiles.has(existingFile)) {
|
|
237
|
+
await fs.unlink(join(pagesPath, existingFile))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Ignore error if pages directory doesn't exist yet
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Write all files
|
|
245
|
+
for (const file of filesToWrite) {
|
|
246
|
+
const filePath = join(websitePath, file.path)
|
|
247
|
+
await fs.writeFile(filePath, file.content)
|
|
248
|
+
}
|
|
194
249
|
}
|
|
195
250
|
|
|
196
251
|
async deleteWebsite(session: FsSession, websiteId: WebsiteId): Promise<void> {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { stringify, split, merge } from './websiteDataSerialization'
|
|
2
|
+
import { expect, it, describe } from '@jest/globals'
|
|
3
|
+
import { WebsiteData, defaultWebsiteData } from '../../types'
|
|
4
|
+
import { WEBSITE_DATA_FILE } from '../../constants'
|
|
5
|
+
|
|
6
|
+
describe('websiteDataSerialization', () => {
|
|
7
|
+
describe('stringify', () => {
|
|
8
|
+
it('should format JSON with sorted keys', () => {
|
|
9
|
+
const data = { z: 1, a: 2, m: { z: 3, a: 4 } }
|
|
10
|
+
const result = stringify(data)
|
|
11
|
+
expect(result).toBe(`{
|
|
12
|
+
"a": 2,
|
|
13
|
+
"m": {
|
|
14
|
+
"a": 4,
|
|
15
|
+
"z": 3
|
|
16
|
+
},
|
|
17
|
+
"z": 1
|
|
18
|
+
}`)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('split', () => {
|
|
23
|
+
it('should split website data into separate files', () => {
|
|
24
|
+
const websiteData: WebsiteData = {
|
|
25
|
+
...defaultWebsiteData,
|
|
26
|
+
pages: [
|
|
27
|
+
{ id: 'page1', getName: () => 'Home' } as any,
|
|
28
|
+
{ id: 'page2', getName: () => 'About' } as any
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const files = split(websiteData)
|
|
33
|
+
|
|
34
|
+
expect(files).toHaveLength(3)
|
|
35
|
+
expect(files[0].path).toBe('src/home-page1.json')
|
|
36
|
+
expect(files[1].path).toBe('src/about-page2.json')
|
|
37
|
+
expect(files[2].path).toBe(WEBSITE_DATA_FILE)
|
|
38
|
+
|
|
39
|
+
const mainFile = JSON.parse(files[2].content)
|
|
40
|
+
expect(mainFile.fileFormatVersion).toBe('1.0.0')
|
|
41
|
+
expect(mainFile.pages[0].isFile).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('merge', () => {
|
|
46
|
+
it('should handle legacy format', async () => {
|
|
47
|
+
const legacyContent = `{
|
|
48
|
+
"settings": {},
|
|
49
|
+
"pages": [{ "id": "page1" }]
|
|
50
|
+
}`
|
|
51
|
+
|
|
52
|
+
const pageLoader = async () => { throw new Error('Should not be called') }
|
|
53
|
+
const result = await merge(legacyContent, pageLoader)
|
|
54
|
+
|
|
55
|
+
expect(result.pages).toHaveLength(1)
|
|
56
|
+
expect(result.pages[0].id).toBe('page1')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
3
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { WebsiteData } from '../../types'
|
|
19
|
+
import { Page } from 'grapesjs'
|
|
20
|
+
import { getPageSlug } from '../../page'
|
|
21
|
+
import { WEBSITE_PAGES_FOLDER, WEBSITE_DATA_FILE } from '../../constants'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serialize JSON with stable formatting for git
|
|
25
|
+
* - Uses 2-space indentation for readability
|
|
26
|
+
* - Sorts object keys deterministically to avoid random diffs
|
|
27
|
+
*/
|
|
28
|
+
export function stringify(data: any): string {
|
|
29
|
+
return JSON.stringify(data, sortKeys, 2)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sort object keys for consistent output across serializations
|
|
34
|
+
*/
|
|
35
|
+
function sortKeys(key: string, value: any): any {
|
|
36
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
37
|
+
const sorted: any = {}
|
|
38
|
+
Object.keys(value).sort().forEach(k => {
|
|
39
|
+
sorted[k] = value[k]
|
|
40
|
+
})
|
|
41
|
+
return sorted
|
|
42
|
+
}
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Split website data into multiple files
|
|
48
|
+
* Returns array of files to create/update
|
|
49
|
+
*/
|
|
50
|
+
export function split(websiteData: WebsiteData): Array<{path: string, content: string}> {
|
|
51
|
+
const files: Array<{path: string, content: string}> = []
|
|
52
|
+
|
|
53
|
+
// Split pages into separate files
|
|
54
|
+
const pages = websiteData.pages?.map((page: Page) => {
|
|
55
|
+
const pageName = getPageName(page)
|
|
56
|
+
const slug = getPageSlug(pageName)
|
|
57
|
+
const fileName = `${slug}-${page.id}.json`
|
|
58
|
+
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}`
|
|
59
|
+
|
|
60
|
+
files.push({
|
|
61
|
+
path: filePath,
|
|
62
|
+
content: stringify(page)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: pageName,
|
|
67
|
+
id: page.id,
|
|
68
|
+
isFile: true,
|
|
69
|
+
}
|
|
70
|
+
}) || []
|
|
71
|
+
|
|
72
|
+
// Create main website data file without pages content
|
|
73
|
+
const websiteDataWithoutPages = {
|
|
74
|
+
...websiteData,
|
|
75
|
+
fileFormatVersion: '1.0.0',
|
|
76
|
+
pages,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
files.push({
|
|
80
|
+
path: WEBSITE_DATA_FILE,
|
|
81
|
+
content: stringify(websiteDataWithoutPages)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return files
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reconstruct website data from main file and page loader function
|
|
89
|
+
* The pageLoader function will be called for each page that needs to be loaded
|
|
90
|
+
*/
|
|
91
|
+
export async function merge(
|
|
92
|
+
websiteDataContent: string,
|
|
93
|
+
pageLoader: (pagePath: string) => Promise<string>
|
|
94
|
+
): Promise<WebsiteData> {
|
|
95
|
+
const websiteDataOb = JSON.parse(websiteDataContent) as any
|
|
96
|
+
const { fileFormatVersion, ...websiteData } = websiteDataOb
|
|
97
|
+
|
|
98
|
+
// Check file format version
|
|
99
|
+
if (fileFormatVersion && fileFormatVersion !== '1.0.0') {
|
|
100
|
+
console.warn('Website data file format version mismatch', fileFormatVersion, '!=', '1.0.0')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle legacy format (no split pages) or empty pages
|
|
104
|
+
if (!websiteData.pages || websiteData.pages.length === 0) {
|
|
105
|
+
return websiteData as WebsiteData
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if pages are already embedded (legacy format)
|
|
109
|
+
if (websiteData.pages[0] && !websiteData.pages[0].isFile) {
|
|
110
|
+
return websiteData as WebsiteData
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Load pages from separate files
|
|
114
|
+
const pages = await Promise.all(websiteData.pages.map(async (pageRef: any) => {
|
|
115
|
+
if (pageRef.isFile) {
|
|
116
|
+
const pageName = pageRef.name
|
|
117
|
+
const slug = getPageSlug(pageName)
|
|
118
|
+
const fileName = `${slug}-${pageRef.id}.json`
|
|
119
|
+
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}`
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const pageContent = await pageLoader(filePath)
|
|
123
|
+
return JSON.parse(pageContent) as Page
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn(`Could not load page file: ${filePath}`, error)
|
|
126
|
+
return pageRef
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return pageRef as Page
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...websiteData,
|
|
134
|
+
pages,
|
|
135
|
+
} as WebsiteData
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get page name from page object
|
|
140
|
+
*/
|
|
141
|
+
function getPageName(page: Page | any): string {
|
|
142
|
+
return (page as any).getName ? (page as Page).getName() : (page as any).name
|
|
143
|
+
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dynamic plugin loader for external GrapesJS plugins
|
|
3
|
-
* These plugins are loaded at runtime to reduce initial bundle size
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface PluginDefinition {
|
|
7
|
-
name: string
|
|
8
|
-
value: any
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Load all external plugins dynamically
|
|
13
|
-
*/
|
|
14
|
-
export async function loadDynamicPlugins(): Promise<PluginDefinition[]> {
|
|
15
|
-
const [
|
|
16
|
-
blocksBasicPlugin,
|
|
17
|
-
styleFilterPlugin,
|
|
18
|
-
formPlugin,
|
|
19
|
-
codePlugin,
|
|
20
|
-
filterStyles,
|
|
21
|
-
symbolsPlugin,
|
|
22
|
-
loadingPlugin,
|
|
23
|
-
fontsDialogPlugin,
|
|
24
|
-
selectorPlugin,
|
|
25
|
-
rateLimitPlugin,
|
|
26
|
-
borderPlugin,
|
|
27
|
-
backgroundPlugin,
|
|
28
|
-
notificationsPlugin,
|
|
29
|
-
keymapsDialogPlugin,
|
|
30
|
-
] = await Promise.all([
|
|
31
|
-
import('grapesjs-blocks-basic').then(m => m.default),
|
|
32
|
-
import('grapesjs-style-filter').then(m => m.default),
|
|
33
|
-
import('grapesjs-plugin-forms').then(m => m.default),
|
|
34
|
-
import('grapesjs-custom-code').then(m => m.default),
|
|
35
|
-
import('@silexlabs/grapesjs-filter-styles').then(m => m.default),
|
|
36
|
-
import('@silexlabs/grapesjs-symbols').then(m => m.default),
|
|
37
|
-
import('@silexlabs/grapesjs-loading').then(m => m.default),
|
|
38
|
-
import('@silexlabs/grapesjs-fonts').then(m => m.default),
|
|
39
|
-
import('@silexlabs/grapesjs-advanced-selector').then(m => m.default),
|
|
40
|
-
import('@silexlabs/grapesjs-storage-rate-limit').then(m => m.default),
|
|
41
|
-
import('grapesjs-style-border').then(m => m.default),
|
|
42
|
-
import('grapesjs-style-bg').then(m => m.default),
|
|
43
|
-
import('@silexlabs/grapesjs-notifications').then(m => m.default),
|
|
44
|
-
import('@silexlabs/grapesjs-keymaps-dialog').then(m => m.default),
|
|
45
|
-
])
|
|
46
|
-
|
|
47
|
-
return [
|
|
48
|
-
{ name: 'grapesjs-blocks-basic', value: blocksBasicPlugin },
|
|
49
|
-
{ name: 'grapesjs-style-filter', value: styleFilterPlugin },
|
|
50
|
-
{ name: 'grapesjs-plugin-forms', value: formPlugin },
|
|
51
|
-
{ name: 'grapesjs-custom-code', value: codePlugin },
|
|
52
|
-
{ name: '@silexlabs/grapesjs-filter-styles', value: filterStyles },
|
|
53
|
-
{ name: '@silexlabs/grapesjs-symbols', value: symbolsPlugin },
|
|
54
|
-
{ name: '@silexlabs/grapesjs-loading', value: loadingPlugin },
|
|
55
|
-
{ name: '@silexlabs/grapesjs-fonts', value: fontsDialogPlugin },
|
|
56
|
-
{ name: '@silexlabs/grapesjs-advanced-selector', value: selectorPlugin },
|
|
57
|
-
{ name: '@silexlabs/grapesjs-storage-rate-limit', value: rateLimitPlugin },
|
|
58
|
-
{ name: 'grapesjs-style-border', value: borderPlugin },
|
|
59
|
-
{ name: 'grapesjs-style-bg', value: backgroundPlugin },
|
|
60
|
-
{ name: '@silexlabs/grapesjs-notifications', value: notificationsPlugin },
|
|
61
|
-
{ name: '@silexlabs/grapesjs-keymaps-dialog', value: keymapsDialogPlugin },
|
|
62
|
-
]
|
|
63
|
-
}
|