@emdash-cms/cloudflare 0.1.0 → 0.1.1
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/LICENSE +9 -0
- package/dist/db/do.mjs +1 -0
- package/dist/db/playground-middleware.mjs +290 -2
- package/package.json +2 -2
- package/src/db/do-preview.ts +1 -0
- package/src/db/playground-loading.ts +267 -0
- package/src/db/playground-middleware.ts +37 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Cloudflare Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/db/do.mjs
CHANGED
|
@@ -326,6 +326,7 @@ function loadingPage() {
|
|
|
326
326
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
327
327
|
<meta http-equiv="refresh" content="2">
|
|
328
328
|
<title>Loading preview...</title>
|
|
329
|
+
<link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
|
|
329
330
|
<style>
|
|
330
331
|
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #fafafa; color: #333; }
|
|
331
332
|
.spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 16px; }
|
|
@@ -7,6 +7,275 @@ import { ulid } from "ulidx";
|
|
|
7
7
|
import { defineMiddleware } from "astro:middleware";
|
|
8
8
|
import virtualConfig from "virtual:emdash/config";
|
|
9
9
|
|
|
10
|
+
//#region src/db/playground-loading.ts
|
|
11
|
+
/**
|
|
12
|
+
* Playground Loading Page
|
|
13
|
+
*
|
|
14
|
+
* Rendered when a user first hits /playground. Shows an animated loading state
|
|
15
|
+
* while the client-side JS calls /_playground/init to create the DO, run
|
|
16
|
+
* migrations, and apply the seed. Once init completes, redirects to the admin.
|
|
17
|
+
*
|
|
18
|
+
* No dependencies -- plain HTML with inline styles and a <script> tag.
|
|
19
|
+
*/
|
|
20
|
+
function renderPlaygroundLoadingPage() {
|
|
21
|
+
return `<!DOCTYPE html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8" />
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
+
<title>EmDash Playground</title>
|
|
27
|
+
<link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
|
|
28
|
+
<style>
|
|
29
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
|
+
|
|
31
|
+
body {
|
|
32
|
+
min-height: 100dvh;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
background: #0a0a0a;
|
|
37
|
+
color: #e0e0e0;
|
|
38
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
39
|
+
-webkit-font-smoothing: antialiased;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.pg-loading {
|
|
43
|
+
text-align: center;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 32px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.pg-logo {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
gap: 12px;
|
|
55
|
+
font-size: 28px;
|
|
56
|
+
font-weight: 700;
|
|
57
|
+
letter-spacing: -0.02em;
|
|
58
|
+
color: #fff;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.pg-logo svg {
|
|
62
|
+
width: 36px;
|
|
63
|
+
height: 36px;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.pg-spinner-wrap {
|
|
68
|
+
position: relative;
|
|
69
|
+
width: 48px;
|
|
70
|
+
height: 48px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.pg-spinner {
|
|
74
|
+
width: 48px;
|
|
75
|
+
height: 48px;
|
|
76
|
+
border: 3px solid rgba(255, 255, 255, 0.08);
|
|
77
|
+
border-top-color: #facc15;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
animation: pg-spin 0.8s linear infinite;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes pg-spin {
|
|
83
|
+
to { transform: rotate(360deg); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.pg-message {
|
|
87
|
+
font-size: 15px;
|
|
88
|
+
color: #888;
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.pg-steps {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 8px;
|
|
96
|
+
margin-top: 4px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.pg-step {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 8px;
|
|
103
|
+
font-size: 13px;
|
|
104
|
+
color: #555;
|
|
105
|
+
transition: color 0.3s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.pg-step.active {
|
|
109
|
+
color: #ccc;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.pg-step.done {
|
|
113
|
+
color: #4ade80;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.pg-step-dot {
|
|
117
|
+
width: 6px;
|
|
118
|
+
height: 6px;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
background: #333;
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
transition: background 0.3s;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.pg-step.active .pg-step-dot {
|
|
126
|
+
background: #facc15;
|
|
127
|
+
box-shadow: 0 0 6px rgba(250, 204, 21, 0.4);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.pg-step.done .pg-step-dot {
|
|
131
|
+
background: #4ade80;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.pg-error {
|
|
135
|
+
display: none;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 16px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.pg-error.visible {
|
|
142
|
+
display: flex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.pg-error-message {
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
color: #f87171;
|
|
148
|
+
max-width: 360px;
|
|
149
|
+
line-height: 1.5;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.pg-retry-btn {
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
padding: 8px 16px;
|
|
157
|
+
background: rgba(250, 204, 21, 0.12);
|
|
158
|
+
color: #facc15;
|
|
159
|
+
border: none;
|
|
160
|
+
border-radius: 999px;
|
|
161
|
+
font-size: 13px;
|
|
162
|
+
font-weight: 500;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
font-family: inherit;
|
|
165
|
+
transition: background 0.15s;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.pg-retry-btn:hover {
|
|
169
|
+
background: rgba(250, 204, 21, 0.22);
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
172
|
+
</head>
|
|
173
|
+
<body>
|
|
174
|
+
<div class="pg-loading">
|
|
175
|
+
<div class="pg-logo"><svg viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="69" height="69" rx="10.518" stroke="url(#pl-b)" stroke-width="6"/><rect x="18" y="34" width="39.366" height="6.561" fill="url(#pl-d)"/><defs><linearGradient id="pl-b" x1="-43" y1="124" x2="92.42" y2="-41.75" gradientUnits="userSpaceOnUse"><stop stop-color="#0F006B"/><stop offset=".08" stop-color="#281A81"/><stop offset=".17" stop-color="#5D0C83"/><stop offset=".25" stop-color="#911475"/><stop offset=".33" stop-color="#CE2F55"/><stop offset=".42" stop-color="#FF6633"/><stop offset=".5" stop-color="#F6821F"/><stop offset=".58" stop-color="#FBAD41"/><stop offset=".67" stop-color="#FFCD89"/><stop offset=".75" stop-color="#FFE9CB"/><stop offset=".83" stop-color="#FFF7EC"/><stop offset=".92" stop-color="#FFF8EE"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="pl-d" x1="91.5" y1="27.5" x2="28.12" y2="54.18" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset=".13" stop-color="#FFF8EE"/><stop offset=".62" stop-color="#FBAD41"/><stop offset=".85" stop-color="#F6821F"/><stop offset="1" stop-color="#FF6633"/></linearGradient></defs></svg>EmDash</div>
|
|
176
|
+
|
|
177
|
+
<div class="pg-spinner-wrap">
|
|
178
|
+
<div class="pg-spinner" id="pg-spinner"></div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div>
|
|
182
|
+
<div class="pg-message" id="pg-message">Creating your playground…</div>
|
|
183
|
+
<div class="pg-steps" id="pg-steps">
|
|
184
|
+
<div class="pg-step active" id="step-db">
|
|
185
|
+
<span class="pg-step-dot"></span>
|
|
186
|
+
Setting up database
|
|
187
|
+
</div>
|
|
188
|
+
<div class="pg-step" id="step-content">
|
|
189
|
+
<span class="pg-step-dot"></span>
|
|
190
|
+
Loading demo content
|
|
191
|
+
</div>
|
|
192
|
+
<div class="pg-step" id="step-ready">
|
|
193
|
+
<span class="pg-step-dot"></span>
|
|
194
|
+
Almost ready
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="pg-error" id="pg-error">
|
|
200
|
+
<div class="pg-error-message" id="pg-error-message"></div>
|
|
201
|
+
<button class="pg-retry-btn" id="pg-retry">Try again</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<script>
|
|
206
|
+
(function() {
|
|
207
|
+
var steps = ["step-db", "step-content", "step-ready"];
|
|
208
|
+
var currentStep = 0;
|
|
209
|
+
|
|
210
|
+
function setStep(index) {
|
|
211
|
+
for (var i = 0; i < steps.length; i++) {
|
|
212
|
+
var el = document.getElementById(steps[i]);
|
|
213
|
+
if (!el) continue;
|
|
214
|
+
el.className = "pg-step" + (i < index ? " done" : i === index ? " active" : "");
|
|
215
|
+
}
|
|
216
|
+
currentStep = index;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function showError(message) {
|
|
220
|
+
document.getElementById("pg-spinner").style.display = "none";
|
|
221
|
+
document.getElementById("pg-message").textContent = "Something went wrong";
|
|
222
|
+
document.getElementById("pg-steps").style.display = "none";
|
|
223
|
+
var errorEl = document.getElementById("pg-error");
|
|
224
|
+
var errorMsg = document.getElementById("pg-error-message");
|
|
225
|
+
if (errorEl) errorEl.className = "pg-error visible";
|
|
226
|
+
if (errorMsg) errorMsg.textContent = message;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function init() {
|
|
230
|
+
setStep(0);
|
|
231
|
+
document.getElementById("pg-spinner").style.display = "";
|
|
232
|
+
document.getElementById("pg-message").textContent = "Creating your playground\\u2026";
|
|
233
|
+
document.getElementById("pg-steps").style.display = "";
|
|
234
|
+
var errorEl = document.getElementById("pg-error");
|
|
235
|
+
if (errorEl) errorEl.className = "pg-error";
|
|
236
|
+
|
|
237
|
+
// Advance steps on a timer for visual feedback while init runs.
|
|
238
|
+
// The actual init is a single server call -- these steps are cosmetic.
|
|
239
|
+
var stepTimer = setTimeout(function() { setStep(1); }, 800);
|
|
240
|
+
var stepTimer2 = setTimeout(function() { setStep(2); }, 2000);
|
|
241
|
+
|
|
242
|
+
fetch("/_playground/init", { method: "POST", credentials: "same-origin" })
|
|
243
|
+
.then(function(res) {
|
|
244
|
+
clearTimeout(stepTimer);
|
|
245
|
+
clearTimeout(stepTimer2);
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
return res.json().then(function(body) {
|
|
248
|
+
throw new Error(body.error?.message || "Initialization failed");
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return res.json();
|
|
252
|
+
})
|
|
253
|
+
.then(function() {
|
|
254
|
+
// Mark all steps done
|
|
255
|
+
setStep(steps.length);
|
|
256
|
+
document.getElementById("pg-message").textContent = "Ready!";
|
|
257
|
+
// Brief pause so the user sees "Ready!" before navigating
|
|
258
|
+
setTimeout(function() {
|
|
259
|
+
location.replace("/_emdash/admin");
|
|
260
|
+
}, 400);
|
|
261
|
+
})
|
|
262
|
+
.catch(function(err) {
|
|
263
|
+
clearTimeout(stepTimer);
|
|
264
|
+
clearTimeout(stepTimer2);
|
|
265
|
+
showError(err.message || "Failed to create playground. Please try again.");
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
document.getElementById("pg-retry").addEventListener("click", init);
|
|
270
|
+
|
|
271
|
+
init();
|
|
272
|
+
})();
|
|
273
|
+
<\/script>
|
|
274
|
+
</body>
|
|
275
|
+
</html>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
10
279
|
//#region src/db/playground-toolbar.ts
|
|
11
280
|
const RE_AMP = /&/g;
|
|
12
281
|
const RE_QUOT = /"/g;
|
|
@@ -485,14 +754,33 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
485
754
|
maxAge: ttl
|
|
486
755
|
});
|
|
487
756
|
}
|
|
757
|
+
if (initializedSessions.has(token)) return context.redirect("/_emdash/admin");
|
|
758
|
+
return new Response(renderPlaygroundLoadingPage(), {
|
|
759
|
+
status: 200,
|
|
760
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
if (url.pathname === "/_playground/init" && context.request.method === "POST") {
|
|
764
|
+
const token = cookies.get(COOKIE_NAME)?.value;
|
|
765
|
+
if (!token) return Response.json({ error: {
|
|
766
|
+
code: "NO_SESSION",
|
|
767
|
+
message: "No playground session"
|
|
768
|
+
} }, { status: 400 });
|
|
769
|
+
if (initializedSessions.has(token)) return Response.json({ ok: true });
|
|
488
770
|
const stub = getStub(binding, token);
|
|
489
771
|
const db = new Kysely({ dialect: new PreviewDODialect({ getStub: () => stub }) });
|
|
490
|
-
|
|
772
|
+
try {
|
|
491
773
|
await initializePlayground(db, token);
|
|
492
774
|
initializedSessions.add(token);
|
|
493
775
|
await getFullStub(binding, token).setTtlAlarm(ttl);
|
|
776
|
+
return Response.json({ ok: true });
|
|
777
|
+
} catch (error) {
|
|
778
|
+
console.error("Playground initialization failed:", error);
|
|
779
|
+
return Response.json({ error: {
|
|
780
|
+
code: "PLAYGROUND_INIT_ERROR",
|
|
781
|
+
message: "Failed to initialize playground"
|
|
782
|
+
} }, { status: 500 });
|
|
494
783
|
}
|
|
495
|
-
return context.redirect("/_emdash/admin");
|
|
496
784
|
}
|
|
497
785
|
if (url.pathname === "/_playground/reset") {
|
|
498
786
|
cookies.delete(COOKIE_NAME, { path: "/" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emdash-cms/cloudflare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Cloudflare adapters for EmDash - D1, R2, Access, and Worker Loader sandbox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"jose": "^6.1.3",
|
|
67
67
|
"kysely-d1": "^0.4.0",
|
|
68
68
|
"ulidx": "^2.4.1",
|
|
69
|
-
"emdash": "0.1.
|
|
69
|
+
"emdash": "0.1.1"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@cloudflare/workers-types": ">=4.0.0",
|
package/src/db/do-preview.ts
CHANGED
|
@@ -57,6 +57,7 @@ function loadingPage(): string {
|
|
|
57
57
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
58
58
|
<meta http-equiv="refresh" content="2">
|
|
59
59
|
<title>Loading preview...</title>
|
|
60
|
+
<link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
|
|
60
61
|
<style>
|
|
61
62
|
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #fafafa; color: #333; }
|
|
62
63
|
.spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 16px; }
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playground Loading Page
|
|
3
|
+
*
|
|
4
|
+
* Rendered when a user first hits /playground. Shows an animated loading state
|
|
5
|
+
* while the client-side JS calls /_playground/init to create the DO, run
|
|
6
|
+
* migrations, and apply the seed. Once init completes, redirects to the admin.
|
|
7
|
+
*
|
|
8
|
+
* No dependencies -- plain HTML with inline styles and a <script> tag.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function renderPlaygroundLoadingPage(): string {
|
|
12
|
+
return `<!DOCTYPE html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
+
<title>EmDash Playground</title>
|
|
18
|
+
<link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
|
|
19
|
+
<style>
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
min-height: 100dvh;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
background: #0a0a0a;
|
|
28
|
+
color: #e0e0e0;
|
|
29
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
30
|
+
-webkit-font-smoothing: antialiased;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.pg-loading {
|
|
34
|
+
text-align: center;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 32px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.pg-logo {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
gap: 12px;
|
|
46
|
+
font-size: 28px;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
letter-spacing: -0.02em;
|
|
49
|
+
color: #fff;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.pg-logo svg {
|
|
53
|
+
width: 36px;
|
|
54
|
+
height: 36px;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.pg-spinner-wrap {
|
|
59
|
+
position: relative;
|
|
60
|
+
width: 48px;
|
|
61
|
+
height: 48px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.pg-spinner {
|
|
65
|
+
width: 48px;
|
|
66
|
+
height: 48px;
|
|
67
|
+
border: 3px solid rgba(255, 255, 255, 0.08);
|
|
68
|
+
border-top-color: #facc15;
|
|
69
|
+
border-radius: 50%;
|
|
70
|
+
animation: pg-spin 0.8s linear infinite;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@keyframes pg-spin {
|
|
74
|
+
to { transform: rotate(360deg); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.pg-message {
|
|
78
|
+
font-size: 15px;
|
|
79
|
+
color: #888;
|
|
80
|
+
line-height: 1.5;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.pg-steps {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
margin-top: 4px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.pg-step {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 8px;
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
color: #555;
|
|
96
|
+
transition: color 0.3s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.pg-step.active {
|
|
100
|
+
color: #ccc;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.pg-step.done {
|
|
104
|
+
color: #4ade80;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.pg-step-dot {
|
|
108
|
+
width: 6px;
|
|
109
|
+
height: 6px;
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
background: #333;
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
transition: background 0.3s;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.pg-step.active .pg-step-dot {
|
|
117
|
+
background: #facc15;
|
|
118
|
+
box-shadow: 0 0 6px rgba(250, 204, 21, 0.4);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.pg-step.done .pg-step-dot {
|
|
122
|
+
background: #4ade80;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.pg-error {
|
|
126
|
+
display: none;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 16px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.pg-error.visible {
|
|
133
|
+
display: flex;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.pg-error-message {
|
|
137
|
+
font-size: 14px;
|
|
138
|
+
color: #f87171;
|
|
139
|
+
max-width: 360px;
|
|
140
|
+
line-height: 1.5;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.pg-retry-btn {
|
|
144
|
+
display: inline-flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 6px;
|
|
147
|
+
padding: 8px 16px;
|
|
148
|
+
background: rgba(250, 204, 21, 0.12);
|
|
149
|
+
color: #facc15;
|
|
150
|
+
border: none;
|
|
151
|
+
border-radius: 999px;
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
font-family: inherit;
|
|
156
|
+
transition: background 0.15s;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.pg-retry-btn:hover {
|
|
160
|
+
background: rgba(250, 204, 21, 0.22);
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
163
|
+
</head>
|
|
164
|
+
<body>
|
|
165
|
+
<div class="pg-loading">
|
|
166
|
+
<div class="pg-logo"><svg viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="69" height="69" rx="10.518" stroke="url(#pl-b)" stroke-width="6"/><rect x="18" y="34" width="39.366" height="6.561" fill="url(#pl-d)"/><defs><linearGradient id="pl-b" x1="-43" y1="124" x2="92.42" y2="-41.75" gradientUnits="userSpaceOnUse"><stop stop-color="#0F006B"/><stop offset=".08" stop-color="#281A81"/><stop offset=".17" stop-color="#5D0C83"/><stop offset=".25" stop-color="#911475"/><stop offset=".33" stop-color="#CE2F55"/><stop offset=".42" stop-color="#FF6633"/><stop offset=".5" stop-color="#F6821F"/><stop offset=".58" stop-color="#FBAD41"/><stop offset=".67" stop-color="#FFCD89"/><stop offset=".75" stop-color="#FFE9CB"/><stop offset=".83" stop-color="#FFF7EC"/><stop offset=".92" stop-color="#FFF8EE"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="pl-d" x1="91.5" y1="27.5" x2="28.12" y2="54.18" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset=".13" stop-color="#FFF8EE"/><stop offset=".62" stop-color="#FBAD41"/><stop offset=".85" stop-color="#F6821F"/><stop offset="1" stop-color="#FF6633"/></linearGradient></defs></svg>EmDash</div>
|
|
167
|
+
|
|
168
|
+
<div class="pg-spinner-wrap">
|
|
169
|
+
<div class="pg-spinner" id="pg-spinner"></div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div>
|
|
173
|
+
<div class="pg-message" id="pg-message">Creating your playground…</div>
|
|
174
|
+
<div class="pg-steps" id="pg-steps">
|
|
175
|
+
<div class="pg-step active" id="step-db">
|
|
176
|
+
<span class="pg-step-dot"></span>
|
|
177
|
+
Setting up database
|
|
178
|
+
</div>
|
|
179
|
+
<div class="pg-step" id="step-content">
|
|
180
|
+
<span class="pg-step-dot"></span>
|
|
181
|
+
Loading demo content
|
|
182
|
+
</div>
|
|
183
|
+
<div class="pg-step" id="step-ready">
|
|
184
|
+
<span class="pg-step-dot"></span>
|
|
185
|
+
Almost ready
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="pg-error" id="pg-error">
|
|
191
|
+
<div class="pg-error-message" id="pg-error-message"></div>
|
|
192
|
+
<button class="pg-retry-btn" id="pg-retry">Try again</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<script>
|
|
197
|
+
(function() {
|
|
198
|
+
var steps = ["step-db", "step-content", "step-ready"];
|
|
199
|
+
var currentStep = 0;
|
|
200
|
+
|
|
201
|
+
function setStep(index) {
|
|
202
|
+
for (var i = 0; i < steps.length; i++) {
|
|
203
|
+
var el = document.getElementById(steps[i]);
|
|
204
|
+
if (!el) continue;
|
|
205
|
+
el.className = "pg-step" + (i < index ? " done" : i === index ? " active" : "");
|
|
206
|
+
}
|
|
207
|
+
currentStep = index;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function showError(message) {
|
|
211
|
+
document.getElementById("pg-spinner").style.display = "none";
|
|
212
|
+
document.getElementById("pg-message").textContent = "Something went wrong";
|
|
213
|
+
document.getElementById("pg-steps").style.display = "none";
|
|
214
|
+
var errorEl = document.getElementById("pg-error");
|
|
215
|
+
var errorMsg = document.getElementById("pg-error-message");
|
|
216
|
+
if (errorEl) errorEl.className = "pg-error visible";
|
|
217
|
+
if (errorMsg) errorMsg.textContent = message;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function init() {
|
|
221
|
+
setStep(0);
|
|
222
|
+
document.getElementById("pg-spinner").style.display = "";
|
|
223
|
+
document.getElementById("pg-message").textContent = "Creating your playground\\u2026";
|
|
224
|
+
document.getElementById("pg-steps").style.display = "";
|
|
225
|
+
var errorEl = document.getElementById("pg-error");
|
|
226
|
+
if (errorEl) errorEl.className = "pg-error";
|
|
227
|
+
|
|
228
|
+
// Advance steps on a timer for visual feedback while init runs.
|
|
229
|
+
// The actual init is a single server call -- these steps are cosmetic.
|
|
230
|
+
var stepTimer = setTimeout(function() { setStep(1); }, 800);
|
|
231
|
+
var stepTimer2 = setTimeout(function() { setStep(2); }, 2000);
|
|
232
|
+
|
|
233
|
+
fetch("/_playground/init", { method: "POST", credentials: "same-origin" })
|
|
234
|
+
.then(function(res) {
|
|
235
|
+
clearTimeout(stepTimer);
|
|
236
|
+
clearTimeout(stepTimer2);
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
return res.json().then(function(body) {
|
|
239
|
+
throw new Error(body.error?.message || "Initialization failed");
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return res.json();
|
|
243
|
+
})
|
|
244
|
+
.then(function() {
|
|
245
|
+
// Mark all steps done
|
|
246
|
+
setStep(steps.length);
|
|
247
|
+
document.getElementById("pg-message").textContent = "Ready!";
|
|
248
|
+
// Brief pause so the user sees "Ready!" before navigating
|
|
249
|
+
setTimeout(function() {
|
|
250
|
+
location.replace("/_emdash/admin");
|
|
251
|
+
}, 400);
|
|
252
|
+
})
|
|
253
|
+
.catch(function(err) {
|
|
254
|
+
clearTimeout(stepTimer);
|
|
255
|
+
clearTimeout(stepTimer2);
|
|
256
|
+
showError(err.message || "Failed to create playground. Please try again.");
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
document.getElementById("pg-retry").addEventListener("click", init);
|
|
261
|
+
|
|
262
|
+
init();
|
|
263
|
+
})();
|
|
264
|
+
</script>
|
|
265
|
+
</body>
|
|
266
|
+
</html>`;
|
|
267
|
+
}
|
|
@@ -24,6 +24,7 @@ import type { EmDashPreviewDB } from "./do-class.js";
|
|
|
24
24
|
import { PreviewDODialect } from "./do-dialect.js";
|
|
25
25
|
import type { PreviewDBStub } from "./do-dialect.js";
|
|
26
26
|
import { isBlockedInPlayground } from "./do-playground-routes.js";
|
|
27
|
+
import { renderPlaygroundLoadingPage } from "./playground-loading.js";
|
|
27
28
|
import { renderPlaygroundToolbar } from "./playground-toolbar.js";
|
|
28
29
|
|
|
29
30
|
/** Default TTL for playground data (1 hour) */
|
|
@@ -244,6 +245,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
244
245
|
const binding = getBindingName();
|
|
245
246
|
|
|
246
247
|
// --- Entry point: /playground ---
|
|
248
|
+
// Show a loading page immediately. The page calls /_playground/init via
|
|
249
|
+
// fetch to do the actual setup, then redirects to admin when ready.
|
|
250
|
+
// If the session is already initialized, skip the loading page.
|
|
247
251
|
if (url.pathname === "/playground") {
|
|
248
252
|
let token = cookies.get(COOKIE_NAME)?.value;
|
|
249
253
|
if (!token) {
|
|
@@ -256,19 +260,49 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
256
260
|
});
|
|
257
261
|
}
|
|
258
262
|
|
|
263
|
+
// Already initialized? Skip the loading page and go straight to admin.
|
|
264
|
+
if (initializedSessions.has(token)) {
|
|
265
|
+
return context.redirect("/_emdash/admin");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return new Response(renderPlaygroundLoadingPage(), {
|
|
269
|
+
status: 200,
|
|
270
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Init endpoint: called by the loading page ---
|
|
275
|
+
if (url.pathname === "/_playground/init" && context.request.method === "POST") {
|
|
276
|
+
const token = cookies.get(COOKIE_NAME)?.value;
|
|
277
|
+
if (!token) {
|
|
278
|
+
return Response.json(
|
|
279
|
+
{ error: { code: "NO_SESSION", message: "No playground session" } },
|
|
280
|
+
{ status: 400 },
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (initializedSessions.has(token)) {
|
|
285
|
+
return Response.json({ ok: true });
|
|
286
|
+
}
|
|
287
|
+
|
|
259
288
|
const stub = getStub(binding, token);
|
|
260
289
|
const dialect = new PreviewDODialect({ getStub: () => stub });
|
|
261
290
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
291
|
const db = new Kysely<any>({ dialect });
|
|
263
292
|
|
|
264
|
-
|
|
293
|
+
try {
|
|
265
294
|
await initializePlayground(db, token);
|
|
266
295
|
initializedSessions.add(token);
|
|
267
296
|
const fullStub = getFullStub(binding, token);
|
|
268
297
|
await fullStub.setTtlAlarm(ttl);
|
|
298
|
+
return Response.json({ ok: true });
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error("Playground initialization failed:", error);
|
|
301
|
+
return Response.json(
|
|
302
|
+
{ error: { code: "PLAYGROUND_INIT_ERROR", message: "Failed to initialize playground" } },
|
|
303
|
+
{ status: 500 },
|
|
304
|
+
);
|
|
269
305
|
}
|
|
270
|
-
|
|
271
|
-
return context.redirect("/_emdash/admin");
|
|
272
306
|
}
|
|
273
307
|
|
|
274
308
|
// --- Reset endpoint ---
|