@beforesemicolon/site-builder 0.35.0 → 0.36.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/cjs/build-templates.js +5 -5
- package/dist/esm/build-templates.js +5 -5
- package/netlify/functions/auth-middleware.js +183 -0
- package/netlify/functions/auth0-config.js +77 -0
- package/netlify/functions/build.js +214 -0
- package/netlify/functions/copy-file-local.js +88 -0
- package/netlify/functions/github.js +771 -0
- package/netlify/functions/validate-user.js +109 -0
- package/netlify/functions/widgets.js +403 -0
- package/netlify.toml +67 -0
- package/package.json +2 -2
- package/scaffolds/.env.example +18 -0
- package/scaffolds/_redirects +12 -0
- package/scaffolds/admin/app.js +244 -0
- package/scaffolds/admin/auth-manager.js +275 -0
- package/scaffolds/admin/controls/code.control.js +22 -0
- package/scaffolds/admin/controls/controls.js +249 -0
- package/scaffolds/admin/controls/file.control.js +829 -0
- package/scaffolds/admin/controls/html.control.js +43 -0
- package/scaffolds/admin/controls/markdown.control.js +31 -0
- package/scaffolds/admin/data.js +543 -0
- package/scaffolds/admin/flashbar.js +104 -0
- package/scaffolds/admin/index.html +44 -0
- package/scaffolds/admin/modal.js +123 -0
- package/scaffolds/admin/preview-widget.js +102 -0
- package/scaffolds/admin/preview.js +197 -0
- package/scaffolds/admin/repository-manager.js +329 -0
- package/scaffolds/admin/styles.css +1526 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get file content from repository via Netlify Function
|
|
3
|
+
* @param {string} filePath - Path to file in repository
|
|
4
|
+
* @returns {Promise<{content: string, sha: string}>} File content and SHA
|
|
5
|
+
*/
|
|
6
|
+
export async function getFileContent(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch('/api/github', {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
action: 'getFile',
|
|
15
|
+
data: { filePath },
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const error = await response.json()
|
|
21
|
+
throw new Error(
|
|
22
|
+
error.error || `Failed to fetch file: ${response.status}`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = await response.json()
|
|
27
|
+
return result
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error fetching file content:', error)
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse widget file and extract inputs section
|
|
36
|
+
* @param {string} fileContent - Widget file content
|
|
37
|
+
* @returns {{inputs: string, beforeInputs: string, afterInputs: string}}
|
|
38
|
+
*/
|
|
39
|
+
export function parseWidgetFile(fileContent) {
|
|
40
|
+
// Find the inputs array in the widget file
|
|
41
|
+
const inputsMatch = fileContent.match(/inputs:\s*\[([\s\S]*?)\],\s*render/)
|
|
42
|
+
|
|
43
|
+
if (!inputsMatch) {
|
|
44
|
+
throw new Error('Could not find inputs section in widget file')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const inputsContent = inputsMatch[1]
|
|
48
|
+
const inputsStartIndex = inputsMatch.index
|
|
49
|
+
const inputsEndIndex =
|
|
50
|
+
inputsStartIndex + inputsMatch[0].length - ', render'.length
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
inputs: inputsContent,
|
|
54
|
+
beforeInputs: fileContent.substring(
|
|
55
|
+
0,
|
|
56
|
+
inputsStartIndex + 'inputs: ['.length
|
|
57
|
+
),
|
|
58
|
+
afterInputs: fileContent.substring(inputsEndIndex),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert widget inputs object to JavaScript code string
|
|
64
|
+
* @param {Array} inputs - Widget inputs array
|
|
65
|
+
* @returns {string} JavaScript code representation
|
|
66
|
+
*/
|
|
67
|
+
export function serializeInputs(inputs) {
|
|
68
|
+
const serializeValue = (value) => {
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
// Escape special characters and preserve newlines
|
|
71
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
74
|
+
return String(value)
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return `[${value.map(serializeValue).join(', ')}]`
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === 'object' && value !== null) {
|
|
80
|
+
const entries = Object.entries(value).map(
|
|
81
|
+
([k, v]) => `${k}: ${serializeValue(v)}`
|
|
82
|
+
)
|
|
83
|
+
return `{${entries.join(', ')}}`
|
|
84
|
+
}
|
|
85
|
+
return 'null'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const serializeInput = (input, indent = ' ') => {
|
|
89
|
+
const parts = []
|
|
90
|
+
|
|
91
|
+
// Add type
|
|
92
|
+
parts.push(`type: "${input.type}"`)
|
|
93
|
+
|
|
94
|
+
// Add name
|
|
95
|
+
parts.push(`name: "${input.name}"`)
|
|
96
|
+
|
|
97
|
+
// Add value
|
|
98
|
+
parts.push(`value: ${serializeValue(input.value)}`)
|
|
99
|
+
|
|
100
|
+
// Add definitions if present (for list/group types)
|
|
101
|
+
if (input.definitions) {
|
|
102
|
+
const defIndent = indent + ' '
|
|
103
|
+
const defsStr = input.definitions
|
|
104
|
+
.map((def) => `\n${defIndent}${serializeInput(def, defIndent)}`)
|
|
105
|
+
.join(',')
|
|
106
|
+
parts.push(`definitions: [${defsStr}\n${indent}]`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `{ ${parts.join(', ')} }`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return inputs.map((input) => `\n ${serializeInput(input)}`).join(',')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update widget file with new inputs
|
|
117
|
+
* @param {string} widgetId - Widget identifier
|
|
118
|
+
* @param {Array} newInputs - New inputs array
|
|
119
|
+
* @returns {Promise<{content: string, sha: string, filePath: string}>} Updated file content
|
|
120
|
+
*/
|
|
121
|
+
export async function updateWidgetFile(widgetId, newInputs) {
|
|
122
|
+
const filePath = `widgets/${widgetId}.js`
|
|
123
|
+
|
|
124
|
+
// Fetch current file content
|
|
125
|
+
const { content, sha } = await getFileContent(filePath)
|
|
126
|
+
|
|
127
|
+
// Parse the file to extract inputs section
|
|
128
|
+
const { beforeInputs, afterInputs } = parseWidgetFile(content)
|
|
129
|
+
|
|
130
|
+
// Serialize new inputs
|
|
131
|
+
const serializedInputs = serializeInputs(newInputs)
|
|
132
|
+
|
|
133
|
+
// Reconstruct the file (add comma before render function)
|
|
134
|
+
const updatedContent = `${beforeInputs}${serializedInputs}\n ],${afterInputs}`
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
content: updatedContent,
|
|
138
|
+
sha,
|
|
139
|
+
filePath,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Convert ArrayBuffer to base64 string for GitHub API
|
|
145
|
+
* @param {ArrayBuffer} buffer - File data as ArrayBuffer
|
|
146
|
+
* @returns {string} Base64 encoded string
|
|
147
|
+
*/
|
|
148
|
+
function arrayBufferToBase64(buffer) {
|
|
149
|
+
const bytes = new Uint8Array(buffer)
|
|
150
|
+
let binary = ''
|
|
151
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
152
|
+
binary += String.fromCharCode(bytes[i])
|
|
153
|
+
}
|
|
154
|
+
return btoa(binary)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Commit changes to repository via Netlify Function
|
|
159
|
+
* @param {string} message - Commit message
|
|
160
|
+
* @param {Array<{filePath: string, content: string, sha: string}>} changedFiles - Files to commit
|
|
161
|
+
* @param {Array<{filePath: string, fileData: Object}>} uploadedFiles - Files to upload (optional)
|
|
162
|
+
* @returns {Promise<Object>} Commit response
|
|
163
|
+
*/
|
|
164
|
+
export async function commitChanges(
|
|
165
|
+
message,
|
|
166
|
+
changedFiles,
|
|
167
|
+
uploadedFiles = [],
|
|
168
|
+
widgets = []
|
|
169
|
+
) {
|
|
170
|
+
const commitData = { message }
|
|
171
|
+
const hasWidgets = Array.isArray(widgets) && widgets.length > 0
|
|
172
|
+
const hasFiles = Array.isArray(changedFiles) && changedFiles.length > 0
|
|
173
|
+
|
|
174
|
+
if (hasWidgets) {
|
|
175
|
+
commitData.widgets = widgets
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (hasFiles) {
|
|
179
|
+
commitData.files = changedFiles
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (uploadedFiles && uploadedFiles.length > 0) {
|
|
183
|
+
commitData.uploadedFiles = uploadedFiles.map((file) => ({
|
|
184
|
+
filePath: file.filePath,
|
|
185
|
+
content: arrayBufferToBase64(file.fileData.data), // Convert ArrayBuffer to base64
|
|
186
|
+
encoding: 'base64', // GitHub API expects base64 for binary files
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hasUploadedFiles =
|
|
191
|
+
Array.isArray(commitData.uploadedFiles) &&
|
|
192
|
+
commitData.uploadedFiles.length > 0
|
|
193
|
+
|
|
194
|
+
if (!hasWidgets && !hasFiles && !hasUploadedFiles) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const response = await fetch('/api/github', {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: {
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
action: 'commitChanges',
|
|
205
|
+
data: commitData,
|
|
206
|
+
}),
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
const error = await response.json()
|
|
211
|
+
console.error('[cms:commitChanges] Commit failed', {
|
|
212
|
+
status: response.status,
|
|
213
|
+
error,
|
|
214
|
+
})
|
|
215
|
+
throw new Error(
|
|
216
|
+
error.error || `Failed to commit changes: ${response.status}`
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return await response.json()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Trigger a Netlify build/deploy
|
|
225
|
+
* @returns {Promise<{buildId: string}>}
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* Wait for auto-build to start after commit
|
|
229
|
+
* Polls for a new build that was created after the commit time
|
|
230
|
+
* @param {string} commitTime - ISO timestamp of when commit was made
|
|
231
|
+
* @returns {Promise<{buildId: string}>} Build ID
|
|
232
|
+
*/
|
|
233
|
+
export async function waitForBuild(commitTime) {
|
|
234
|
+
const maxAttempts = 12 // 1 minute max (5 second intervals)
|
|
235
|
+
let attempts = 0
|
|
236
|
+
|
|
237
|
+
while (attempts < maxAttempts) {
|
|
238
|
+
const response = await fetch('/api/build', {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: {
|
|
241
|
+
'Content-Type': 'application/json',
|
|
242
|
+
},
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
action: 'getLatestBuild',
|
|
245
|
+
}),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
const error = await response.json()
|
|
250
|
+
throw new Error(
|
|
251
|
+
error.error || `Failed to get latest build: ${response.status}`
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { buildId, createdAt } = await response.json()
|
|
256
|
+
|
|
257
|
+
// Check if this build was created after our commit
|
|
258
|
+
if (new Date(createdAt) >= new Date(commitTime)) {
|
|
259
|
+
return { buildId }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Wait 5 seconds before next check
|
|
263
|
+
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
264
|
+
attempts++
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
throw new Error(
|
|
268
|
+
'Timeout waiting for build to start - check Netlify dashboard'
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check build status via Netlify Function
|
|
274
|
+
* @param {string} buildId - Build ID to check
|
|
275
|
+
* @returns {Promise<{status: string, done: boolean}>} Build status
|
|
276
|
+
*/
|
|
277
|
+
export async function checkBuildStatus(buildId) {
|
|
278
|
+
const response = await fetch('/api/build', {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: {
|
|
281
|
+
'Content-Type': 'application/json',
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
action: 'checkBuildStatus',
|
|
285
|
+
data: { buildId },
|
|
286
|
+
}),
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
const error = await response.json()
|
|
291
|
+
throw new Error(
|
|
292
|
+
error.error || `Failed to check build status: ${response.status}`
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return await response.json()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Poll build status until complete
|
|
301
|
+
* @param {string} buildId - Build ID to poll
|
|
302
|
+
* @param {Function} onProgress - Progress callback
|
|
303
|
+
* @returns {Promise<void>}
|
|
304
|
+
*/
|
|
305
|
+
export async function pollBuildStatus(buildId) {
|
|
306
|
+
const maxAttempts = 60 // 5 minutes max (5 second intervals)
|
|
307
|
+
let attempts = 0
|
|
308
|
+
|
|
309
|
+
while (attempts < maxAttempts) {
|
|
310
|
+
const { done, status, error } = await checkBuildStatus(buildId)
|
|
311
|
+
|
|
312
|
+
if (done) {
|
|
313
|
+
// Check if build failed
|
|
314
|
+
if (status === 'error') {
|
|
315
|
+
throw new Error(
|
|
316
|
+
error ||
|
|
317
|
+
'Build failed - check Netlify dashboard for details'
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Wait 5 seconds before next check
|
|
324
|
+
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
325
|
+
attempts++
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
throw new Error('Build timeout - please check Netlify dashboard')
|
|
329
|
+
}
|