@jant/core 0.3.45 → 0.3.47
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/bin/commands/db/execute-file.js +12 -4
- package/bin/commands/db/rehearse.js +2 -2
- package/bin/commands/export.js +12 -4
- package/bin/commands/import-site.js +99 -305
- package/bin/commands/migrate.js +36 -69
- package/bin/commands/reset-password.js +10 -4
- package/bin/commands/site/export.js +59 -248
- package/bin/commands/site/snapshot/export.js +58 -45
- package/bin/commands/site/snapshot/import.js +104 -52
- package/bin/lib/node-env.js +100 -0
- package/bin/lib/runtime-target.js +64 -0
- package/bin/lib/site-snapshot.js +185 -54
- package/bin/lib/sql-export.js +19 -2
- package/dist/{app-C-L7wL6o.js → app-3REcR-3U.js} +332 -190
- package/dist/app-B67XOEyo.js +6 -0
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/{client-auth-Dcon89Av.js → client-auth-Ce5WEAVS.js} +236 -183
- package/dist/client/_assets/client-s71Js1Cu.css +2 -0
- package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
- package/dist/github-sync-C593r22F.js +4 -0
- package/dist/github-sync-bL1hnx3Q.js +428 -0
- package/dist/index.js +3 -2
- package/dist/node.js +5 -4
- package/package.json +3 -2
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +18 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
- package/src/client/components/jant-compose-dialog.ts +7 -6
- package/src/client/components/jant-compose-editor.ts +6 -5
- package/src/client/components/jant-settings-general.ts +164 -22
- package/src/client/components/settings-types.ts +4 -6
- package/src/client/random-uuid.ts +23 -0
- package/src/client-auth.ts +1 -1
- package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
- package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
- package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
- package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
- package/src/db/migrations/meta/0021_snapshot.json +2121 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
- package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +21 -26
- package/src/db/rehearsal-fixtures/demo-current.json +1 -1
- package/src/db/schema.ts +16 -20
- package/src/i18n/__tests__/middleware.test.ts +43 -1
- package/src/i18n/coverage.generated.ts +17 -0
- package/src/i18n/i18n.ts +18 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/locales/settings/en.po +16 -11
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +17 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +16 -11
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +84 -2
- package/src/i18n/middleware.ts +25 -16
- package/src/i18n/supported-locales.ts +153 -0
- package/src/lib/__tests__/csp-builder.test.ts +19 -2
- package/src/lib/__tests__/feed.test.ts +242 -1
- package/src/lib/__tests__/post-meta.test.ts +0 -1
- package/src/lib/__tests__/view.test.ts +0 -1
- package/src/lib/csp-builder.ts +28 -10
- package/src/lib/feed.ts +153 -3
- package/src/middleware/__tests__/secure-headers.test.ts +89 -0
- package/src/middleware/auth.ts +1 -1
- package/src/middleware/secure-headers.ts +47 -1
- package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
- package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
- package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
- package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
- package/src/node/__tests__/cli-sql-export.test.ts +49 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +8 -2
- package/src/routes/api/__tests__/settings.test.ts +3 -2
- package/src/routes/api/github-sync.tsx +1 -1
- package/src/routes/api/settings.ts +4 -1
- package/src/routes/auth/signin.tsx +6 -0
- package/src/routes/pages/archive.tsx +4 -2
- package/src/services/__tests__/post.test.ts +19 -19
- package/src/services/__tests__/search.test.ts +0 -1
- package/src/services/__tests__/settings.test.ts +22 -3
- package/src/services/bootstrap.ts +7 -3
- package/src/services/collection.ts +3 -3
- package/src/services/export.ts +0 -3
- package/src/services/navigation.ts +0 -2
- package/src/services/path.ts +1 -38
- package/src/services/post.ts +32 -66
- package/src/services/search.ts +0 -6
- package/src/services/settings.ts +47 -6
- package/src/services/site-admin.ts +6 -1
- package/src/styles/ui.css +12 -23
- package/src/types/entities.ts +0 -1
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/dash/settings/GeneralContent.tsx +17 -19
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
- package/src/ui/feed/NoteCard.tsx +1 -11
- package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
- package/src/ui/pages/HomePage.tsx +1 -4
- package/src/ui/pages/PostPage.tsx +2 -0
- package/bin/commands/collections.js +0 -268
- package/bin/commands/media.js +0 -302
- package/bin/commands/posts.js +0 -262
- package/bin/commands/search.js +0 -53
- package/bin/commands/settings.js +0 -93
- package/bin/lib/http-api.js +0 -223
- package/bin/lib/media-upload.js +0 -206
- package/dist/app-Hvqe7Ks_.js +0 -5
- package/dist/client/_assets/client-DDs6NzB3.css +0 -2
- package/src/__tests__/bin/content-cli.test.ts +0 -179
- package/src/__tests__/bin/media-cli.test.ts +0 -192
- /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
- /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
readFile,
|
|
6
6
|
readdir,
|
|
7
7
|
rm,
|
|
8
|
-
stat,
|
|
9
8
|
writeFile,
|
|
10
9
|
} from "node:fs/promises";
|
|
11
10
|
import { tmpdir } from "node:os";
|
|
@@ -24,15 +23,14 @@ import {
|
|
|
24
23
|
buildSnapshotStorageQuery,
|
|
25
24
|
collectSnapshotObjects,
|
|
26
25
|
getSnapshotSelectSql,
|
|
27
|
-
sha256File,
|
|
28
26
|
SNAPSHOT_TABLES,
|
|
29
27
|
snapshotObjectPath,
|
|
30
28
|
} from "../../../lib/site-snapshot.js";
|
|
31
29
|
import { resolveCliSite } from "../../../lib/site-selection.js";
|
|
32
30
|
import { dumpDatabaseToSql } from "../../../lib/sql-export.js";
|
|
33
31
|
import {
|
|
32
|
+
bootstrapCliRuntime,
|
|
34
33
|
getCliRuntimeLabel,
|
|
35
|
-
resolveCliRuntime,
|
|
36
34
|
} from "../../../lib/runtime-target.js";
|
|
37
35
|
import { resolveWranglerVarString } from "../../../lib/wrangler-config.js";
|
|
38
36
|
|
|
@@ -99,8 +97,12 @@ async function assertWritableOutput(outputPath, force) {
|
|
|
99
97
|
|
|
100
98
|
async function createNodeExportContext() {
|
|
101
99
|
const nodeDatabase = await openNodeDatabase(process.env);
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
// Only need the storage driver — `createNodeCliRuntime` would also resolve
|
|
101
|
+
// the current site, which (a) is redundant with the bin-level resolveCliSite
|
|
102
|
+
// call below and (b) prints a generic "/setup first" error when the
|
|
103
|
+
// snapshot's own error path is more informative.
|
|
104
|
+
const { createStorageDriver } = await loadNodeRuntime();
|
|
105
|
+
const storage = createStorageDriver(nodeDatabase.bindings);
|
|
104
106
|
|
|
105
107
|
return {
|
|
106
108
|
dialect: nodeDatabase.database.dialect,
|
|
@@ -111,11 +113,11 @@ async function createNodeExportContext() {
|
|
|
111
113
|
return nodeDatabase.query(sql);
|
|
112
114
|
},
|
|
113
115
|
async downloadObject(key, filePath) {
|
|
114
|
-
if (!
|
|
116
|
+
if (!storage) {
|
|
115
117
|
throw new Error("Snapshot export requires configured storage.");
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
const object = await
|
|
120
|
+
const object = await storage.get(key);
|
|
119
121
|
if (!object?.body) {
|
|
120
122
|
throw new Error(`Storage object not found: ${key}`);
|
|
121
123
|
}
|
|
@@ -178,6 +180,7 @@ export async function run(argv) {
|
|
|
178
180
|
host: { type: "string" },
|
|
179
181
|
help: { type: "boolean", short: "h" },
|
|
180
182
|
local: { type: "boolean", default: false },
|
|
183
|
+
node: { type: "boolean", default: false },
|
|
181
184
|
output: {
|
|
182
185
|
type: "string",
|
|
183
186
|
short: "o",
|
|
@@ -187,13 +190,14 @@ export async function run(argv) {
|
|
|
187
190
|
"persist-to": { type: "string" },
|
|
188
191
|
remote: { type: "boolean", default: false },
|
|
189
192
|
site: { type: "string" },
|
|
193
|
+
"skip-objects": { type: "boolean", default: false },
|
|
190
194
|
url: { type: "string" },
|
|
191
195
|
},
|
|
192
196
|
});
|
|
193
197
|
|
|
194
198
|
if (values.help) {
|
|
195
199
|
console.log(
|
|
196
|
-
"Usage: jant site snapshot export [--local | --remote] [--output <dir|zip>]",
|
|
200
|
+
"Usage: jant site snapshot export [--local | --remote | --node] [--output <dir|zip>]",
|
|
197
201
|
);
|
|
198
202
|
console.log("");
|
|
199
203
|
console.log(
|
|
@@ -205,6 +209,9 @@ export async function run(argv) {
|
|
|
205
209
|
" --local Force local D1 instead of DATABASE_URL",
|
|
206
210
|
);
|
|
207
211
|
console.log(" --remote Export from remote D1");
|
|
212
|
+
console.log(
|
|
213
|
+
" --node Force Node runtime even if DATABASE_URL is unset",
|
|
214
|
+
);
|
|
208
215
|
console.log(
|
|
209
216
|
" --output, -o Output directory or .zip file (default: jant-site-snapshot)",
|
|
210
217
|
);
|
|
@@ -227,14 +234,33 @@ export async function run(argv) {
|
|
|
227
234
|
console.log(
|
|
228
235
|
" --persist-to Local D1/R2 state directory override",
|
|
229
236
|
);
|
|
237
|
+
console.log(
|
|
238
|
+
" --skip-objects Skip downloading storage objects. The archive only contains meta.json and db.sql.",
|
|
239
|
+
);
|
|
240
|
+
console.log(
|
|
241
|
+
" Only safe when the import target's storage already has the same keys",
|
|
242
|
+
);
|
|
243
|
+
console.log(
|
|
244
|
+
" (e.g. moving between Workers that share an R2 bucket). Otherwise the",
|
|
245
|
+
);
|
|
246
|
+
console.log(
|
|
247
|
+
" imported site will be missing media — pair with `--allow-missing-objects`",
|
|
248
|
+
);
|
|
249
|
+
console.log(" on import.");
|
|
230
250
|
console.log("");
|
|
231
251
|
console.log(
|
|
232
|
-
"
|
|
252
|
+
"`.env.node` next to your project (or in packages/core/) is auto-loaded.",
|
|
253
|
+
);
|
|
254
|
+
console.log(
|
|
255
|
+
"If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
|
|
256
|
+
);
|
|
257
|
+
console.log(
|
|
258
|
+
"this command uses the Node database runtime and configured storage driver.",
|
|
233
259
|
);
|
|
234
260
|
process.exit(0);
|
|
235
261
|
}
|
|
236
262
|
|
|
237
|
-
const runtime =
|
|
263
|
+
const { runtime } = bootstrapCliRuntime(values);
|
|
238
264
|
const outputPath = resolve(process.cwd(), values.output);
|
|
239
265
|
const shouldZip = isZipPath(outputPath);
|
|
240
266
|
const scratchDir = shouldZip
|
|
@@ -259,6 +285,7 @@ export async function run(argv) {
|
|
|
259
285
|
url: values.url,
|
|
260
286
|
});
|
|
261
287
|
|
|
288
|
+
console.log(`Dumping database (${SNAPSHOT_TABLES.length} tables)...`);
|
|
262
289
|
const dbSql = await dumpDatabaseToSql(
|
|
263
290
|
{
|
|
264
291
|
query(sql) {
|
|
@@ -275,55 +302,41 @@ export async function run(argv) {
|
|
|
275
302
|
getSnapshotSelectSql(tableName, site.id),
|
|
276
303
|
]),
|
|
277
304
|
),
|
|
305
|
+
onProgress: ({ index, total, table }) => {
|
|
306
|
+
console.log(` [${index}/${total}] ${table}`);
|
|
307
|
+
},
|
|
278
308
|
},
|
|
279
309
|
);
|
|
280
310
|
|
|
311
|
+
console.log("Listing storage objects...");
|
|
281
312
|
const objectRows = await context.query(buildSnapshotStorageQuery(site.id));
|
|
282
313
|
const objects = collectSnapshotObjects(objectRows);
|
|
283
|
-
const manifestObjects = [];
|
|
284
314
|
|
|
285
315
|
await writeFile(join(scratchDir, "db.sql"), dbSql);
|
|
286
316
|
|
|
287
|
-
if (objects
|
|
288
|
-
|
|
289
|
-
|
|
317
|
+
if (values["skip-objects"]) {
|
|
318
|
+
if (objects.length > 0) {
|
|
319
|
+
console.log(
|
|
320
|
+
`--skip-objects: leaving ${objects.length} referenced object(s) out of the archive.`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
if (objects.length > 0) {
|
|
325
|
+
console.log(`Downloading ${objects.length} referenced object(s)...`);
|
|
326
|
+
}
|
|
290
327
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
manifestObjects.push({
|
|
298
|
-
key: object.key,
|
|
299
|
-
file: relativeObjectPath,
|
|
300
|
-
contentType: object.contentType || undefined,
|
|
301
|
-
size: fileStat.size,
|
|
302
|
-
sha256: await sha256File(absoluteObjectPath),
|
|
303
|
-
});
|
|
328
|
+
for (const [index, object] of objects.entries()) {
|
|
329
|
+
const relativeObjectPath = snapshotObjectPath(object.key);
|
|
330
|
+
const absoluteObjectPath = join(scratchDir, relativeObjectPath);
|
|
331
|
+
console.log(`[${index + 1}/${objects.length}] ${object.key}`);
|
|
332
|
+
await context.downloadObject(object.key, absoluteObjectPath);
|
|
333
|
+
}
|
|
304
334
|
}
|
|
305
335
|
|
|
306
336
|
await writeFile(
|
|
307
337
|
join(scratchDir, "meta.json"),
|
|
308
338
|
JSON.stringify(
|
|
309
|
-
buildSnapshotMeta(
|
|
310
|
-
{
|
|
311
|
-
runtime,
|
|
312
|
-
label: getCliRuntimeLabel(runtime),
|
|
313
|
-
},
|
|
314
|
-
site,
|
|
315
|
-
),
|
|
316
|
-
null,
|
|
317
|
-
2,
|
|
318
|
-
) + "\n",
|
|
319
|
-
);
|
|
320
|
-
await writeFile(
|
|
321
|
-
join(scratchDir, "storage-manifest.json"),
|
|
322
|
-
JSON.stringify(
|
|
323
|
-
{
|
|
324
|
-
version: 1,
|
|
325
|
-
objects: manifestObjects,
|
|
326
|
-
},
|
|
339
|
+
buildSnapshotMeta(site, { dialect: context.dialect }),
|
|
327
340
|
null,
|
|
328
341
|
2,
|
|
329
342
|
) + "\n",
|
|
@@ -16,24 +16,25 @@ import { loadNodeRuntime } from "../../../lib/load-node-runtime.js";
|
|
|
16
16
|
import { openNodeDatabase } from "../../../lib/node-database.js";
|
|
17
17
|
import { deleteR2Object, uploadR2Object } from "../../../lib/r2-query.js";
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
assertSnapshotDialectMatches,
|
|
20
20
|
assertSnapshotMeta,
|
|
21
21
|
buildReplaceSql,
|
|
22
22
|
buildSnapshotStorageQuery,
|
|
23
23
|
collectSnapshotObjects,
|
|
24
|
+
enumerateSnapshotObjectFiles,
|
|
25
|
+
extractMediaStorageKeysFromDumpSql,
|
|
24
26
|
getSnapshotBootstrapSite,
|
|
25
27
|
normalizeD1Sql,
|
|
26
|
-
|
|
28
|
+
remapSnapshotObjectKey,
|
|
27
29
|
rewriteLegacySnapshotSql,
|
|
28
30
|
rewriteSnapshotSiteIdentifiers,
|
|
29
|
-
sha256File,
|
|
30
31
|
validateSnapshotTargetSite,
|
|
31
32
|
} from "../../../lib/site-snapshot.js";
|
|
32
33
|
import {
|
|
33
34
|
getCliSiteResolutionMode,
|
|
34
35
|
resolveCliSite,
|
|
35
36
|
} from "../../../lib/site-selection.js";
|
|
36
|
-
import {
|
|
37
|
+
import { bootstrapCliRuntime } from "../../../lib/runtime-target.js";
|
|
37
38
|
|
|
38
39
|
function isZipPath(filePath) {
|
|
39
40
|
return filePath.toLowerCase().endsWith(".zip");
|
|
@@ -52,10 +53,15 @@ function createWranglerOptions(values) {
|
|
|
52
53
|
|
|
53
54
|
async function createNodeImportContext() {
|
|
54
55
|
const nodeDatabase = await openNodeDatabase(process.env);
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// Only need the storage driver — `createNodeCliRuntime` would also resolve
|
|
57
|
+
// the current site, which (a) is redundant with the bin-level resolveCliSite
|
|
58
|
+
// call below and (b) prints a generic "/setup first" error when the
|
|
59
|
+
// snapshot's own error path is more informative.
|
|
60
|
+
const { createStorageDriver } = await loadNodeRuntime();
|
|
61
|
+
const storage = createStorageDriver(nodeDatabase.bindings);
|
|
57
62
|
|
|
58
63
|
return {
|
|
64
|
+
dialect: nodeDatabase.database.dialect,
|
|
59
65
|
async close() {
|
|
60
66
|
await nodeDatabase.close();
|
|
61
67
|
},
|
|
@@ -66,20 +72,20 @@ async function createNodeImportContext() {
|
|
|
66
72
|
await nodeDatabase.execute(sql);
|
|
67
73
|
},
|
|
68
74
|
async uploadObject(key, filePath, contentType) {
|
|
69
|
-
if (!
|
|
75
|
+
if (!storage) {
|
|
70
76
|
throw new Error("Snapshot import requires configured storage.");
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
const bytes = new Uint8Array(await readFile(filePath));
|
|
74
|
-
await
|
|
80
|
+
await storage.put(key, bytes, {
|
|
75
81
|
contentType: contentType || undefined,
|
|
76
82
|
});
|
|
77
83
|
},
|
|
78
84
|
async deleteObject(key) {
|
|
79
|
-
if (!
|
|
85
|
+
if (!storage) {
|
|
80
86
|
return;
|
|
81
87
|
}
|
|
82
|
-
await
|
|
88
|
+
await storage.delete(key);
|
|
83
89
|
},
|
|
84
90
|
};
|
|
85
91
|
}
|
|
@@ -88,6 +94,7 @@ function createD1ImportContext(runtime, values) {
|
|
|
88
94
|
const wranglerOptions = createWranglerOptions(values);
|
|
89
95
|
|
|
90
96
|
return {
|
|
97
|
+
dialect: "sqlite",
|
|
91
98
|
async close() {},
|
|
92
99
|
async query(sql) {
|
|
93
100
|
return queryD1(sql, runtime, wranglerOptions);
|
|
@@ -155,23 +162,6 @@ async function readSnapshotJson(rootDir, filename) {
|
|
|
155
162
|
return JSON.parse(await readFile(absolutePath, "utf-8"));
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
async function validateManifestObjects(rootDir, manifest) {
|
|
159
|
-
for (const object of manifest.objects) {
|
|
160
|
-
const absolutePath = join(rootDir, object.file);
|
|
161
|
-
const fileStat = await stat(absolutePath).catch(() => null);
|
|
162
|
-
if (!fileStat?.isFile()) {
|
|
163
|
-
throw new Error(`Snapshot object file is missing: ${object.file}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const actualHash = await sha256File(absolutePath);
|
|
167
|
-
if (actualHash !== object.sha256) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`Snapshot object checksum mismatch for ${object.key}: expected ${object.sha256}, got ${actualHash}`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
165
|
export async function run(argv) {
|
|
176
166
|
const { values } = parseArgs({
|
|
177
167
|
args: argv,
|
|
@@ -184,12 +174,14 @@ export async function run(argv) {
|
|
|
184
174
|
host: { type: "string" },
|
|
185
175
|
help: { type: "boolean", short: "h" },
|
|
186
176
|
local: { type: "boolean", default: false },
|
|
177
|
+
node: { type: "boolean", default: false },
|
|
187
178
|
path: { type: "string", default: "." },
|
|
188
179
|
"path-prefix": { type: "string" },
|
|
189
180
|
"persist-to": { type: "string" },
|
|
190
181
|
remote: { type: "boolean", default: false },
|
|
191
182
|
replace: { type: "boolean", default: false },
|
|
192
183
|
"remap-site": { type: "boolean", default: false },
|
|
184
|
+
"allow-missing-objects": { type: "boolean", default: false },
|
|
193
185
|
site: { type: "string" },
|
|
194
186
|
url: { type: "string" },
|
|
195
187
|
},
|
|
@@ -197,7 +189,7 @@ export async function run(argv) {
|
|
|
197
189
|
|
|
198
190
|
if (values.help) {
|
|
199
191
|
console.log(
|
|
200
|
-
"Usage: jant site snapshot import --path <dir|zip> --replace [--local | --remote]",
|
|
192
|
+
"Usage: jant site snapshot import --path <dir|zip> --replace [--local | --remote | --node]",
|
|
201
193
|
);
|
|
202
194
|
console.log("");
|
|
203
195
|
console.log(
|
|
@@ -219,6 +211,9 @@ export async function run(argv) {
|
|
|
219
211
|
" --local Force local D1 instead of DATABASE_URL",
|
|
220
212
|
);
|
|
221
213
|
console.log(" --remote Import into remote D1");
|
|
214
|
+
console.log(
|
|
215
|
+
" --node Force Node runtime even if DATABASE_URL is unset",
|
|
216
|
+
);
|
|
222
217
|
console.log(
|
|
223
218
|
" --config Wrangler config file (default: wrangler.toml)",
|
|
224
219
|
);
|
|
@@ -236,6 +231,27 @@ export async function run(argv) {
|
|
|
236
231
|
console.log(
|
|
237
232
|
" --remap-site Rewrite snapshot site_id and storage keys to the resolved target site",
|
|
238
233
|
);
|
|
234
|
+
console.log(
|
|
235
|
+
" --allow-missing-objects Continue importing even when objects/ is missing files referenced by db.sql.",
|
|
236
|
+
);
|
|
237
|
+
console.log(
|
|
238
|
+
" Use this when the target storage already has those keys (e.g. a snapshot",
|
|
239
|
+
);
|
|
240
|
+
console.log(
|
|
241
|
+
" exported with --skip-objects between sites that share an R2 bucket).",
|
|
242
|
+
);
|
|
243
|
+
console.log(
|
|
244
|
+
" Without this flag, import aborts before applying db.sql and prints the",
|
|
245
|
+
);
|
|
246
|
+
console.log(" missing key list.");
|
|
247
|
+
console.log("");
|
|
248
|
+
console.log(
|
|
249
|
+
"`.env.node` next to your project (or in packages/core/) is auto-loaded.",
|
|
250
|
+
);
|
|
251
|
+
console.log(
|
|
252
|
+
"If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
|
|
253
|
+
);
|
|
254
|
+
console.log("this command uses the Node database runtime.");
|
|
239
255
|
console.log("");
|
|
240
256
|
console.log(
|
|
241
257
|
"In single-site mode, snapshot imports automatically remap to the only initialized site.",
|
|
@@ -253,7 +269,7 @@ export async function run(argv) {
|
|
|
253
269
|
);
|
|
254
270
|
}
|
|
255
271
|
|
|
256
|
-
const runtime =
|
|
272
|
+
const { runtime } = bootstrapCliRuntime(values);
|
|
257
273
|
const inputPath = resolve(process.cwd(), values.path);
|
|
258
274
|
const materialized = await materializeSnapshotInput(inputPath);
|
|
259
275
|
const context =
|
|
@@ -263,16 +279,10 @@ export async function run(argv) {
|
|
|
263
279
|
|
|
264
280
|
try {
|
|
265
281
|
const meta = await readSnapshotJson(materialized.rootDir, "meta.json");
|
|
266
|
-
const rawManifest = await readSnapshotJson(
|
|
267
|
-
materialized.rootDir,
|
|
268
|
-
"storage-manifest.json",
|
|
269
|
-
);
|
|
270
282
|
assertSnapshotMeta(meta);
|
|
283
|
+
assertSnapshotDialectMatches(meta, context.dialect);
|
|
271
284
|
const explicitRemap = values["remap-site"] === true;
|
|
272
285
|
const snapshotSite = getSnapshotBootstrapSite(meta);
|
|
273
|
-
const originalManifest = rawManifest;
|
|
274
|
-
assertSnapshotManifest(originalManifest);
|
|
275
|
-
await validateManifestObjects(materialized.rootDir, originalManifest);
|
|
276
286
|
const resolutionMode = getCliSiteResolutionMode(process.env);
|
|
277
287
|
|
|
278
288
|
const { site: targetSite } = await resolveCliSite(context, {
|
|
@@ -307,17 +317,63 @@ export async function run(argv) {
|
|
|
307
317
|
);
|
|
308
318
|
}
|
|
309
319
|
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
:
|
|
320
|
+
const sourceSiteId = shouldRemapSite ? (snapshotSite?.id ?? "") : "";
|
|
321
|
+
const objectFiles = await enumerateSnapshotObjectFiles(
|
|
322
|
+
materialized.rootDir,
|
|
323
|
+
);
|
|
324
|
+
const snapshotObjects = objectFiles.map((entry) => ({
|
|
325
|
+
filePath: entry.filePath,
|
|
326
|
+
contentType: entry.contentType,
|
|
327
|
+
key: shouldRemapSite
|
|
328
|
+
? remapSnapshotObjectKey(entry.key, sourceSiteId, targetSite.id)
|
|
329
|
+
: entry.key,
|
|
330
|
+
}));
|
|
331
|
+
const snapshotKeys = new Set(snapshotObjects.map((object) => object.key));
|
|
332
|
+
|
|
333
|
+
const rawDbSql = await readFile(
|
|
334
|
+
join(materialized.rootDir, "db.sql"),
|
|
335
|
+
"utf-8",
|
|
336
|
+
);
|
|
317
337
|
|
|
318
|
-
|
|
319
|
-
|
|
338
|
+
// Preflight: every storage_key/poster_key referenced by db.sql must have
|
|
339
|
+
// a corresponding file in objects/, or we'll end up with broken media
|
|
340
|
+
// unless the target storage already has it. Default to abort; let the
|
|
341
|
+
// user override with --allow-missing-objects when they know the files
|
|
342
|
+
// already live in the target bucket (typical --skip-objects flow).
|
|
343
|
+
const sourceKeysInObjects = new Set(objectFiles.map((entry) => entry.key));
|
|
344
|
+
const expectedSourceKeys = extractMediaStorageKeysFromDumpSql(
|
|
345
|
+
rawDbSql,
|
|
346
|
+
snapshotSite?.id ?? "",
|
|
320
347
|
);
|
|
348
|
+
const missingFromObjects = [...expectedSourceKeys]
|
|
349
|
+
.filter((key) => !sourceKeysInObjects.has(key))
|
|
350
|
+
.sort();
|
|
351
|
+
|
|
352
|
+
if (missingFromObjects.length > 0) {
|
|
353
|
+
const display = missingFromObjects.slice(0, 10);
|
|
354
|
+
const remainder = missingFromObjects.length - display.length;
|
|
355
|
+
console.warn(
|
|
356
|
+
`\n${missingFromObjects.length} object(s) referenced by db.sql are missing from the snapshot's objects/:`,
|
|
357
|
+
);
|
|
358
|
+
for (const key of display) {
|
|
359
|
+
console.warn(` ${key}`);
|
|
360
|
+
}
|
|
361
|
+
if (remainder > 0) {
|
|
362
|
+
console.warn(` …and ${remainder} more`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!values["allow-missing-objects"]) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"Snapshot is missing storage objects referenced by db.sql. " +
|
|
368
|
+
"Pass --allow-missing-objects to import anyway (only safe when the target storage already has these keys).",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.warn(
|
|
373
|
+
"Continuing with --allow-missing-objects; the target storage must already have these keys or media references will 404.\n",
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
321
377
|
const currentObjectRows = await context.query(
|
|
322
378
|
buildSnapshotStorageQuery(targetSite.id),
|
|
323
379
|
);
|
|
@@ -325,18 +381,14 @@ export async function run(argv) {
|
|
|
325
381
|
collectSnapshotObjects(currentObjectRows).map((object) => object.key),
|
|
326
382
|
);
|
|
327
383
|
|
|
328
|
-
for (const object of
|
|
384
|
+
for (const object of snapshotObjects) {
|
|
329
385
|
await context.uploadObject(
|
|
330
386
|
object.key,
|
|
331
|
-
|
|
332
|
-
|
|
387
|
+
object.filePath,
|
|
388
|
+
object.contentType || "",
|
|
333
389
|
);
|
|
334
390
|
}
|
|
335
391
|
|
|
336
|
-
const rawDbSql = await readFile(
|
|
337
|
-
join(materialized.rootDir, "db.sql"),
|
|
338
|
-
"utf-8",
|
|
339
|
-
);
|
|
340
392
|
const dbSql = snapshotSite
|
|
341
393
|
? shouldRemapSite
|
|
342
394
|
? rewriteSnapshotSiteIdentifiers(
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
function stripSurroundingQuotes(value) {
|
|
6
|
+
if (value.length < 2) return value;
|
|
7
|
+
const first = value[0];
|
|
8
|
+
const last = value[value.length - 1];
|
|
9
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
10
|
+
return value.slice(1, -1);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fileExists(envPath) {
|
|
16
|
+
try {
|
|
17
|
+
readFileSync(envPath, "utf8");
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Locate `.env.node` for CLI auto-load. Searches, in order:
|
|
26
|
+
* 1. `<cwd>/.env.node` — user's site directory
|
|
27
|
+
* 2. `<bin>/../../.env.node` — `packages/core/.env.node` (in-repo dev)
|
|
28
|
+
*
|
|
29
|
+
* Returns the first existing path, or `null` if none is found.
|
|
30
|
+
*/
|
|
31
|
+
export function findNodeEnvPath(cwd = process.cwd()) {
|
|
32
|
+
const candidates = [
|
|
33
|
+
resolve(cwd, ".env.node"),
|
|
34
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "../../.env.node"),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (fileExists(candidate)) {
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse the .env.node file and assign keys into `env`. Existing values in
|
|
48
|
+
* `env` are preserved (already-exported shell vars win over file values).
|
|
49
|
+
*
|
|
50
|
+
* Returns a result object useful for debug logging:
|
|
51
|
+
* { envPath, found, assignedKeys, skippedKeys }
|
|
52
|
+
*/
|
|
53
|
+
export function loadNodeEnvFile(envPath, env = process.env) {
|
|
54
|
+
const result = {
|
|
55
|
+
envPath,
|
|
56
|
+
found: false,
|
|
57
|
+
assignedKeys: [],
|
|
58
|
+
skippedKeys: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let content;
|
|
62
|
+
try {
|
|
63
|
+
content = readFileSync(envPath, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
result.found = true;
|
|
69
|
+
for (const line of content.split("\n")) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
72
|
+
const eqIdx = trimmed.indexOf("=");
|
|
73
|
+
if (eqIdx < 1) continue;
|
|
74
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
75
|
+
const value = stripSurroundingQuotes(trimmed.slice(eqIdx + 1).trim());
|
|
76
|
+
if (key in env) {
|
|
77
|
+
result.skippedKeys.push(key);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
env[key] = value;
|
|
81
|
+
result.assignedKeys.push(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Auto-locate and load `.env.node` for any DB-touching CLI command.
|
|
89
|
+
*
|
|
90
|
+
* Always called before `resolveCliRuntime()`, so DATABASE_URL / DATA_DIR
|
|
91
|
+
* defined in `.env.node` make `--node` (or auto-detect) work without
|
|
92
|
+
* requiring the user to source the file manually.
|
|
93
|
+
*/
|
|
94
|
+
export function autoloadNodeEnv(env = process.env) {
|
|
95
|
+
const envPath = findNodeEnvPath();
|
|
96
|
+
if (!envPath) {
|
|
97
|
+
return { envPath: null, found: false, assignedKeys: [], skippedKeys: [] };
|
|
98
|
+
}
|
|
99
|
+
return loadNodeEnvFile(envPath, env);
|
|
100
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { autoloadNodeEnv } from "./node-env.js";
|
|
2
|
+
import { resolveDatabaseDialect } from "./node-sqlite.js";
|
|
3
|
+
|
|
1
4
|
export function resolveCliRuntime(values, env = process.env) {
|
|
2
5
|
const flags = [values.local, values.remote, values.node].filter(Boolean);
|
|
3
6
|
if (flags.length > 1) {
|
|
@@ -36,3 +39,64 @@ export function getCliRuntimeLabel(runtime) {
|
|
|
36
39
|
return "Node database";
|
|
37
40
|
}
|
|
38
41
|
}
|
|
42
|
+
|
|
43
|
+
function describeNodeTarget(env) {
|
|
44
|
+
const databaseUrl = env.DATABASE_URL;
|
|
45
|
+
if (typeof databaseUrl === "string" && databaseUrl.length > 0) {
|
|
46
|
+
const dialect = resolveDatabaseDialect(databaseUrl);
|
|
47
|
+
if (dialect === "sqlite") {
|
|
48
|
+
return `sqlite ${databaseUrl}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const parsed = new URL(databaseUrl);
|
|
53
|
+
const protocol = parsed.protocol.replace(/:$/, "");
|
|
54
|
+
const host = parsed.hostname || "?";
|
|
55
|
+
const port = parsed.port ? `:${parsed.port}` : "";
|
|
56
|
+
const database = parsed.pathname.replace(/^\/+/, "") || "?";
|
|
57
|
+
return `${protocol} ${host}${port}/${database}`;
|
|
58
|
+
} catch {
|
|
59
|
+
return "<invalid DATABASE_URL>";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof env.DATA_DIR === "string" && env.DATA_DIR.length > 0) {
|
|
64
|
+
return `DATA_DIR=${env.DATA_DIR}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return "<unset>";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatRuntimeBanner(runtime, env = process.env) {
|
|
71
|
+
switch (runtime) {
|
|
72
|
+
case "node":
|
|
73
|
+
return `[jant] target = node (${describeNodeTarget(env)})`;
|
|
74
|
+
case "d1-remote":
|
|
75
|
+
return "[jant] target = remote D1 (wrangler)";
|
|
76
|
+
case "d1-local":
|
|
77
|
+
default:
|
|
78
|
+
return "[jant] target = local D1 (wrangler)";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One-call helper for DB-touching CLI commands:
|
|
84
|
+
* 1. Auto-load `.env.node` (so DATABASE_URL/DATA_DIR work without sourcing).
|
|
85
|
+
* 2. Resolve the runtime from flags and env.
|
|
86
|
+
* 3. Print a one-line banner so the user immediately sees which target
|
|
87
|
+
* was picked, instead of finding out minutes later when something fails.
|
|
88
|
+
*
|
|
89
|
+
* Pass `{ silent: true }` to suppress the banner (e.g. in tests or when the
|
|
90
|
+
* caller wants to print its own header first).
|
|
91
|
+
*/
|
|
92
|
+
export function bootstrapCliRuntime(values, options = {}) {
|
|
93
|
+
const env = options.env ?? process.env;
|
|
94
|
+
const envLoad = autoloadNodeEnv(env);
|
|
95
|
+
const runtime = resolveCliRuntime(values, env);
|
|
96
|
+
|
|
97
|
+
if (!options.silent) {
|
|
98
|
+
console.log(formatRuntimeBanner(runtime, env));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { runtime, envLoad };
|
|
102
|
+
}
|