create-meith 0.17.2 → 0.19.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/LICENSE.md +21 -165
- package/dist/bin.mjs +640 -69
- package/package.json +2 -2
- package/src/bin.ts +1 -1
- package/src/index.ts +8 -0
- package/src/scaffold.ts +686 -67
package/dist/bin.mjs
CHANGED
|
@@ -8,6 +8,59 @@ import { promisify } from "node:util";
|
|
|
8
8
|
|
|
9
9
|
// src/scaffold.ts
|
|
10
10
|
var DEFAULT_REPOSITORY_URL = "https://github.com/meith-dev/meith";
|
|
11
|
+
var DEFAULT_TEMPLATE_REPOSITORY_URL = "https://github.com/meith-dev/vercel-template";
|
|
12
|
+
var NEXT_VERSION = "16.3.1";
|
|
13
|
+
var AT_ROOT_FLAG = "--at-root";
|
|
14
|
+
var MATERIALIZED_AT_ROOT = [
|
|
15
|
+
"app",
|
|
16
|
+
"src",
|
|
17
|
+
"public",
|
|
18
|
+
"next.config.mjs",
|
|
19
|
+
"postcss.config.mjs",
|
|
20
|
+
"components.json",
|
|
21
|
+
"instrumentation.ts",
|
|
22
|
+
"proxy.ts",
|
|
23
|
+
"tsconfig.json",
|
|
24
|
+
"next-env.d.ts"
|
|
25
|
+
];
|
|
26
|
+
var VERCEL_BUILD_COMMAND = `community migrate && forum-web build ${AT_ROOT_FLAG}`;
|
|
27
|
+
var TICK_PATH = "/api/system/tick";
|
|
28
|
+
var TICK_SCHEDULE = "* * * * *";
|
|
29
|
+
var MATERIALIZED_PUBLIC = [
|
|
30
|
+
"placeholder-logo.png",
|
|
31
|
+
"placeholder-logo.svg",
|
|
32
|
+
"placeholder-user.jpg",
|
|
33
|
+
"placeholder.jpg",
|
|
34
|
+
"placeholder.svg",
|
|
35
|
+
"sw.js"
|
|
36
|
+
];
|
|
37
|
+
var AT_ROOT_IGNORE_PATHS = MATERIALIZED_AT_ROOT.flatMap(
|
|
38
|
+
(entry) => entry === "public" ? MATERIALIZED_PUBLIC.map((file) => `/public/${file}`) : [`/${entry}`]
|
|
39
|
+
).join("\n");
|
|
40
|
+
var AT_ROOT_IGNORES = `# What \`forum-web ${AT_ROOT_FLAG}\` writes into this directory: @meith/web's own
|
|
41
|
+
# Next app, materialized here rather than into .meith/app so that the build
|
|
42
|
+
# artefact lands at ./.next, where Vercel's Next.js builder reads it. Every
|
|
43
|
+
# path here belongs to the framework and is rewritten on every build.
|
|
44
|
+
#
|
|
45
|
+
# public/ is listed file by file rather than as a directory, because that one
|
|
46
|
+
# is shared: forum-web decides what it owns per file, so this board's own
|
|
47
|
+
# public/ads.txt, public/.well-known/... or domain-verification file sits
|
|
48
|
+
# beside the framework's and is tracked normally.
|
|
49
|
+
#
|
|
50
|
+
# app/ and src/ are ignored WHOLESALE, and that has a consequence worth
|
|
51
|
+
# knowing before you go looking for it: a file you add under either is left
|
|
52
|
+
# alone by the build and still never committed, so it works locally and is
|
|
53
|
+
# simply absent from the deploy, which builds from what git has. Extend the
|
|
54
|
+
# board with a plugin or a theme instead \u2014 the forum loads those from
|
|
55
|
+
# community.config.ts, and they are yours to commit. forum-web prints a
|
|
56
|
+
# warning naming any file of yours it finds there.
|
|
57
|
+
#
|
|
58
|
+
# For the rest, a build refuses rather than overwriting a file it did not
|
|
59
|
+
# write, and names it. The two exceptions are tsconfig.json and
|
|
60
|
+
# next-env.d.ts: forum-web generates those from scratch every run rather than
|
|
61
|
+
# copying them, so it cannot tell one of yours from a stale one of its own
|
|
62
|
+
# and replaces them without asking.
|
|
63
|
+
${AT_ROOT_IGNORE_PATHS}`;
|
|
11
64
|
var NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/;
|
|
12
65
|
function validateName(name) {
|
|
13
66
|
if (name === "") return "A project name is required.";
|
|
@@ -20,8 +73,305 @@ function validateName(name) {
|
|
|
20
73
|
}
|
|
21
74
|
return null;
|
|
22
75
|
}
|
|
76
|
+
var ENV_REQUIRED_HEADING = `# \u2500\u2500\u2500 Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`;
|
|
77
|
+
var ENV_OPTIONAL_HEADING = `# \u2500\u2500\u2500 Optional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`;
|
|
78
|
+
var ENV_DATABASE_URL_PROSE = `# Your Postgres connection string.
|
|
79
|
+
#
|
|
80
|
+
# If it is a managed database that offers a TRANSACTION-MODE POOLER string, use
|
|
81
|
+
# that rather than the direct one \u2014 Neon, Supabase and their kind hand out both,
|
|
82
|
+
# and on the direct string a board works in testing and starts refusing
|
|
83
|
+
# connections under the first real traffic, with an error that names the
|
|
84
|
+
# database rather than the cause. Your own Postgres, with a fixed number of
|
|
85
|
+
# processes in front of it, does not need one.`;
|
|
86
|
+
var ENV_DIRECT_DATABASE_URL_PROSE = `# The other half of that pair: the DIRECT (non-pooler) string, used only by
|
|
87
|
+
# \`community migrate\` and \`community backup\`. Migrations hold a session-level
|
|
88
|
+
# advisory lock so that two deploys landing together queue instead of both
|
|
89
|
+
# applying the same migration, and a transaction-mode pooler cannot hold that
|
|
90
|
+
# lock: it takes the connection back the moment the lock statement ends, which
|
|
91
|
+
# leaves the lock on a backend another client gets. Set both and each gets the
|
|
92
|
+
# connection it needs; set only DATABASE_URL and migrations use it too, which is
|
|
93
|
+
# right for a Postgres you run yourself.`;
|
|
94
|
+
var ENV_AUTH_SECRET_PROSE = `# Session and token signing. No default, deliberately: a shipped default is a
|
|
95
|
+
# board every reader of the source can sign a session for.
|
|
96
|
+
#
|
|
97
|
+
# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"`;
|
|
98
|
+
var ENV_TICK_SECRET_PROSE = `# The shared secret the tick caller presents to GET /api/system/tick. Generate
|
|
99
|
+
# it the same way. Without it the tick is unauthenticated, and the tick is how
|
|
100
|
+
# bans expire and digests send.`;
|
|
101
|
+
var ENV_DATA_SOURCE_PROSE = `# fixture = deterministic in-memory sample data, no database needed. This is
|
|
102
|
+
# what \`npm run build\` uses, and what a checkout with no database falls back to.`;
|
|
103
|
+
var ENV_APP_URL_PROSE = `# Absolute, no trailing slash. Used in mail, feeds and canonical URLs \u2014 every
|
|
104
|
+
# place a relative URL cannot work because there is no request to be relative to.
|
|
105
|
+
#
|
|
106
|
+
# Optional: leave it blank and the installer asks, prefilled from the address you
|
|
107
|
+
# load /install at, and stores the answer on the board where the settings screen
|
|
108
|
+
# can change it without a redeploy. Set it here and it wins outright.`;
|
|
109
|
+
var ENV_SMTP_MAIL_BLOCK = `# Mail. Leave these alone and the installer asks for mail on first run, storing
|
|
110
|
+
# it on the board \u2014 a settings screen with a test button, no redeploy. Set
|
|
111
|
+
# MAIL_DRIVER here instead and the environment wins outright, which is what you
|
|
112
|
+
# want if the credential must not live in the database.
|
|
113
|
+
#
|
|
114
|
+
# The default sends NOTHING: each message goes to the server log, so password
|
|
115
|
+
# reset fails silently until mail is configured one way or the other.
|
|
116
|
+
# MAIL_DRIVER=smtp
|
|
117
|
+
# MAIL_SMTP_HOST=smtp.example.com
|
|
118
|
+
# MAIL_SMTP_PORT=465
|
|
119
|
+
# MAIL_SMTP_SECURITY=tls # tls (465) | starttls (587) | none
|
|
120
|
+
# MAIL_SMTP_USERNAME=
|
|
121
|
+
# MAIL_SMTP_PASSWORD=
|
|
122
|
+
# MAIL_FROM=noreply@yourdomain.com`;
|
|
123
|
+
function selfHostEnvExample(name) {
|
|
124
|
+
return `# ${name} \u2014 environment.
|
|
125
|
+
#
|
|
126
|
+
# Copy to .env.local for development. On the server this is \`.env\` beside the
|
|
127
|
+
# compose file; nothing here belongs in git.
|
|
128
|
+
|
|
129
|
+
${ENV_REQUIRED_HEADING}
|
|
130
|
+
|
|
131
|
+
${ENV_DATABASE_URL_PROSE}
|
|
132
|
+
DATABASE_URL=
|
|
133
|
+
|
|
134
|
+
${ENV_DIRECT_DATABASE_URL_PROSE}
|
|
135
|
+
# DIRECT_DATABASE_URL=
|
|
136
|
+
|
|
137
|
+
${ENV_AUTH_SECRET_PROSE}
|
|
138
|
+
AUTH_SECRET=
|
|
139
|
+
|
|
140
|
+
${ENV_TICK_SECRET_PROSE}
|
|
141
|
+
TICK_SECRET=
|
|
142
|
+
|
|
143
|
+
${ENV_OPTIONAL_HEADING}
|
|
144
|
+
|
|
145
|
+
${ENV_DATA_SOURCE_PROSE}
|
|
146
|
+
DATA_SOURCE=postgres
|
|
147
|
+
|
|
148
|
+
${ENV_APP_URL_PROSE}
|
|
149
|
+
APP_URL=
|
|
150
|
+
|
|
151
|
+
${ENV_SMTP_MAIL_BLOCK}
|
|
152
|
+
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
function vercelEnvExample(name) {
|
|
156
|
+
return `# ${name} \u2014 environment, on Vercel.
|
|
157
|
+
#
|
|
158
|
+
# Nothing on Vercel reads this file. The platform holds each of these as a
|
|
159
|
+
# project environment variable, and the Deploy Button in README.md asks for the
|
|
160
|
+
# ones it cannot provision itself. This is the reference for what they mean \u2014
|
|
161
|
+
# and the file to copy to .env.local to run the same board on your own machine.
|
|
162
|
+
|
|
163
|
+
# \u2500\u2500\u2500 Drivers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
164
|
+
|
|
165
|
+
# An instance is created for a request, may be frozen between requests, and is
|
|
166
|
+
# destroyed without warning; it has a writable /tmp nothing else can read and no
|
|
167
|
+
# background process of its own. Every driver below therefore keeps its state
|
|
168
|
+
# somewhere outside the instance, and these five values are not a default to
|
|
169
|
+
# tune \u2014 they are the one combination the board supports on functions.
|
|
170
|
+
#
|
|
171
|
+
# DATA_SOURCE=fixture is a read-only sample board with no write side.
|
|
172
|
+
# QUEUE_DRIVER=memory loses every queued job when the instance goes away, which
|
|
173
|
+
# is after almost every request, and the board already refuses it in production.
|
|
174
|
+
# CACHE_DRIVER=next and memory cache inside the process, so each instance serves
|
|
175
|
+
# its own stale copy for up to a minute. FILESTORE_DRIVER=local writes to a disk
|
|
176
|
+
# no other instance can read and that is discarded with the instance \u2014 on Vercel
|
|
177
|
+
# the board refuses it outright rather than losing uploads quietly.
|
|
178
|
+
#
|
|
179
|
+
# The deploy form asks for none of them. On Vercel the board works each one out
|
|
180
|
+
# from what the linked stores publish: a DATABASE_URL means postgres for the
|
|
181
|
+
# data source and the queue, a Redis connection string means CACHE_DRIVER=redis,
|
|
182
|
+
# a Blob store's read-write token means FILESTORE_DRIVER=blob, and a
|
|
183
|
+
# RESEND_API_KEY with a MAIL_FROM beside it means mail over the provider's HTTPS
|
|
184
|
+
# API. Setting one here overrides the derivation, which is what this file is for
|
|
185
|
+
# when you copy it to .env.local.
|
|
186
|
+
#
|
|
187
|
+
# Every one of those derivations is scoped to Vercel, and each fires only from a
|
|
188
|
+
# value that is unambiguously the thing itself \u2014 a redis:// or rediss:// URL, a
|
|
189
|
+
# read-write token. A board you run anywhere else is untouched by all of it and
|
|
190
|
+
# still takes these values from this file, exactly as it did before.
|
|
191
|
+
#
|
|
192
|
+
# A derivation that cannot resolve is a configuration error, not an invitation
|
|
193
|
+
# to pick something safe-looking. On Vercel, with the cache or the object store
|
|
194
|
+
# missing, the board refuses to boot and names every variable it looked at. It
|
|
195
|
+
# will not quietly cache inside the instance, and it will not quietly write
|
|
196
|
+
# uploads to a disk that is about to disappear.
|
|
197
|
+
DATA_SOURCE=postgres
|
|
198
|
+
QUEUE_DRIVER=postgres
|
|
199
|
+
CACHE_DRIVER=redis
|
|
200
|
+
FILESTORE_DRIVER=blob
|
|
201
|
+
MAIL_DRIVER=http
|
|
202
|
+
|
|
203
|
+
${ENV_REQUIRED_HEADING}
|
|
204
|
+
|
|
205
|
+
${ENV_DATABASE_URL_PROSE}
|
|
206
|
+
DATABASE_URL=
|
|
207
|
+
|
|
208
|
+
${ENV_DIRECT_DATABASE_URL_PROSE}
|
|
209
|
+
#
|
|
210
|
+
# On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
|
|
211
|
+
# here is the pooler string, the build runs \`community migrate\` against it, and
|
|
212
|
+
# /install takes the second of those two session locks on first run. Left blank,
|
|
213
|
+
# the board reads Neon's own direct string \u2014 \`DATABASE_URL_UNPOOLED\` first, then
|
|
214
|
+
# \`POSTGRES_URL_NON_POOLING\` \u2014 and refuses to boot if neither is there, naming
|
|
215
|
+
# both. Never \`POSTGRES_URL\`: that one is pooled.
|
|
216
|
+
DIRECT_DATABASE_URL=
|
|
217
|
+
|
|
218
|
+
# The shared cache \u2014 a Redis or Valkey endpoint, \`rediss://\` for TLS. Redis
|
|
219
|
+
# holds cache entries and nothing else: losing it costs the board a warm cache,
|
|
220
|
+
# not data, and signs nobody out. Left blank on Vercel, the board reads the
|
|
221
|
+
# Upstash store's own \`KV_URL\`, which is the one variable it publishes that
|
|
222
|
+
# speaks the Redis protocol \u2014 \`KV_REST_API_URL\` is an HTTPS endpoint and is
|
|
223
|
+
# never used for this. A name we do not know goes here by hand.
|
|
224
|
+
REDIS_URL=
|
|
225
|
+
|
|
226
|
+
${ENV_AUTH_SECRET_PROSE}
|
|
227
|
+
AUTH_SECRET=
|
|
228
|
+
|
|
229
|
+
${ENV_TICK_SECRET_PROSE}
|
|
230
|
+
#
|
|
231
|
+
# Vercel Cron sends \`Authorization: Bearer <CRON_SECRET>\` and cannot be told to
|
|
232
|
+
# send any other name, so CRON_SECRET is the one to set here. The board accepts
|
|
233
|
+
# either name, and both when both are set. Whichever you use, 32 characters is
|
|
234
|
+
# the floor \u2014 stricter than the 16 Vercel's own cron documentation suggests, so
|
|
235
|
+
# a secret generated by following those instructions is rejected here.
|
|
236
|
+
CRON_SECRET=
|
|
237
|
+
# TICK_SECRET=
|
|
238
|
+
|
|
239
|
+
# Uploads, in the Vercel Blob store the Deploy Button provisions. A store
|
|
240
|
+
# attached to the project publishes BLOB_STORE_ID and nothing else \u2014 no token \u2014
|
|
241
|
+
# because the SDK authenticates with the deployment's own OIDC identity: the
|
|
242
|
+
# board hands it the store id and lets it fetch the credential. There is nothing
|
|
243
|
+
# to type and nothing to mistype, and FILESTORE_DRIVER=blob derives from this
|
|
244
|
+
# variable being present.
|
|
245
|
+
#
|
|
246
|
+
# BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
|
|
247
|
+
# store. Set it when something has to reach the store from OUTSIDE a Vercel
|
|
248
|
+
# deployment \u2014 \`community backup\` run on your own machine is the case that
|
|
249
|
+
# matters \u2014 because there is no OIDC identity there to borrow. Set both and the
|
|
250
|
+
# board prefers the store id, unless the token names a different store, in which
|
|
251
|
+
# case the token wins: naming another store is a deliberate act.
|
|
252
|
+
#
|
|
253
|
+
# Every object is written with private access: an object URL is not a public
|
|
254
|
+
# link, and member content is served by the board, which is where permissions
|
|
255
|
+
# are checked. An upload is held whole in the instance's memory on the way in
|
|
256
|
+
# and on the way out, so the function's memory limit, not the store, is what
|
|
257
|
+
# caps a file.
|
|
258
|
+
BLOB_STORE_ID=
|
|
259
|
+
BLOB_READ_WRITE_TOKEN=
|
|
260
|
+
|
|
261
|
+
# Uploads in an S3-compatible bucket instead \u2014 AWS, R2, MinIO, Spaces. This is
|
|
262
|
+
# the portable option, and the one every other deployment of this board uses:
|
|
263
|
+
# a bucket is a thing you hold, and it is not the only way to get the objects
|
|
264
|
+
# out of it. Set FILESTORE_DRIVER=s3 above and the first four below; boot fails
|
|
265
|
+
# naming any that are missing. S3_ENDPOINT is for anything that is not AWS and
|
|
266
|
+
# switches the client to path-style addressing; set S3_REGION=auto for R2.
|
|
267
|
+
# S3_PUBLIC_BASE_URL is the host objects are *served* from when that is not the
|
|
268
|
+
# API endpoint.
|
|
269
|
+
# S3_BUCKET=
|
|
270
|
+
# S3_REGION=
|
|
271
|
+
# S3_ACCESS_KEY_ID=
|
|
272
|
+
# S3_SECRET_ACCESS_KEY=
|
|
273
|
+
# S3_ENDPOINT=
|
|
274
|
+
# S3_PUBLIC_BASE_URL=
|
|
275
|
+
|
|
276
|
+
# Mail over the provider's own HTTPS API, on 443 \u2014 the one outbound path a
|
|
277
|
+
# function can rely on. SMTP on port 25 is blocked by serverless egress and the
|
|
278
|
+
# board refuses it on Vercel; 587 with STARTTLS may work, but an API does not
|
|
279
|
+
# depend on the platform's egress rules staying as they are.
|
|
280
|
+
#
|
|
281
|
+
# MAIL_FROM is yours to decide, and it belongs to the step that adds Resend
|
|
282
|
+
# rather than to the deploy: it must be an address at a domain the provider has
|
|
283
|
+
# verified for you, no default is right, and until a provider is added the board
|
|
284
|
+
# cannot send from any address at all. Set it alongside the integration.
|
|
285
|
+
MAIL_FROM=
|
|
286
|
+
|
|
287
|
+
# Add the Resend integration to the project from Vercel's marketplace and it
|
|
288
|
+
# publishes its key under this name, which the board reads: with RESEND_API_KEY
|
|
289
|
+
# set, and MAIL_FROM beside it, the board sends over Resend's HTTPS API and
|
|
290
|
+
# needs neither of the two variables below. The mail driver itself is a plain
|
|
291
|
+
# JSON-over-HTTPS sender and is not Resend-specific \u2014 this is one injected name
|
|
292
|
+
# bridged to the generic pair, not a provider baked into the board.
|
|
293
|
+
RESEND_API_KEY=
|
|
294
|
+
|
|
295
|
+
# Any other provider with the same shape \u2014 a bearer token and an endpoint that
|
|
296
|
+
# accepts {from, to, subject, text, html, reply_to}. Set BOTH, plus
|
|
297
|
+
# MAIL_DRIVER=http above; they do not turn the driver on by themselves, and only
|
|
298
|
+
# RESEND_API_KEY implies it.
|
|
299
|
+
#
|
|
300
|
+
# Setting just one of them stands the Resend bridge down completely, on purpose:
|
|
301
|
+
# the board will not hand a key issued for Resend to an endpoint you chose, nor
|
|
302
|
+
# aim your token at Resend. Boot fails naming the half you left out. Delete
|
|
303
|
+
# RESEND_API_KEY once you have moved off Resend.
|
|
304
|
+
# MAIL_HTTP_ENDPOINT=
|
|
305
|
+
# MAIL_HTTP_TOKEN=
|
|
306
|
+
|
|
307
|
+
${ENV_OPTIONAL_HEADING}
|
|
308
|
+
|
|
309
|
+
${ENV_APP_URL_PROSE}
|
|
310
|
+
APP_URL=
|
|
311
|
+
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
function envExample(name, target) {
|
|
315
|
+
return target === "vercel" ? vercelEnvExample(name) : selfHostEnvExample(name);
|
|
316
|
+
}
|
|
317
|
+
var SELF_HOST_DEPLOY_KIT = [
|
|
318
|
+
".dockerignore",
|
|
319
|
+
".github/workflows/build.yml",
|
|
320
|
+
"Dockerfile",
|
|
321
|
+
"docker-compose.yml",
|
|
322
|
+
"docker-entrypoint.sh",
|
|
323
|
+
"docker-healthcheck.sh"
|
|
324
|
+
];
|
|
325
|
+
var VERCEL_DERIVED_DRIVERS = [
|
|
326
|
+
"DATA_SOURCE=postgres",
|
|
327
|
+
"QUEUE_DRIVER=postgres",
|
|
328
|
+
"CACHE_DRIVER=redis",
|
|
329
|
+
"FILESTORE_DRIVER=blob",
|
|
330
|
+
"MAIL_DRIVER=http"
|
|
331
|
+
];
|
|
332
|
+
var VERCEL_PROMPTED_ENV = ["AUTH_SECRET", "CRON_SECRET"];
|
|
333
|
+
var VERCEL_MARKETPLACE_STORES = [
|
|
334
|
+
{ type: "integration", integrationSlug: "neon", productSlug: "neon", protocol: "storage" },
|
|
335
|
+
{
|
|
336
|
+
type: "integration",
|
|
337
|
+
integrationSlug: "upstash",
|
|
338
|
+
productSlug: "upstash-kv",
|
|
339
|
+
protocol: "storage"
|
|
340
|
+
},
|
|
341
|
+
{ type: "blob" }
|
|
342
|
+
];
|
|
343
|
+
function deployButtonUrl(templateRepositoryUrl) {
|
|
344
|
+
const params = new URLSearchParams([
|
|
345
|
+
["repository-url", templateRepositoryUrl],
|
|
346
|
+
["project-name", "meith-board"],
|
|
347
|
+
["repository-name", "meith-board"],
|
|
348
|
+
["env", VERCEL_PROMPTED_ENV.join(",")],
|
|
349
|
+
[
|
|
350
|
+
"envDescription",
|
|
351
|
+
"Two secrets, generated rather than chosen \u2014 32 characters or more each. Everything else the board reads from the database, cache and blob store this form links."
|
|
352
|
+
],
|
|
353
|
+
["envLink", `${templateRepositoryUrl}/blob/main/.env.example`],
|
|
354
|
+
["stores", JSON.stringify(VERCEL_MARKETPLACE_STORES)],
|
|
355
|
+
["skippable-integrations", "1"]
|
|
356
|
+
]);
|
|
357
|
+
return `https://vercel.com/new/clone?${params.toString()}`;
|
|
358
|
+
}
|
|
359
|
+
function vercelJson() {
|
|
360
|
+
return `${JSON.stringify(
|
|
361
|
+
{
|
|
362
|
+
framework: "nextjs",
|
|
363
|
+
buildCommand: VERCEL_BUILD_COMMAND,
|
|
364
|
+
crons: [{ path: TICK_PATH, schedule: TICK_SCHEDULE }]
|
|
365
|
+
},
|
|
366
|
+
null,
|
|
367
|
+
2
|
|
368
|
+
)}
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
23
371
|
function scaffold(options) {
|
|
24
372
|
const { name, version, repositoryUrl } = options;
|
|
373
|
+
const target = options.target ?? "self-host";
|
|
374
|
+
const atRootFlag = target === "vercel" ? ` ${AT_ROOT_FLAG}` : "";
|
|
25
375
|
const files = /* @__PURE__ */ new Map();
|
|
26
376
|
files.set(
|
|
27
377
|
"package.json",
|
|
@@ -32,15 +382,16 @@ function scaffold(options) {
|
|
|
32
382
|
private: true,
|
|
33
383
|
type: "module",
|
|
34
384
|
scripts: {
|
|
35
|
-
dev:
|
|
36
|
-
build:
|
|
37
|
-
start:
|
|
385
|
+
dev: `forum-web dev${atRootFlag}`,
|
|
386
|
+
build: `forum-web build${atRootFlag}`,
|
|
387
|
+
start: `forum-web start${atRootFlag}`,
|
|
38
388
|
community: "community"
|
|
39
389
|
},
|
|
40
390
|
dependencies: {
|
|
41
391
|
"@meith/web": version,
|
|
42
392
|
"@meith/cli": version,
|
|
43
|
-
"@meith/theme-default": version
|
|
393
|
+
"@meith/theme-default": version,
|
|
394
|
+
next: NEXT_VERSION
|
|
44
395
|
},
|
|
45
396
|
engines: { node: ">=22" }
|
|
46
397
|
},
|
|
@@ -54,7 +405,7 @@ function scaffold(options) {
|
|
|
54
405
|
`# Every @meith/* dependency here is an exact version, not a range \u2014 see
|
|
55
406
|
# README.md, "Upgrading", for why a range breaks the build. This makes that
|
|
56
407
|
# the default for any \`npm install\` run in this project from here on,
|
|
57
|
-
# including a plugin installed by hand later, not only the
|
|
408
|
+
# including a plugin installed by hand later, not only the four packages
|
|
58
409
|
# the scaffold pinned itself.
|
|
59
410
|
save-exact=true
|
|
60
411
|
`
|
|
@@ -134,67 +485,7 @@ export function installedPluginDefinitions() {
|
|
|
134
485
|
}
|
|
135
486
|
`
|
|
136
487
|
);
|
|
137
|
-
files.set(
|
|
138
|
-
".env.example",
|
|
139
|
-
`# ${name} \u2014 environment.
|
|
140
|
-
#
|
|
141
|
-
# Copy to .env.local for development. On the server this is \`.env\` beside the
|
|
142
|
-
# compose file; nothing here belongs in git.
|
|
143
|
-
|
|
144
|
-
# \u2500\u2500\u2500 Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
145
|
-
|
|
146
|
-
# Your Postgres connection string.
|
|
147
|
-
#
|
|
148
|
-
# If it is a managed database that offers a TRANSACTION-MODE POOLER string, use
|
|
149
|
-
# that rather than the direct one \u2014 Neon, Supabase and their kind hand out both,
|
|
150
|
-
# and on the direct string a board works in testing and starts refusing
|
|
151
|
-
# connections under the first real traffic, with an error that names the
|
|
152
|
-
# database rather than the cause. Your own Postgres, with a fixed number of
|
|
153
|
-
# processes in front of it, does not need one.
|
|
154
|
-
DATABASE_URL=
|
|
155
|
-
|
|
156
|
-
# Session and token signing. No default, deliberately: a shipped default is a
|
|
157
|
-
# board every reader of the source can sign a session for.
|
|
158
|
-
#
|
|
159
|
-
# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
|
160
|
-
AUTH_SECRET=
|
|
161
|
-
|
|
162
|
-
# The shared secret the tick caller presents to GET /api/system/tick. Generate
|
|
163
|
-
# it the same way. Without it the tick is unauthenticated, and the tick is how
|
|
164
|
-
# bans expire and digests send.
|
|
165
|
-
TICK_SECRET=
|
|
166
|
-
|
|
167
|
-
# \u2500\u2500\u2500 Optional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
168
|
-
|
|
169
|
-
# fixture = deterministic in-memory sample data, no database needed. This is
|
|
170
|
-
# what \`npm run build\` uses, and what a checkout with no database falls back to.
|
|
171
|
-
DATA_SOURCE=postgres
|
|
172
|
-
|
|
173
|
-
# Absolute, no trailing slash. Used in mail, feeds and canonical URLs \u2014 every
|
|
174
|
-
# place a relative URL cannot work because there is no request to be relative to.
|
|
175
|
-
#
|
|
176
|
-
# Optional: leave it blank and the installer asks, prefilled from the address you
|
|
177
|
-
# load /install at, and stores the answer on the board where the settings screen
|
|
178
|
-
# can change it without a redeploy. Set it here and it wins outright.
|
|
179
|
-
APP_URL=
|
|
180
|
-
|
|
181
|
-
# Mail. Leave these alone and the installer asks for mail on first run, storing
|
|
182
|
-
# it on the board \u2014 a settings screen with a test button, no redeploy. Set
|
|
183
|
-
# MAIL_DRIVER here instead and the environment wins outright, which is what you
|
|
184
|
-
# want if the credential must not live in the database.
|
|
185
|
-
#
|
|
186
|
-
# The default sends NOTHING: each message goes to the server log, so password
|
|
187
|
-
# reset fails silently until mail is configured one way or the other.
|
|
188
|
-
# MAIL_DRIVER=smtp
|
|
189
|
-
# MAIL_SMTP_HOST=smtp.example.com
|
|
190
|
-
# MAIL_SMTP_PORT=465
|
|
191
|
-
# MAIL_SMTP_SECURITY=tls # tls (465) | starttls (587) | none
|
|
192
|
-
# MAIL_SMTP_USERNAME=
|
|
193
|
-
# MAIL_SMTP_PASSWORD=
|
|
194
|
-
# MAIL_FROM=noreply@yourdomain.com
|
|
195
|
-
|
|
196
|
-
`
|
|
197
|
-
);
|
|
488
|
+
files.set(".env.example", envExample(name, target));
|
|
198
489
|
files.set(
|
|
199
490
|
".gitignore",
|
|
200
491
|
`node_modules
|
|
@@ -629,11 +920,20 @@ echo "<password>" | npm run community -- user:create --username <name> --email <
|
|
|
629
920
|
|
|
630
921
|
\`\`\`sh
|
|
631
922
|
npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
|
|
632
|
-
|
|
923
|
+
npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
|
|
924
|
+
git commit -am "Upgrade Meith and the Next.js version it builds with"
|
|
633
925
|
git push
|
|
634
926
|
\`\`\`
|
|
635
927
|
|
|
636
|
-
|
|
928
|
+
The second command is not optional. This board pins \`next\` itself, and
|
|
929
|
+
nothing bumps it for you: upgrading only the \`@meith/*\` packages leaves the
|
|
930
|
+
board's own pin on the old Next while \`@meith/web\` depends on the new one,
|
|
931
|
+
which npm resolves by installing both \u2014 the build then runs on one version
|
|
932
|
+
while everything reading \`package.json\` sees the other. Reading the version
|
|
933
|
+
out of the freshly installed \`@meith/web\` is what keeps the two the same
|
|
934
|
+
without anybody having to know the number.
|
|
935
|
+
|
|
936
|
+
That \`package.json\` change is the whole pin: \`Dockerfile\`'s own
|
|
637
937
|
\`FROM\` line takes the version as a build argument, and
|
|
638
938
|
\`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
|
|
639
939
|
own \`@meith/web\` dependency when it rebuilds \u2014 nothing in \`Dockerfile\`
|
|
@@ -654,8 +954,279 @@ there is no down migration to undo a destructive one, and a button that pretende
|
|
|
654
954
|
otherwise would be worse than its absence.
|
|
655
955
|
`
|
|
656
956
|
);
|
|
957
|
+
if (target === "vercel") {
|
|
958
|
+
return vercelTree(files, {
|
|
959
|
+
name,
|
|
960
|
+
repositoryUrl,
|
|
961
|
+
templateRepositoryUrl: options.templateRepositoryUrl ?? DEFAULT_TEMPLATE_REPOSITORY_URL
|
|
962
|
+
});
|
|
963
|
+
}
|
|
657
964
|
return files;
|
|
658
965
|
}
|
|
966
|
+
function vercelTree(base, options) {
|
|
967
|
+
const files = new Map(base);
|
|
968
|
+
for (const path of SELF_HOST_DEPLOY_KIT) files.delete(path);
|
|
969
|
+
files.set(
|
|
970
|
+
".gitignore",
|
|
971
|
+
`node_modules
|
|
972
|
+
.next
|
|
973
|
+
.meith
|
|
974
|
+
.vercel
|
|
975
|
+
.env
|
|
976
|
+
.env.local
|
|
977
|
+
.env*.local
|
|
978
|
+
*.log
|
|
979
|
+
.DS_Store
|
|
980
|
+
|
|
981
|
+
${AT_ROOT_IGNORES}
|
|
982
|
+
`
|
|
983
|
+
);
|
|
984
|
+
files.set("vercel.json", vercelJson());
|
|
985
|
+
files.set("README.md", vercelReadme(options));
|
|
986
|
+
return files;
|
|
987
|
+
}
|
|
988
|
+
function vercelReadme({ name, repositoryUrl, templateRepositoryUrl }) {
|
|
989
|
+
return `# ${name}
|
|
990
|
+
|
|
991
|
+
A forum, built on [Meith](${repositoryUrl}), running as Vercel functions.
|
|
992
|
+
|
|
993
|
+
[](${deployButtonUrl(templateRepositoryUrl)})
|
|
994
|
+
|
|
995
|
+
## What the button provisions
|
|
996
|
+
|
|
997
|
+
- **A copy of this repository** under your own GitHub account. Vercel builds
|
|
998
|
+
from it, and every later push to \`main\` redeploys.
|
|
999
|
+
- **A Neon Postgres database**, attached to the project. Neon publishes the
|
|
1000
|
+
pooled connection string as \`DATABASE_URL\` and the direct one as
|
|
1001
|
+
\`DATABASE_URL_UNPOOLED\`.
|
|
1002
|
+
- **An Upstash Redis store**, attached the same way, for the shared cache. It
|
|
1003
|
+
publishes \`KV_URL\`, which the board reads as \`REDIS_URL\` \u2014 \`KV_REST_API_URL\`
|
|
1004
|
+
beside it is an HTTPS endpoint and is not used for this.
|
|
1005
|
+
- **A Vercel Blob store** for uploads, which publishes \`BLOB_STORE_ID\` into the
|
|
1006
|
+
project by itself. That is the whole credential: the board hands the id to
|
|
1007
|
+
Vercel's SDK, which authenticates with the deployment's own OIDC identity, so
|
|
1008
|
+
there is no token to copy. This is what used to be four hand-typed \`S3_*\`
|
|
1009
|
+
secrets.
|
|
1010
|
+
- **A Vercel project** carrying \`vercel.json\` \u2014 the build command
|
|
1011
|
+
\`${VERCEL_BUILD_COMMAND}\`,
|
|
1012
|
+
which applies the schema before it builds, materializes the board's app at
|
|
1013
|
+
the project root so the artefact lands where Vercel reads it, and the cron
|
|
1014
|
+
entry that drives the tick.
|
|
1015
|
+
|
|
1016
|
+
**Mail is the one thing the button does not set up**, and it takes one click
|
|
1017
|
+
after the deploy \u2014 see *Mail, in one click* below. That is also where the
|
|
1018
|
+
address the board sends from goes: it has to be at a domain your provider has
|
|
1019
|
+
verified, which cannot be true of any domain before a provider exists. The
|
|
1020
|
+
board boots and runs without mail, and delivers nothing, silently, until it is
|
|
1021
|
+
done.
|
|
1022
|
+
|
|
1023
|
+
## What to type into the deploy form
|
|
1024
|
+
|
|
1025
|
+
**Two secrets**, generated rather than chosen. Thirty-two characters is a floor
|
|
1026
|
+
the board enforces at boot, not a suggestion:
|
|
1027
|
+
|
|
1028
|
+
\`\`\`sh
|
|
1029
|
+
openssl rand -hex 32 # AUTH_SECRET
|
|
1030
|
+
openssl rand -hex 32 # CRON_SECRET
|
|
1031
|
+
\`\`\`
|
|
1032
|
+
|
|
1033
|
+
\`CRON_SECRET\` is the name Vercel Cron sends, as \`Authorization: Bearer\`, and it
|
|
1034
|
+
cannot be told to send another \u2014 the caller is the platform, so this one has to
|
|
1035
|
+
be an environment variable both ends can read, and cannot be something the
|
|
1036
|
+
board makes up for itself. Note that this floor is stricter than the 16
|
|
1037
|
+
characters Vercel's own cron documentation suggests \u2014 a value generated by
|
|
1038
|
+
following those instructions is refused here, and the fix is a longer secret.
|
|
1039
|
+
|
|
1040
|
+
\`AUTH_SECRET\` seals members' two-factor secrets and signs the unsubscribe links
|
|
1041
|
+
in outgoing mail. It stays in the environment deliberately: a copy of the
|
|
1042
|
+
database is then not enough to forge either.
|
|
1043
|
+
|
|
1044
|
+
**That is the whole form.** Everything else the board works out from the stores
|
|
1045
|
+
this button just linked to the project:
|
|
1046
|
+
|
|
1047
|
+
\`\`\`ini
|
|
1048
|
+
${VERCEL_DERIVED_DRIVERS.join("\n")}
|
|
1049
|
+
\`\`\`
|
|
1050
|
+
|
|
1051
|
+
\`DIRECT_DATABASE_URL\` comes from Neon's own \`DATABASE_URL_UNPOOLED\`, or
|
|
1052
|
+
\`POSTGRES_URL_NON_POOLING\` if that one is absent \u2014 migrations and the first-run
|
|
1053
|
+
installer each hold a session-level advisory lock, which the pooled
|
|
1054
|
+
\`DATABASE_URL\` cannot hold. \`REDIS_URL\` comes from Upstash's \`KV_URL\`, the one
|
|
1055
|
+
variable it publishes that speaks the Redis protocol.
|
|
1056
|
+
|
|
1057
|
+
Every one of those derivations is scoped to this platform, fires only where you
|
|
1058
|
+
have not set the variable yourself, and **refuses to boot rather than guess**.
|
|
1059
|
+
If a store is missing, or publishes a name this board does not know, the deploy
|
|
1060
|
+
stops with a message naming every variable it looked at \u2014 it will not fall back
|
|
1061
|
+
to caching inside each instance, or to uploads on a disk that is discarded with
|
|
1062
|
+
the instance. When the name is one we do not know, set \`REDIS_URL\` or
|
|
1063
|
+
\`DIRECT_DATABASE_URL\` in the project's environment settings and the derivation
|
|
1064
|
+
stands aside.
|
|
1065
|
+
|
|
1066
|
+
If you would rather keep uploads somewhere you hold yourself \u2014 see *Leaving
|
|
1067
|
+
Vercel* below for why that matters \u2014 set \`FILESTORE_DRIVER=s3\` and add
|
|
1068
|
+
\`S3_BUCKET\`, \`S3_REGION\`, \`S3_ACCESS_KEY_ID\` and \`S3_SECRET_ACCESS_KEY\` in the
|
|
1069
|
+
project's environment settings, with \`S3_ENDPOINT\` for a bucket that is not AWS
|
|
1070
|
+
(\`S3_REGION=auto\` for R2). The same board runs either way.
|
|
1071
|
+
|
|
1072
|
+
## Mail, in one click
|
|
1073
|
+
|
|
1074
|
+
A board that cannot send mail cannot reset a password, so do this before you
|
|
1075
|
+
invite anybody.
|
|
1076
|
+
|
|
1077
|
+
1. Open your project on Vercel, go to **Storage \u2192 Marketplace** (or
|
|
1078
|
+
**Integrations**), and add **Resend**. It creates a Resend account linked to
|
|
1079
|
+
the project and connects your sending domain.
|
|
1080
|
+
2. Verify that domain in the Resend dashboard if you have not already. Resend
|
|
1081
|
+
refuses to send from an address at a domain it has not verified.
|
|
1082
|
+
3. Add \`MAIL_FROM\` to the project's environment settings \u2014 an address at that
|
|
1083
|
+
verified domain, and the only mail value you ever type. The deploy form does
|
|
1084
|
+
not ask for it, because a sender address is not something you can know
|
|
1085
|
+
before there is a provider to verify it.
|
|
1086
|
+
4. Redeploy, or let the next push redeploy.
|
|
1087
|
+
|
|
1088
|
+
That is all. The integration publishes its key into the project as
|
|
1089
|
+
\`RESEND_API_KEY\`, and the board reads that name: with it set, and \`MAIL_FROM\`
|
|
1090
|
+
beside it, mail sends over Resend's HTTPS API with nothing further to
|
|
1091
|
+
configure.
|
|
1092
|
+
|
|
1093
|
+
The board is not tied to Resend. Its mail driver is a plain JSON-over-HTTPS
|
|
1094
|
+
sender that posts \`{from, to, subject, text, html, reply_to}\` with a bearer
|
|
1095
|
+
token \u2014 Resend's \`POST /emails\` happens to be exactly that shape, which is why
|
|
1096
|
+
it needs no adapter. Any provider with the same shape works: set
|
|
1097
|
+
\`MAIL_HTTP_ENDPOINT\`, \`MAIL_HTTP_TOKEN\` and \`MAIL_DRIVER=http\` in the
|
|
1098
|
+
project's environment settings, and set the first two **together** \u2014 either
|
|
1099
|
+
one on its own stands the Resend bridge down, so a key issued for Resend is
|
|
1100
|
+
never presented to an endpoint you chose. Only \`RESEND_API_KEY\` turns the
|
|
1101
|
+
driver on by itself. Delete it once you have moved off Resend.
|
|
1102
|
+
|
|
1103
|
+
Check it worked: sign in as the administrator and use the test button on
|
|
1104
|
+
**/admin \u2192 Settings \u2192 Mail**.
|
|
1105
|
+
|
|
1106
|
+
## First run: \`/install\`
|
|
1107
|
+
|
|
1108
|
+
The build applies migrations, but an empty schema is not yet a board. Open
|
|
1109
|
+
\`https://<your-deployment>/install\` once the first deploy is green. It asks for
|
|
1110
|
+
the board's name and address and for the first administrator's username, email
|
|
1111
|
+
and password, creates the board and that account, and then **seals itself**:
|
|
1112
|
+
\`/install\` answers 404 from then on. Run it against the database you intend to
|
|
1113
|
+
keep \u2014 the screens are the ones
|
|
1114
|
+
[docs/quickstart.md](${repositoryUrl}/blob/main/docs/quickstart.md#4-run-the-installer)
|
|
1115
|
+
walks through.
|
|
1116
|
+
|
|
1117
|
+
## The tick
|
|
1118
|
+
|
|
1119
|
+
\`vercel.json\` asks Vercel to call \`${TICK_PATH}\` on \`${TICK_SCHEDULE}\`. That
|
|
1120
|
+
route is how bans expire, digests send, mail leaves the outbox and the queue
|
|
1121
|
+
drains; nothing here runs it on its own, because there is no worker process on
|
|
1122
|
+
a function platform. Two things about it are worth knowing **before** you
|
|
1123
|
+
deploy rather than after:
|
|
1124
|
+
|
|
1125
|
+
- **A per-minute schedule needs a paid plan.** Hobby allows a couple of cron
|
|
1126
|
+
jobs and runs each of them roughly once a day, at an hour Vercel chooses;
|
|
1127
|
+
only paid plans accept an arbitrary cron expression. A board ticking daily
|
|
1128
|
+
still loses nothing \u2014 tasks are written so a missed run delays work rather
|
|
1129
|
+
than dropping it \u2014 but "as it happens" notifications become a daily digest in
|
|
1130
|
+
all but name. To keep a minute-by-minute tick on Hobby, drive
|
|
1131
|
+
\`${TICK_PATH}\` from something else that can call a URL on a schedule \u2014 a
|
|
1132
|
+
GitHub Actions workflow, a systemd timer, an uptime pinger \u2014 presenting
|
|
1133
|
+
\`TICK_SECRET\` instead.
|
|
1134
|
+
- **\`maxDuration = 300\` is validated when the project builds, not when the
|
|
1135
|
+
function runs.** A plan that does not allow 300 seconds therefore **fails the
|
|
1136
|
+
deployment** rather than clamping the request. With Fluid Compute \u2014 the
|
|
1137
|
+
default for new projects \u2014 Hobby allows 300 and this builds as written. With
|
|
1138
|
+
Fluid Compute switched off, Hobby caps a function at 60 seconds and the build
|
|
1139
|
+
fails. Turn Fluid Compute back on.
|
|
1140
|
+
|
|
1141
|
+
A tick that reaches the tasks and runs them answers \`200\` even when one of them
|
|
1142
|
+
threw, with \`ok: false\` and the failure named in \`ran\`. That is deliberate:
|
|
1143
|
+
schedulers retry non-2xx answers, and a task that fails every time would turn
|
|
1144
|
+
each retry into another attempt against whatever it is failing against.
|
|
1145
|
+
|
|
1146
|
+
## Upgrading
|
|
1147
|
+
|
|
1148
|
+
\`\`\`sh
|
|
1149
|
+
npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
|
|
1150
|
+
npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
|
|
1151
|
+
git commit -am "Upgrade Meith and the Next.js version it builds with"
|
|
1152
|
+
git push
|
|
1153
|
+
\`\`\`
|
|
1154
|
+
|
|
1155
|
+
Vercel rebuilds on the push, and the build command applies the new migrations
|
|
1156
|
+
before it builds. \`--save-exact\` matters and \`.npmrc\` already sets it for
|
|
1157
|
+
everything else installed here.
|
|
1158
|
+
|
|
1159
|
+
The second command is not optional. This board pins \`next\` itself \u2014 Vercel
|
|
1160
|
+
reads that pin to pick its Next.js builder \u2014 and nothing bumps it for you.
|
|
1161
|
+
Upgrading only the \`@meith/*\` packages leaves two versions of Next
|
|
1162
|
+
installed, the board built with one and the platform configured for the
|
|
1163
|
+
other. Reading the version out of the freshly installed \`@meith/web\` keeps
|
|
1164
|
+
them the same without anybody having to know the number.
|
|
1165
|
+
|
|
1166
|
+
Migrations are forward-only. Recovery is by restore, so take a backup first \u2014
|
|
1167
|
+
there is no down migration to undo a destructive one.
|
|
1168
|
+
|
|
1169
|
+
## Leaving Vercel
|
|
1170
|
+
|
|
1171
|
+
A board must stay movable, and the Blob store is the one part of this shape that
|
|
1172
|
+
is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
|
|
1173
|
+
that any host accepts, but a Vercel Blob store is reachable only through Vercel's
|
|
1174
|
+
own API and there is no bucket to sync out of it. **The uploads are the thing you
|
|
1175
|
+
have to carry out deliberately, and \`community backup\` is how.**
|
|
1176
|
+
|
|
1177
|
+
Under \`FILESTORE_DRIVER=blob\`, \`community backup\` includes the uploads **by
|
|
1178
|
+
default** \u2014 it walks the Blob store, pulls every object, and puts them in the
|
|
1179
|
+
bundle beside the database dump. This is the opposite of the \`s3\` default, which
|
|
1180
|
+
skips them, because a bucket has its own backup story you can drive yourself and
|
|
1181
|
+
a Blob store does not:
|
|
1182
|
+
|
|
1183
|
+
\`\`\`sh
|
|
1184
|
+
DATABASE_URL=\u2026 # Neon's pooled string
|
|
1185
|
+
DIRECT_DATABASE_URL=\u2026 # Neon's DATABASE_URL_UNPOOLED
|
|
1186
|
+
FILESTORE_DRIVER=blob
|
|
1187
|
+
BLOB_READ_WRITE_TOKEN=\u2026 # create one on the store; see below
|
|
1188
|
+
npm run community -- backup
|
|
1189
|
+
\`\`\`
|
|
1190
|
+
|
|
1191
|
+
Run that from a checkout of this repository, with those four values in the
|
|
1192
|
+
environment \u2014 the CLI talks to Neon and to the Blob store over the network, so
|
|
1193
|
+
it does not have to run on Vercel.
|
|
1194
|
+
|
|
1195
|
+
That last one is the one value this route asks you to make by hand, and only
|
|
1196
|
+
here. On the deployment the board reaches the store with \`BLOB_STORE_ID\` and
|
|
1197
|
+
the deployment's OIDC identity, which a command on your own machine does not
|
|
1198
|
+
have. Open the store under **Storage**, create a read-write token, and use it
|
|
1199
|
+
for the backup; the board itself never needs it. The bundle it writes holds the dump *and*
|
|
1200
|
+
every object. Check the last line it prints: if it says *no uploads*, the
|
|
1201
|
+
uploads are not in the bundle and restoring it gives a board whose posts have
|
|
1202
|
+
broken images.
|
|
1203
|
+
|
|
1204
|
+
Restoring puts them wherever the *restoring* board's \`FILESTORE_DRIVER\` points,
|
|
1205
|
+
so the same bundle moves the board either onward or away:
|
|
1206
|
+
|
|
1207
|
+
\`\`\`sh
|
|
1208
|
+
# onto a self-hosted board with a bucket
|
|
1209
|
+
FILESTORE_DRIVER=s3 S3_BUCKET=\u2026 RESTORE_DATABASE_URL=\u2026 npm run community -- restore bundle.tar.gz
|
|
1210
|
+
|
|
1211
|
+
# onto a board that keeps uploads on its own disk
|
|
1212
|
+
RESTORE_DATABASE_URL=\u2026 npm run community -- restore bundle.tar.gz --uploads-dir ./uploads
|
|
1213
|
+
\`\`\`
|
|
1214
|
+
|
|
1215
|
+
Take one before you need it. A Blob store deleted with the Vercel project takes
|
|
1216
|
+
the attachments with it, and there is no second copy anywhere unless you made
|
|
1217
|
+
one.
|
|
1218
|
+
|
|
1219
|
+
## Somewhere other than Vercel
|
|
1220
|
+
|
|
1221
|
+
Everything above is one deployment shape.
|
|
1222
|
+
[docs/self-hosting.md](${repositoryUrl}/blob/main/docs/self-hosting.md) is the
|
|
1223
|
+
same board as containers you run yourself, and \`npx create-meith <name>\`
|
|
1224
|
+
scaffolds that shape instead \u2014 a Dockerfile, a compose file and a workflow that
|
|
1225
|
+
builds the image. [docs/scaling.md](${repositoryUrl}/blob/main/docs/scaling.md)
|
|
1226
|
+
explains why the drivers above are what they are, and why an S3-compatible
|
|
1227
|
+
bucket is the portable choice for uploads everywhere but here.
|
|
1228
|
+
`;
|
|
1229
|
+
}
|
|
659
1230
|
function nextSteps(name) {
|
|
660
1231
|
return [`cd ${name}`, "npm install", "cp .env.example .env.local", "npm run dev"];
|
|
661
1232
|
}
|
|
@@ -758,7 +1329,7 @@ async function run(argv, version) {
|
|
|
758
1329
|
}
|
|
759
1330
|
|
|
760
1331
|
// src/bin.ts
|
|
761
|
-
var result = await run(process.argv.slice(2), "0.
|
|
1332
|
+
var result = await run(process.argv.slice(2), "0.19.0");
|
|
762
1333
|
for (const line of result.lines) {
|
|
763
1334
|
if (result.code === 0) console.log(line);
|
|
764
1335
|
else console.error(line);
|