@getvitops/create 0.8.0-alpha.0 → 0.8.2
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/CHANGELOG.md +306 -5
- package/package.json +2 -2
- package/templates/emdash/.agents/skills/promote/SKILL.md +24 -7
- package/templates/emdash/.claude/hooks/session-start.sh +0 -0
- package/templates/emdash/.github/workflows/backmerge.yml +18 -0
- package/templates/emdash/.github/workflows/refresh-dev.yml +25 -0
- package/templates/emdash/.node-version +1 -0
- package/templates/emdash/AGENTS.md +51 -9
- package/templates/emdash/README.md +10 -4
- package/templates/emdash/SETUP.html +53 -0
- package/templates/emdash/astro.config.mjs +22 -3
- package/templates/emdash/package.json +10 -9
- package/templates/emdash/pnpm-lock.yaml +183 -0
- package/templates/emdash/pnpm-workspace.yaml +1 -1
- package/templates/emdash/public/_headers +10 -0
- package/templates/emdash/scripts/build-dev-wrangler.mjs +6 -0
- package/templates/emdash/scripts/refresh-dev.mjs +182 -0
- package/templates/emdash/seed/seed.json +104 -5
- package/templates/emdash/site.json +2 -2
- package/templates/emdash/src/pages/[...slug].astro +8 -2
- package/templates/emdash/src/pages/index.astro +8 -2
- package/templates/emdash/wrangler.jsonc +4 -0
- package/templates/static/.github/workflows/backmerge.yml +18 -0
- package/templates/static/.node-version +1 -0
- package/templates/static/AGENTS.md +9 -9
- package/templates/static/SETUP.html +53 -0
- package/templates/static/package.json +4 -4
- package/templates/static/pnpm-workspace.yaml +21 -0
- package/templates/static/public/_headers +10 -0
- package/templates/static/site.json +2 -2
|
@@ -194,6 +194,59 @@
|
|
|
194
194
|
so.
|
|
195
195
|
</span>
|
|
196
196
|
</label>
|
|
197
|
+
<label class="custom-check">
|
|
198
|
+
<input type="checkbox" data-sop="hosting.content-in-prod" />
|
|
199
|
+
<span class="custom-check__box"
|
|
200
|
+
><span class="custom-check__indicator custom-check__indicator--check"></span
|
|
201
|
+
></span>
|
|
202
|
+
<span>
|
|
203
|
+
<strong>Know where content is edited (EmDash template).</strong> Editors work on the
|
|
204
|
+
<em>production</em> site — drafts, "Publish changes" and preview links are the
|
|
205
|
+
staging step. The dev site holds a disposable copy of production's database; it is
|
|
206
|
+
refreshed from prod and never promoted back. Code, pages and
|
|
207
|
+
<code>site.json</code> still go dev → main.
|
|
208
|
+
</span>
|
|
209
|
+
</label>
|
|
210
|
+
<label class="custom-check">
|
|
211
|
+
<input type="checkbox" data-sop="hosting.refresh-dev" />
|
|
212
|
+
<span class="custom-check__box"
|
|
213
|
+
><span class="custom-check__indicator custom-check__indicator--check"></span
|
|
214
|
+
></span>
|
|
215
|
+
<span>
|
|
216
|
+
EmDash template: run <strong>Refresh dev from prod</strong> (Actions tab) once the
|
|
217
|
+
prod site has content, and again before rehearsing a schema change. It runs weekly
|
|
218
|
+
on its own. To copy media as well, set <code>R2_ACCESS_KEY_ID</code> and
|
|
219
|
+
<code>R2_SECRET_ACCESS_KEY</code> (an R2 API token's S3 credentials) as repo
|
|
220
|
+
secrets; without them only the database is refreshed.
|
|
221
|
+
</span>
|
|
222
|
+
</label>
|
|
223
|
+
<label class="custom-check">
|
|
224
|
+
<input type="checkbox" data-sop="hosting.magic-link" />
|
|
225
|
+
<span class="custom-check__box"
|
|
226
|
+
><span class="custom-check__indicator custom-check__indicator--check"></span
|
|
227
|
+
></span>
|
|
228
|
+
<span>
|
|
229
|
+
EmDash template: onboard the sender domain for sign-in email —
|
|
230
|
+
<code>npx vitops email setup send.<your-domain></code>, then put that address
|
|
231
|
+
in <code>cloudflareEmail({ from })</code> in <code>astro.config.mjs</code> (it ships
|
|
232
|
+
a placeholder). Editors sign in with a magic link ("Sign in with email" on the login
|
|
233
|
+
page) because a passkey only works at the origin it was registered on, so a
|
|
234
|
+
refreshed dev site has no other way in. Check <strong>Settings → Email</strong> and
|
|
235
|
+
send the test email on both prod and dev.
|
|
236
|
+
</span>
|
|
237
|
+
</label>
|
|
238
|
+
<label class="custom-check">
|
|
239
|
+
<input type="checkbox" data-sop="hosting.preview-secret" />
|
|
240
|
+
<span class="custom-check__box"
|
|
241
|
+
><span class="custom-check__indicator custom-check__indicator--check"></span
|
|
242
|
+
></span>
|
|
243
|
+
<span>
|
|
244
|
+
EmDash template: set a different <code>EMDASH_PREVIEW_SECRET</code> on the dev
|
|
245
|
+
worker (<code>wrangler secret put EMDASH_PREVIEW_SECRET</code>). The secret lives in
|
|
246
|
+
the database, so a refresh copies prod's — and prod preview links would then open
|
|
247
|
+
drafts on dev.
|
|
248
|
+
</span>
|
|
249
|
+
</label>
|
|
197
250
|
</div>
|
|
198
251
|
</details>
|
|
199
252
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import react from '@astrojs/react';
|
|
2
|
+
import { cloudflareEmail } from '@emdash-cms/cloudflare/plugins';
|
|
2
3
|
import vitops from '@getvitops/astro';
|
|
3
4
|
import { resolveOrganizationName } from '@getvitops/astro/routes';
|
|
4
5
|
import { vitopsEmdash, vitopsHosting } from '@getvitops/emdash';
|
|
@@ -35,8 +36,9 @@ export default defineConfig({
|
|
|
35
36
|
emdash({
|
|
36
37
|
database,
|
|
37
38
|
storage,
|
|
38
|
-
// Vitops design-system blocks (image compare, copy snippet,
|
|
39
|
-
// disclosure,
|
|
39
|
+
// Vitops design-system blocks (link/button, image compare, copy snippet,
|
|
40
|
+
// banner, disclosure, accordion, hero, carousel, gallery, cards, marquee) in the
|
|
41
|
+
// editor slash menu. Narrow them with `vitopsEmdash({ blocks: { exclude } })`. Default 'integration'
|
|
40
42
|
// script delivery: Layout.astro renders <Head /> from @getvitops/astro,
|
|
41
43
|
// which emits the web-component runtime tags.
|
|
42
44
|
//
|
|
@@ -46,7 +48,24 @@ export default defineConfig({
|
|
|
46
48
|
// a VITOPS_GITHUB_TOKEN secret are set (see SETUP.html) — see
|
|
47
49
|
// AGENTS.md's "EmDash plugin as the design-system editing surface".
|
|
48
50
|
// Until then the live half still works; only the sync half is skipped.
|
|
49
|
-
|
|
51
|
+
//
|
|
52
|
+
// cloudflareEmail sends magic-link sign-in, invites and account recovery
|
|
53
|
+
// through Cloudflare Email Sending (the EMAIL binding in wrangler.jsonc).
|
|
54
|
+
// Magic link is how an editor gets into a site whose database was copied
|
|
55
|
+
// from another origin: a passkey only works at the origin it was
|
|
56
|
+
// registered on, so a dev database refreshed from prod
|
|
57
|
+
// (scripts/refresh-dev.mjs) would otherwise lock everyone out of dev. The
|
|
58
|
+
// cloned users already exist, so the same email signs in on both origins.
|
|
59
|
+
// `from` must be on a domain onboarded with Cloudflare Email Sending —
|
|
60
|
+
// `npx vitops email setup send.<your-domain>` — and is usually the
|
|
61
|
+
// subdomain, not the apex (see AGENTS.md's "Sender-domain onboarding").
|
|
62
|
+
// Without it, magic link answers "Email is not configured".
|
|
63
|
+
plugins: [
|
|
64
|
+
vitopsEmdash({ designSystemEditor: { config } }),
|
|
65
|
+
cloudflareEmail({
|
|
66
|
+
from: { email: 'cms@send.my-emdash-site.example.com', name: organizationName },
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
50
69
|
// 1.0's runtime migration mode defaults to 'auto' (apply pending core
|
|
51
70
|
// migrations on first request) — fine for local dev, but a deploy
|
|
52
71
|
// should have already migrated before traffic arrives. 'check' 503s a
|
|
@@ -20,22 +20,23 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@astrojs/cloudflare": "^14.0.0",
|
|
23
|
-
"@astrojs/react": "^
|
|
24
|
-
"@emdash-cms/cloudflare": "^1.0
|
|
23
|
+
"@astrojs/react": "^7.1.2",
|
|
24
|
+
"@emdash-cms/cloudflare": "^1.4.0",
|
|
25
25
|
"@getvitops/astro": "latest",
|
|
26
26
|
"@getvitops/core": "latest",
|
|
27
27
|
"@getvitops/emdash": "latest",
|
|
28
28
|
"@tailwindcss/vite": "^4.3.3",
|
|
29
|
-
"astro": "^7.
|
|
30
|
-
"emdash": "^1.0
|
|
31
|
-
"react": "^19.
|
|
32
|
-
"react-dom": "^19.
|
|
29
|
+
"astro": "^7.4.4",
|
|
30
|
+
"emdash": "^1.4.0",
|
|
31
|
+
"react": "^19.3.1",
|
|
32
|
+
"react-dom": "^19.3.1",
|
|
33
33
|
"tailwindcss": "^4.3.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@astrojs/check": "^0.9.
|
|
36
|
+
"@astrojs/check": "^0.9.12",
|
|
37
37
|
"@cloudflare/workers-types": "^4.20260305.1",
|
|
38
38
|
"@getvitops/cli": "latest",
|
|
39
|
+
"@iconify-json/fa7-solid": "^1.2.5",
|
|
39
40
|
"typescript": "^6.0.3",
|
|
40
41
|
"vite": "latest",
|
|
41
42
|
"vite-plus": "latest",
|
|
@@ -44,11 +45,11 @@
|
|
|
44
45
|
"devEngines": {
|
|
45
46
|
"runtime": {
|
|
46
47
|
"name": "node",
|
|
47
|
-
"version": "24
|
|
48
|
+
"version": "24",
|
|
48
49
|
"onFail": "download"
|
|
49
50
|
}
|
|
50
51
|
},
|
|
51
|
-
"packageManager": "pnpm@
|
|
52
|
+
"packageManager": "pnpm@12.16.1",
|
|
52
53
|
"emdash": {
|
|
53
54
|
"seed": "seed/seed.json"
|
|
54
55
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
lockfileVersion: '9.0'
|
|
3
|
+
|
|
4
|
+
importers:
|
|
5
|
+
|
|
6
|
+
.:
|
|
7
|
+
configDependencies: {}
|
|
8
|
+
packageManagerDependencies:
|
|
9
|
+
'@pnpm/exe':
|
|
10
|
+
specifier: 12.16.1
|
|
11
|
+
version: 12.16.1
|
|
12
|
+
pnpm:
|
|
13
|
+
specifier: 12.16.1
|
|
14
|
+
version: 12.16.1
|
|
15
|
+
|
|
16
|
+
packages:
|
|
17
|
+
|
|
18
|
+
'@pnpm/exe.android-arm64@12.16.1':
|
|
19
|
+
resolution: {integrity: sha512-PKf78Ew+i0CBYf0Xc+eMcqQiqyBT+u8XCVg1F5roc98/6wWfdYlWhrUMmEBiKWaJ3JqHxrtt8l1NwZ7qfJt2/Q==}
|
|
20
|
+
cpu: [arm64]
|
|
21
|
+
os: [android]
|
|
22
|
+
|
|
23
|
+
'@pnpm/exe.android-x64@12.16.1':
|
|
24
|
+
resolution: {integrity: sha512-xdu7gMbB98pdvhwMeA6612Of515FDlRUtfA/1f7nWPZfPLnEdSRBO2u+1N0XBtQ/ennhPH/lrxB1gC6gnR/cdg==}
|
|
25
|
+
cpu: [x64]
|
|
26
|
+
os: [android]
|
|
27
|
+
|
|
28
|
+
'@pnpm/exe.darwin-arm64@12.16.1':
|
|
29
|
+
resolution: {integrity: sha512-pVbmIIbhixiLvADLu1WA3TaW8FZy/D72R1De27WbbFx8/2mxvnsTyFVm4fO9kjpjMY7RzJd48CiQ96p8fO3ebQ==}
|
|
30
|
+
cpu: [arm64]
|
|
31
|
+
os: [darwin]
|
|
32
|
+
|
|
33
|
+
'@pnpm/exe.darwin-x64@12.16.1':
|
|
34
|
+
resolution: {integrity: sha512-xZvQBFqcZjO7vFKTK3XsJpqY6lnBL9EWSLCzVUPq3kZ94+D96gwJCa76xxGNK8Cj32f4eeM91Qg+yNRR4sRpDw==}
|
|
35
|
+
cpu: [x64]
|
|
36
|
+
os: [darwin]
|
|
37
|
+
|
|
38
|
+
'@pnpm/exe.freebsd-x64@12.16.1':
|
|
39
|
+
resolution: {integrity: sha512-zTtunfi9mBhRXiXEJYfwiJj0tk7MgTSfqDSRh+Az9FZ9THazVIiQiVNS8jpcsftDOqu7LKNffQdjmgwHDThfUg==}
|
|
40
|
+
cpu: [x64]
|
|
41
|
+
os: [freebsd]
|
|
42
|
+
|
|
43
|
+
'@pnpm/exe.linux-arm64-musl@12.16.1':
|
|
44
|
+
resolution: {integrity: sha512-BJ3mOmcmQgcUo92Ee/WfEOt1pRiv3y2MMAkhEiYHd85UiPpHTfFhv+SpCbALmfExzXk3UlaRtMYwHlkQymc4VA==}
|
|
45
|
+
cpu: [arm64]
|
|
46
|
+
os: [linux]
|
|
47
|
+
libc: [musl]
|
|
48
|
+
|
|
49
|
+
'@pnpm/exe.linux-arm64@12.16.1':
|
|
50
|
+
resolution: {integrity: sha512-JaRd5gNxwdNGBBkrZFt8bknrK4YmHVJBW0JMhUQ0fJ0dEYp6C2GT9ExaCqOO8EwYEyqM+jpKgJIK+8WFaJy3ew==}
|
|
51
|
+
cpu: [arm64]
|
|
52
|
+
os: [linux]
|
|
53
|
+
libc: [glibc]
|
|
54
|
+
|
|
55
|
+
'@pnpm/exe.linux-ppc64@12.16.1':
|
|
56
|
+
resolution: {integrity: sha512-FnSV0Jv40S1cMv7vFNWwvu0X567Lec4L4HCRead3CFcD6zQDNbGw81s8N5bcKPahtPD9ApVuhsbmFCx4rw6RMw==}
|
|
57
|
+
cpu: [ppc64]
|
|
58
|
+
os: [linux]
|
|
59
|
+
libc: [glibc]
|
|
60
|
+
|
|
61
|
+
'@pnpm/exe.linux-riscv64@12.16.1':
|
|
62
|
+
resolution: {integrity: sha512-C1Sww4FN5IttD4atRRhAKmQtSGWVp04ZAKNxO8pfu2bkRPwOW+7v+2DH1Xs1W+kZYYpO86BYDvUcadefF+AgaQ==}
|
|
63
|
+
cpu: [riscv64]
|
|
64
|
+
os: [linux]
|
|
65
|
+
libc: [glibc]
|
|
66
|
+
|
|
67
|
+
'@pnpm/exe.linux-s390x@12.16.1':
|
|
68
|
+
resolution: {integrity: sha512-kn2fautsTcxIMODoovQVqZyfpDRdNgGkoeVqNdrKqbeBBvrEl+fXLkmZiq5xDht1LMr4sYiCNcsMfNMT6JPFmg==}
|
|
69
|
+
cpu: [s390x]
|
|
70
|
+
os: [linux]
|
|
71
|
+
libc: [glibc]
|
|
72
|
+
|
|
73
|
+
'@pnpm/exe.linux-x64-musl@12.16.1':
|
|
74
|
+
resolution: {integrity: sha512-PM6ccHCOe/O9SsTflTJTYWfwogJZa6EmUvpnwZgiPguJBddh20wDQKgxUPXf48rDsPm9vMsIij0uHswwwbXL8g==}
|
|
75
|
+
cpu: [x64]
|
|
76
|
+
os: [linux]
|
|
77
|
+
libc: [musl]
|
|
78
|
+
|
|
79
|
+
'@pnpm/exe.linux-x64@12.16.1':
|
|
80
|
+
resolution: {integrity: sha512-RK//8ZLcXGW8A2k8bQOXZDBN1Qu92zN4dYnG1bzj1zoQ1fkZ6jNzZ1AvXtsfGVma795CJtvoaF5bLXf4l/FIUw==}
|
|
81
|
+
cpu: [x64]
|
|
82
|
+
os: [linux]
|
|
83
|
+
libc: [glibc]
|
|
84
|
+
|
|
85
|
+
'@pnpm/exe.win32-arm64@12.16.1':
|
|
86
|
+
resolution: {integrity: sha512-Ox3kNNVxtGw6pK7SVejmeOUgHhUTZgAM7NQQ6cU7frPw3ix9+j7d2MGbRg3fyEFAaKWq8ZAZLTQGcDyLnAu+qA==}
|
|
87
|
+
cpu: [arm64]
|
|
88
|
+
os: [win32]
|
|
89
|
+
|
|
90
|
+
'@pnpm/exe.win32-x64@12.16.1':
|
|
91
|
+
resolution: {integrity: sha512-k+FhRJ0ttphFoFIVB1Sk4v76YJAulafmA/Ly57cysesUhhIzVlApeOShUz0jXubKx317TThlEfpQrukldnqORA==}
|
|
92
|
+
cpu: [x64]
|
|
93
|
+
os: [win32]
|
|
94
|
+
|
|
95
|
+
'@pnpm/exe@12.16.1':
|
|
96
|
+
resolution: {integrity: sha512-Opsh2R7IuFY3ekd0C282Tmzgr5hiSlKMOv05/3dJXUrrp5NbmjC1Cm5PCFsa8xsiXY7z4oSkI4Qof1LjTkgAJg==}
|
|
97
|
+
engines: {node: '>=18'}
|
|
98
|
+
hasBin: true
|
|
99
|
+
|
|
100
|
+
pnpm@12.16.1:
|
|
101
|
+
resolution: {integrity: sha512-CR0d7nknV1Zhsl2rPCrjHK/BxU1rce1qZgDPDByxBDSkR+rhQBXxsj4ua+3u1535SpMuIWTSgyKA3OP7MvZW0Q==}
|
|
102
|
+
engines: {node: '>=18'}
|
|
103
|
+
hasBin: true
|
|
104
|
+
|
|
105
|
+
snapshots:
|
|
106
|
+
|
|
107
|
+
'@pnpm/exe.android-arm64@12.16.1':
|
|
108
|
+
optional: true
|
|
109
|
+
|
|
110
|
+
'@pnpm/exe.android-x64@12.16.1':
|
|
111
|
+
optional: true
|
|
112
|
+
|
|
113
|
+
'@pnpm/exe.darwin-arm64@12.16.1':
|
|
114
|
+
optional: true
|
|
115
|
+
|
|
116
|
+
'@pnpm/exe.darwin-x64@12.16.1':
|
|
117
|
+
optional: true
|
|
118
|
+
|
|
119
|
+
'@pnpm/exe.freebsd-x64@12.16.1':
|
|
120
|
+
optional: true
|
|
121
|
+
|
|
122
|
+
'@pnpm/exe.linux-arm64-musl@12.16.1':
|
|
123
|
+
optional: true
|
|
124
|
+
|
|
125
|
+
'@pnpm/exe.linux-arm64@12.16.1':
|
|
126
|
+
optional: true
|
|
127
|
+
|
|
128
|
+
'@pnpm/exe.linux-ppc64@12.16.1':
|
|
129
|
+
optional: true
|
|
130
|
+
|
|
131
|
+
'@pnpm/exe.linux-riscv64@12.16.1':
|
|
132
|
+
optional: true
|
|
133
|
+
|
|
134
|
+
'@pnpm/exe.linux-s390x@12.16.1':
|
|
135
|
+
optional: true
|
|
136
|
+
|
|
137
|
+
'@pnpm/exe.linux-x64-musl@12.16.1':
|
|
138
|
+
optional: true
|
|
139
|
+
|
|
140
|
+
'@pnpm/exe.linux-x64@12.16.1':
|
|
141
|
+
optional: true
|
|
142
|
+
|
|
143
|
+
'@pnpm/exe.win32-arm64@12.16.1':
|
|
144
|
+
optional: true
|
|
145
|
+
|
|
146
|
+
'@pnpm/exe.win32-x64@12.16.1':
|
|
147
|
+
optional: true
|
|
148
|
+
|
|
149
|
+
'@pnpm/exe@12.16.1':
|
|
150
|
+
optionalDependencies:
|
|
151
|
+
'@pnpm/exe.android-arm64': 12.16.1
|
|
152
|
+
'@pnpm/exe.android-x64': 12.16.1
|
|
153
|
+
'@pnpm/exe.darwin-arm64': 12.16.1
|
|
154
|
+
'@pnpm/exe.darwin-x64': 12.16.1
|
|
155
|
+
'@pnpm/exe.freebsd-x64': 12.16.1
|
|
156
|
+
'@pnpm/exe.linux-arm64': 12.16.1
|
|
157
|
+
'@pnpm/exe.linux-arm64-musl': 12.16.1
|
|
158
|
+
'@pnpm/exe.linux-ppc64': 12.16.1
|
|
159
|
+
'@pnpm/exe.linux-riscv64': 12.16.1
|
|
160
|
+
'@pnpm/exe.linux-s390x': 12.16.1
|
|
161
|
+
'@pnpm/exe.linux-x64': 12.16.1
|
|
162
|
+
'@pnpm/exe.linux-x64-musl': 12.16.1
|
|
163
|
+
'@pnpm/exe.win32-arm64': 12.16.1
|
|
164
|
+
'@pnpm/exe.win32-x64': 12.16.1
|
|
165
|
+
|
|
166
|
+
pnpm@12.16.1:
|
|
167
|
+
optionalDependencies:
|
|
168
|
+
'@pnpm/exe.android-arm64': 12.16.1
|
|
169
|
+
'@pnpm/exe.android-x64': 12.16.1
|
|
170
|
+
'@pnpm/exe.darwin-arm64': 12.16.1
|
|
171
|
+
'@pnpm/exe.darwin-x64': 12.16.1
|
|
172
|
+
'@pnpm/exe.freebsd-x64': 12.16.1
|
|
173
|
+
'@pnpm/exe.linux-arm64': 12.16.1
|
|
174
|
+
'@pnpm/exe.linux-arm64-musl': 12.16.1
|
|
175
|
+
'@pnpm/exe.linux-ppc64': 12.16.1
|
|
176
|
+
'@pnpm/exe.linux-riscv64': 12.16.1
|
|
177
|
+
'@pnpm/exe.linux-s390x': 12.16.1
|
|
178
|
+
'@pnpm/exe.linux-x64': 12.16.1
|
|
179
|
+
'@pnpm/exe.linux-x64-musl': 12.16.1
|
|
180
|
+
'@pnpm/exe.win32-arm64': 12.16.1
|
|
181
|
+
'@pnpm/exe.win32-x64': 12.16.1
|
|
182
|
+
|
|
183
|
+
---
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Cloudflare Workers static assets apply this file to every asset response.
|
|
2
|
+
#
|
|
3
|
+
# A baseline that cannot break a page. No Content-Security-Policy on purpose: <Head /> emits
|
|
4
|
+
# inline scripts (the consent stub, the colour-scheme bootstrap), so a strict policy needs
|
|
5
|
+
# per-site nonces or hashes — add one deliberately once you know which third-party tags you load.
|
|
6
|
+
/*
|
|
7
|
+
X-Content-Type-Options: nosniff
|
|
8
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
9
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
|
|
10
|
+
X-Frame-Options: SAMEORIGIN
|
|
@@ -66,6 +66,12 @@ cfg.r2_buckets = (cfg.r2_buckets ?? []).map((r) =>
|
|
|
66
66
|
r.binding === 'MEDIA' ? { ...r, bucket_name: `${r.bucket_name}-dev` } : r,
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
+
// No cron on dev. Dev's database is a copy of prod (scripts/refresh-dev.mjs),
|
|
70
|
+
// so it carries prod's scheduled publishes and plugin jobs; leaving the trigger
|
|
71
|
+
// on would run them a second time against the copy. The cost: scheduled
|
|
72
|
+
// publishing can't be exercised on dev.
|
|
73
|
+
delete cfg.triggers;
|
|
74
|
+
|
|
69
75
|
// Flat config — no nested environments.
|
|
70
76
|
delete cfg.env;
|
|
71
77
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Refreshes the DEV D1 database (and, optionally, the dev media bucket) from
|
|
2
|
+
// PRODUCTION. One direction only: prod is the source of truth for content, dev
|
|
3
|
+
// is a disposable copy used to rehearse code and schema changes against real
|
|
4
|
+
// data (EmDash's own "rehearse changes on a preview environment" pattern).
|
|
5
|
+
//
|
|
6
|
+
// node scripts/refresh-dev.mjs # wipe dev D1, import prod's export
|
|
7
|
+
// node scripts/refresh-dev.mjs --dry # run every check, change nothing
|
|
8
|
+
//
|
|
9
|
+
// Names come from wrangler.jsonc, the same way scripts/build-dev-wrangler.mjs
|
|
10
|
+
// derives them, so the two can't disagree about which resources are "dev":
|
|
11
|
+
//
|
|
12
|
+
// D1 <database_name> → <database_name>-dev (id from $DEV_D1_ID)
|
|
13
|
+
// R2 <bucket_name> → <bucket_name>-dev
|
|
14
|
+
//
|
|
15
|
+
// SAFETY, in the order it is enforced:
|
|
16
|
+
// 1. $DEV_D1_ID must be set, and `wrangler d1 info <name>-dev` must report
|
|
17
|
+
// that same id. A name that resolves to a different database than the id
|
|
18
|
+
// we were told about means one of the two is stale.
|
|
19
|
+
// 2. The dev id must differ from prod's. This script DROPS every table in
|
|
20
|
+
// the target, so pointing it at prod would erase production — the exact
|
|
21
|
+
// thing the dev/prod split exists to prevent.
|
|
22
|
+
// Both checks run before the first write, and `--dry` stops after them.
|
|
23
|
+
//
|
|
24
|
+
// Not copied: anything outside D1/R2. Sessions live in the Astro session store
|
|
25
|
+
// (KV), not D1, so nothing is carried over there. The preview-link secret IS in
|
|
26
|
+
// D1 — set EMDASH_PREVIEW_SECRET on the dev worker so prod preview links don't
|
|
27
|
+
// verify on dev (see SETUP.html).
|
|
28
|
+
//
|
|
29
|
+
// Media is copied only when R2 S3 credentials are present, because wrangler has
|
|
30
|
+
// no bucket-to-bucket sync and rclone needs the S3 API:
|
|
31
|
+
// R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, CLOUDFLARE_ACCOUNT_ID
|
|
32
|
+
// Without them the database is refreshed and media is left alone, with a note —
|
|
33
|
+
// dev then shows broken images for anything uploaded since the last copy.
|
|
34
|
+
//
|
|
35
|
+
// KNOWN LIMIT: `wrangler d1 export` does not support virtual tables, so an FTS5
|
|
36
|
+
// search table breaks the export. EmDash rebuilds its search index on the first
|
|
37
|
+
// search call, so if an export ever fails on one, drop it from prod's schema
|
|
38
|
+
// dump rather than working around it here.
|
|
39
|
+
|
|
40
|
+
import { execFileSync } from 'node:child_process';
|
|
41
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
42
|
+
import { tmpdir } from 'node:os';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
import { pathToFileURL } from 'node:url';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pull the D1 database and R2 bucket names out of a wrangler.jsonc source.
|
|
48
|
+
* Regex rather than a JSONC parser: the template carries no parser, the file
|
|
49
|
+
* is ours, and these two keys are the only thing read. `//` comment lines are
|
|
50
|
+
* stripped first so a commented-out example block can't be picked up.
|
|
51
|
+
*/
|
|
52
|
+
export function parseBindingNames(jsonc) {
|
|
53
|
+
const code = jsonc
|
|
54
|
+
.split('\n')
|
|
55
|
+
.filter((line) => !line.trim().startsWith('//'))
|
|
56
|
+
.join('\n');
|
|
57
|
+
const database = /"database_name"\s*:\s*"([^"]+)"/.exec(code)?.[1];
|
|
58
|
+
const bucket = /"bucket_name"\s*:\s*"([^"]+)"/.exec(code)?.[1];
|
|
59
|
+
return { database, bucket };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Throws unless it is safe to wipe `devId`'s database. Pure, so the one check
|
|
64
|
+
* that stands between this script and production is asserted in a test.
|
|
65
|
+
*/
|
|
66
|
+
export function assertSafeTarget({ devName, devId, devReportedId, prodId }) {
|
|
67
|
+
if (!devId) throw new Error('DEV_D1_ID is not set.');
|
|
68
|
+
if (!devName.endsWith('-dev')) throw new Error(`Refusing: "${devName}" is not a -dev database.`);
|
|
69
|
+
if (devReportedId !== devId) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`DEV_D1_ID (${devId}) does not match "${devName}" (${devReportedId}). One of them is stale.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (devId === prodId) throw new Error('Refusing: the dev database id is the production id.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* SQL that empties a database: views, then virtual tables, then ordinary
|
|
79
|
+
* tables. `rows` is `{ name, type, sql }` from sqlite_master. A virtual table
|
|
80
|
+
* owns shadow tables named `<virtual>_*`, which SQLite refuses to drop on their
|
|
81
|
+
* own and removes with the parent, so those are skipped.
|
|
82
|
+
*/
|
|
83
|
+
export function dropStatements(rows) {
|
|
84
|
+
const virtual = rows.filter((r) => /^CREATE VIRTUAL TABLE/i.test(r.sql ?? ''));
|
|
85
|
+
const isShadow = (name) => virtual.some((v) => name.startsWith(`${v.name}_`));
|
|
86
|
+
const tables = rows.filter(
|
|
87
|
+
(r) => r.type === 'table' && !virtual.includes(r) && !isShadow(r.name),
|
|
88
|
+
);
|
|
89
|
+
const q = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
90
|
+
return [
|
|
91
|
+
'PRAGMA defer_foreign_keys = true;',
|
|
92
|
+
...rows.filter((r) => r.type === 'view').map((r) => `DROP VIEW IF EXISTS ${q(r.name)};`),
|
|
93
|
+
...virtual.map((r) => `DROP TABLE IF EXISTS ${q(r.name)};`),
|
|
94
|
+
...tables.map((r) => `DROP TABLE IF EXISTS ${q(r.name)};`),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const wrangler = (args, opts = {}) =>
|
|
99
|
+
execFileSync('pnpm', ['exec', 'wrangler', ...args], { encoding: 'utf8', ...opts });
|
|
100
|
+
|
|
101
|
+
const d1Id = (name) => JSON.parse(wrangler(['d1', 'info', name, '--json'])).uuid;
|
|
102
|
+
|
|
103
|
+
function main() {
|
|
104
|
+
const dry = process.argv.includes('--dry');
|
|
105
|
+
const wranglerConfig = 'wrangler.jsonc';
|
|
106
|
+
if (!existsSync(wranglerConfig))
|
|
107
|
+
throw new Error(`Run from the project root (no ${wranglerConfig}).`);
|
|
108
|
+
|
|
109
|
+
const { database, bucket } = parseBindingNames(readFileSync(wranglerConfig, 'utf8'));
|
|
110
|
+
if (!database) throw new Error(`No D1 database_name found in ${wranglerConfig}.`);
|
|
111
|
+
const devDatabase = `${database}-dev`;
|
|
112
|
+
|
|
113
|
+
assertSafeTarget({
|
|
114
|
+
devName: devDatabase,
|
|
115
|
+
devId: process.env.DEV_D1_ID,
|
|
116
|
+
devReportedId: d1Id(devDatabase),
|
|
117
|
+
prodId: d1Id(database),
|
|
118
|
+
});
|
|
119
|
+
console.log(`OK to refresh ${devDatabase} from ${database}.`);
|
|
120
|
+
|
|
121
|
+
const dir = mkdtempSync(join(tmpdir(), 'refresh-dev-'));
|
|
122
|
+
const dump = join(dir, 'prod.sql');
|
|
123
|
+
const wipe = join(dir, 'wipe.sql');
|
|
124
|
+
|
|
125
|
+
const master = JSON.parse(
|
|
126
|
+
wrangler([
|
|
127
|
+
'd1',
|
|
128
|
+
'execute',
|
|
129
|
+
devDatabase,
|
|
130
|
+
'--remote',
|
|
131
|
+
'--json',
|
|
132
|
+
'--command',
|
|
133
|
+
"SELECT name, type, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' AND name NOT LIKE '\\_cf\\_%' ESCAPE '\\' AND type IN ('table','view')",
|
|
134
|
+
]),
|
|
135
|
+
)[0].results;
|
|
136
|
+
writeFileSync(wipe, `${dropStatements(master).join('\n')}\n`);
|
|
137
|
+
|
|
138
|
+
if (dry) {
|
|
139
|
+
console.log(`--dry: would drop ${master.length} objects from ${devDatabase} and import prod.`);
|
|
140
|
+
console.log(
|
|
141
|
+
bucket ? `--dry: would copy ${bucket} → ${bucket}-dev.` : '--dry: no R2 bucket found.',
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
wrangler(['d1', 'export', database, '--remote', '--output', dump], { stdio: 'inherit' });
|
|
147
|
+
wrangler(['d1', 'execute', devDatabase, '--remote', '--file', wipe], { stdio: 'inherit' });
|
|
148
|
+
wrangler(['d1', 'execute', devDatabase, '--remote', '--file', dump], { stdio: 'inherit' });
|
|
149
|
+
console.log(`Imported ${database} into ${devDatabase}.`);
|
|
150
|
+
|
|
151
|
+
const { R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, CLOUDFLARE_ACCOUNT_ID } = process.env;
|
|
152
|
+
if (!bucket) return;
|
|
153
|
+
if (!R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !CLOUDFLARE_ACCOUNT_ID) {
|
|
154
|
+
console.log(
|
|
155
|
+
`Skipped media: R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY not set, ${bucket}-dev left as is.`,
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Credentials ride rclone's env-var config, not the command line, so the
|
|
160
|
+
// secret never appears in a process listing.
|
|
161
|
+
const env = {
|
|
162
|
+
...process.env,
|
|
163
|
+
RCLONE_CONFIG_R2_TYPE: 's3',
|
|
164
|
+
RCLONE_CONFIG_R2_PROVIDER: 'Cloudflare',
|
|
165
|
+
RCLONE_CONFIG_R2_ENDPOINT: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
166
|
+
RCLONE_CONFIG_R2_ACCESS_KEY_ID: R2_ACCESS_KEY_ID,
|
|
167
|
+
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: R2_SECRET_ACCESS_KEY,
|
|
168
|
+
};
|
|
169
|
+
// `sync`, not `copy`: media deleted from prod should disappear from dev too.
|
|
170
|
+
execFileSync('rclone', ['sync', `R2:${bucket}`, `R2:${bucket}-dev`], { env, stdio: 'inherit' });
|
|
171
|
+
console.log(`Synced ${bucket} → ${bucket}-dev.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Importable for tests without running; executed when invoked as a script.
|
|
175
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
176
|
+
try {
|
|
177
|
+
main();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(`ERROR: ${err.message}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|