@jant/core 0.3.46 → 0.3.48

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.
Files changed (110) hide show
  1. package/bin/commands/db/execute-file.js +12 -4
  2. package/bin/commands/db/rehearse.js +2 -2
  3. package/bin/commands/export.js +12 -4
  4. package/bin/commands/import-site.js +60 -267
  5. package/bin/commands/migrate.js +36 -69
  6. package/bin/commands/reset-password.js +10 -4
  7. package/bin/commands/site/export.js +59 -248
  8. package/bin/commands/site/snapshot/export.js +58 -45
  9. package/bin/commands/site/snapshot/import.js +104 -52
  10. package/bin/lib/node-env.js +100 -0
  11. package/bin/lib/runtime-target.js +64 -0
  12. package/bin/lib/site-snapshot.js +185 -54
  13. package/bin/lib/sql-export.js +19 -2
  14. package/dist/app-DU7dpJID.js +6 -0
  15. package/dist/{app-DB-P66E5.js → app-DdnIoX7y.js} +333 -191
  16. package/dist/client/.vite/manifest.json +2 -2
  17. package/dist/client/_assets/client-BoUn7xBo.css +2 -0
  18. package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
  19. package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
  20. package/dist/github-sync-C593r22F.js +4 -0
  21. package/dist/github-sync-bL1hnx3Q.js +428 -0
  22. package/dist/index.js +3 -2
  23. package/dist/node.js +5 -4
  24. package/package.json +3 -2
  25. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  26. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
  27. package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
  28. package/src/client/components/jant-settings-general.ts +164 -22
  29. package/src/client/components/settings-types.ts +4 -6
  30. package/src/client-auth.ts +1 -1
  31. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  32. package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
  33. package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
  34. package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
  35. package/src/db/migrations/meta/0021_snapshot.json +2121 -0
  36. package/src/db/migrations/meta/_journal.json +7 -0
  37. package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
  38. package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
  39. package/src/db/migrations/pg/meta/_journal.json +7 -0
  40. package/src/db/pg/schema.ts +21 -26
  41. package/src/db/rehearsal-fixtures/demo-current.json +1 -1
  42. package/src/db/schema.ts +16 -20
  43. package/src/i18n/__tests__/middleware.test.ts +43 -1
  44. package/src/i18n/coverage.generated.ts +17 -0
  45. package/src/i18n/i18n.ts +18 -2
  46. package/src/i18n/index.ts +3 -0
  47. package/src/i18n/locales/settings/en.po +16 -11
  48. package/src/i18n/locales/settings/en.ts +1 -1
  49. package/src/i18n/locales/settings/zh-Hans.po +17 -12
  50. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  51. package/src/i18n/locales/settings/zh-Hant.po +16 -11
  52. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  53. package/src/i18n/locales.ts +84 -2
  54. package/src/i18n/middleware.ts +25 -16
  55. package/src/i18n/supported-locales.ts +153 -0
  56. package/src/lib/__tests__/csp-builder.test.ts +19 -2
  57. package/src/lib/__tests__/feed.test.ts +242 -1
  58. package/src/lib/__tests__/post-meta.test.ts +0 -1
  59. package/src/lib/__tests__/view.test.ts +0 -1
  60. package/src/lib/api-posts.ts +9 -7
  61. package/src/lib/csp-builder.ts +28 -10
  62. package/src/lib/feed.ts +153 -3
  63. package/src/middleware/__tests__/secure-headers.test.ts +89 -0
  64. package/src/middleware/auth.ts +1 -1
  65. package/src/middleware/secure-headers.ts +47 -1
  66. package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
  67. package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
  68. package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
  69. package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
  70. package/src/node/__tests__/cli-sql-export.test.ts +49 -0
  71. package/src/node/index.ts +1 -0
  72. package/src/preset.css +8 -2
  73. package/src/routes/api/__tests__/settings.test.ts +3 -2
  74. package/src/routes/api/github-sync.tsx +1 -1
  75. package/src/routes/api/settings.ts +4 -1
  76. package/src/routes/auth/signin.tsx +6 -0
  77. package/src/routes/pages/archive.tsx +4 -2
  78. package/src/services/__tests__/post.test.ts +19 -19
  79. package/src/services/__tests__/search.test.ts +0 -1
  80. package/src/services/__tests__/settings.test.ts +22 -3
  81. package/src/services/bootstrap.ts +7 -3
  82. package/src/services/collection.ts +3 -3
  83. package/src/services/export.ts +0 -3
  84. package/src/services/navigation.ts +0 -2
  85. package/src/services/path.ts +1 -38
  86. package/src/services/post.ts +32 -66
  87. package/src/services/search.ts +0 -6
  88. package/src/services/settings.ts +47 -6
  89. package/src/services/site-admin.ts +6 -1
  90. package/src/styles/ui.css +14 -25
  91. package/src/types/entities.ts +0 -1
  92. package/src/ui/color-themes.ts +1 -1
  93. package/src/ui/dash/settings/GeneralContent.tsx +17 -19
  94. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
  95. package/src/ui/feed/NoteCard.tsx +1 -11
  96. package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
  97. package/src/ui/pages/PostPage.tsx +2 -0
  98. package/bin/commands/collections.js +0 -268
  99. package/bin/commands/media.js +0 -302
  100. package/bin/commands/posts.js +0 -262
  101. package/bin/commands/search.js +0 -53
  102. package/bin/commands/settings.js +0 -93
  103. package/bin/lib/http-api.js +0 -223
  104. package/bin/lib/media-upload.js +0 -206
  105. package/dist/app-CM7sb3xO.js +0 -5
  106. package/dist/client/_assets/client-DDs6NzB3.css +0 -2
  107. package/src/__tests__/bin/content-cli.test.ts +0 -179
  108. package/src/__tests__/bin/media-cli.test.ts +0 -192
  109. /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
  110. /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
@@ -4,8 +4,8 @@ import { executeD1, queryD1 } from "../lib/d1-query.js";
4
4
  import { openNodeDatabase } from "../lib/node-database.js";
5
5
  import { loadNodeRuntime } from "../lib/load-node-runtime.js";
6
6
  import {
7
+ bootstrapCliRuntime,
7
8
  getCliRuntimeLabel,
8
- resolveCliRuntime,
9
9
  } from "../lib/runtime-target.js";
10
10
  import { resolveCliSite } from "../lib/site-selection.js";
11
11
 
@@ -18,6 +18,7 @@ export async function run(argv) {
18
18
  env: { type: "string" },
19
19
  host: { type: "string" },
20
20
  local: { type: "boolean", default: false },
21
+ node: { type: "boolean", default: false },
21
22
  "path-prefix": { type: "string" },
22
23
  "persist-to": { type: "string" },
23
24
  remote: { type: "boolean", default: false },
@@ -28,13 +29,14 @@ export async function run(argv) {
28
29
  });
29
30
 
30
31
  if (values.help) {
31
- console.log("Usage: jant reset-password [--local | --remote]");
32
+ console.log("Usage: jant reset-password [--local | --remote | --node]");
32
33
  console.log("");
33
34
  console.log("Generate a password reset token (expires in 15 minutes).");
34
35
  console.log("");
35
36
  console.log("Options:");
36
37
  console.log(" --local Force local D1 instead of DATABASE_URL");
37
38
  console.log(" --remote Run against remote D1 database (default: local)");
39
+ console.log(" --node Force Node runtime even if DATABASE_URL is unset");
38
40
  console.log(" --site Target site id");
39
41
  console.log(" --host Target site host");
40
42
  console.log(" --url Target site URL");
@@ -45,12 +47,16 @@ export async function run(argv) {
45
47
  console.log(" --persist-to Local D1 state directory override");
46
48
  console.log("");
47
49
  console.log(
48
- "If DATABASE_URL or DATA_DIR is set and no runtime flag is passed, this command uses the Node database runtime.",
50
+ "`.env.node` next to your project (or in packages/core/) is auto-loaded.",
49
51
  );
52
+ console.log(
53
+ "If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
54
+ );
55
+ console.log("this command uses the Node database runtime.");
50
56
  process.exit(0);
51
57
  }
52
58
 
53
- const runtime = resolveCliRuntime(values);
59
+ const { runtime } = bootstrapCliRuntime(values);
54
60
 
55
61
  const token = randomBytes(32).toString("hex");
56
62
  const hash = createHash("sha256").update(token).digest("hex");
@@ -6,22 +6,8 @@ import {
6
6
  CLI_API_TOKEN_ENV_VAR,
7
7
  getCliApiToken,
8
8
  } from "../../lib/cli-api-token.js";
9
- import { openNodeDatabase } from "../../lib/node-database.js";
10
- import { loadNodeRuntime } from "../../lib/load-node-runtime.js";
11
9
  import { pullSiteExportZipBytes } from "../../lib/site-pull-media.js";
12
10
 
13
- function describeLocalExportSource(input) {
14
- if (input.siteUrl) {
15
- return input.siteUrl;
16
- }
17
-
18
- if (input.siteDomain?.host) {
19
- return `https://${input.siteDomain.host}${input.siteDomain.pathPrefix || ""}`;
20
- }
21
-
22
- return `site "${input.site.key}"`;
23
- }
24
-
25
11
  async function exportRemoteSite(url, token) {
26
12
  const response = await fetch(`${url.replace(/\/$/, "")}/api/export/hugo`, {
27
13
  method: "POST",
@@ -83,189 +69,46 @@ function logPullProgress(event) {
83
69
  }
84
70
  }
85
71
 
86
- function getStorageKeyFromUrl(url, appConfig) {
87
- try {
88
- const resolvedUrl = new URL(url, appConfig.siteUrl);
89
- let pathname = resolvedUrl.pathname;
90
- const publicPathPrefixes = [
91
- appConfig.r2PublicUrl,
92
- appConfig.s3PublicUrl,
93
- appConfig.localPublicUrl,
94
- ]
95
- .filter(Boolean)
96
- .map((value) => {
97
- try {
98
- const parsed = new URL(value);
99
- return parsed.pathname.replace(/\/+$/, "");
100
- } catch {
101
- return "";
102
- }
103
- })
104
- .filter(Boolean);
105
-
106
- for (const prefix of publicPathPrefixes) {
107
- if (pathname.startsWith(`${prefix}/`)) {
108
- pathname = pathname.slice(prefix.length + 1);
109
- break;
110
- }
111
- if (pathname === prefix) {
112
- pathname = "";
113
- break;
114
- }
115
- }
116
-
117
- const sitePathPrefix = appConfig.sitePathPrefix || "";
118
- if (sitePathPrefix && pathname.startsWith(`${sitePathPrefix}/`)) {
119
- pathname = pathname.slice(sitePathPrefix.length + 1);
120
- } else {
121
- pathname = pathname.replace(/^\/+/, "");
122
- }
123
-
124
- if (!pathname.startsWith("media/") && !pathname.startsWith("favicon/")) {
125
- return null;
126
- }
127
-
128
- return pathname;
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- async function readStorageBody(body) {
135
- const reader = body.getReader();
136
- const chunks = [];
137
- let totalLength = 0;
138
-
139
- for (;;) {
140
- const { done, value } = await reader.read();
141
- if (done) break;
142
- chunks.push(value);
143
- totalLength += value.length;
144
- }
145
-
146
- const bytes = new Uint8Array(totalLength);
147
- let offset = 0;
148
- for (const chunk of chunks) {
149
- bytes.set(chunk, offset);
150
- offset += chunk.length;
151
- }
152
-
153
- return bytes;
154
- }
155
-
156
- function createLocalAssetLoader(storage, appConfig) {
157
- if (!storage) {
158
- return null;
159
- }
160
-
161
- return async ({ resolvedUrl }) => {
162
- const storageKey = getStorageKeyFromUrl(resolvedUrl, appConfig);
163
- if (!storageKey) {
164
- return null;
165
- }
166
-
167
- const object = await storage.get(storageKey);
168
- if (!object?.body) {
169
- return null;
170
- }
171
-
172
- return {
173
- bytes: await readStorageBody(object.body),
174
- contentType: object.contentType || "",
175
- };
176
- };
177
- }
178
-
179
- async function exportLocalSite(env = process.env) {
180
- const nodeDatabase = await openNodeDatabase(env);
181
-
182
- try {
183
- const {
184
- createExportService,
185
- createNodeCliRuntime,
186
- resolveConfig,
187
- buildThemeStyle,
188
- BUILTIN_COLOR_THEMES,
189
- BUILTIN_FONT_THEMES,
190
- getCjkSerifCssVariables,
191
- getFontThemeCssVariables,
192
- } = await loadNodeRuntime();
193
- const runtime = await createNodeCliRuntime(nodeDatabase.bindings);
194
- const allSettings = await runtime.services.settings.getAll();
195
- const navItems = await runtime.services.navItems.list();
196
- const appConfig = resolveConfig(nodeDatabase.bindings, allSettings);
197
- const activeTheme = BUILTIN_COLOR_THEMES.find(
198
- (theme) => theme.id === (appConfig.themeId || appConfig.defaultThemeId),
199
- );
200
- const fontTheme = appConfig.fontThemeId
201
- ? BUILTIN_FONT_THEMES.find((theme) => theme.id === appConfig.fontThemeId)
202
- : undefined;
203
- const fontOverrides = {
204
- ...getCjkSerifCssVariables(appConfig.siteLanguage),
205
- ...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
206
- };
207
- const themeCss = buildThemeStyle(
208
- activeTheme,
209
- appConfig.themeMode,
210
- fontOverrides,
211
- );
212
- const appleTouchKey = allSettings.SITE_FAVICON_APPLE_TOUCH || "";
213
- const exportService = createExportService(
214
- runtime.services,
215
- {
216
- siteName: appConfig.siteName,
217
- siteUrl: appConfig.siteUrl,
218
- siteDescription: appConfig.siteDescription,
219
- siteLanguage: appConfig.siteLanguage,
220
- showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
221
- homeDefaultView: appConfig.homeDefaultView,
222
- mainRssFeed: appConfig.mainRssFeed,
223
- siteFooter: appConfig.siteFooter,
224
- showHeaderAvatar: appConfig.showHeaderAvatar,
225
- siteAvatarUrl: appConfig.siteAvatarUrl,
226
- faviconIcoBase64: allSettings.SITE_FAVICON_ICO || undefined,
227
- appleTouchIconStorageKey: appleTouchKey || undefined,
228
- faviconVersion: appConfig.faviconVersion,
229
- themeId: appConfig.themeId,
230
- defaultThemeId: appConfig.defaultThemeId,
231
- fontThemeId: appConfig.fontThemeId,
232
- themeMode: appConfig.themeMode,
233
- noindex: appConfig.noindex,
234
- themeCss,
235
- customCss: appConfig.customCSS,
236
- r2PublicUrl: appConfig.r2PublicUrl,
237
- s3PublicUrl: appConfig.s3PublicUrl,
238
- localPublicUrl: appConfig.localPublicUrl,
239
- imageTransformUrl: appConfig.imageTransformUrl,
240
- sitePathPrefix: appConfig.sitePathPrefix,
241
- navItems,
242
- pageSize: appConfig.pageSize,
243
- archivePageSize: appConfig.archivePageSize,
244
- rssFeedLimit: appConfig.rssFeedLimit,
245
- },
246
- {
247
- storage: runtime.storage,
248
- },
249
- );
250
-
251
- return {
252
- zip: await exportService.generateHugoSite(),
253
- assetLoader: createLocalAssetLoader(runtime.storage, appConfig),
254
- source: describeLocalExportSource({
255
- site: runtime.currentSite,
256
- siteDomain: runtime.currentSiteDomain,
257
- siteUrl: appConfig.siteUrl,
258
- }),
259
- };
260
- } finally {
261
- await nodeDatabase.close();
262
- }
72
+ function printUsage() {
73
+ console.log("Usage: jant site export <url> [options]");
74
+ console.log("");
75
+ console.log("Export a Jant site as a Hugo ZIP archive or directory.");
76
+ console.log("");
77
+ console.log("Arguments:");
78
+ console.log(" <url> Jant site URL (required)");
79
+ console.log("");
80
+ console.log("Options:");
81
+ console.log(
82
+ " --output, -o Output ZIP path (default: jant-site-export.zip)",
83
+ );
84
+ console.log(
85
+ " --directory, -d Export directly to a directory for hugo serve/debugging",
86
+ );
87
+ console.log(
88
+ " --pull-media Download referenced media into static/media/ (default: on)",
89
+ );
90
+ console.log(" --no-pull-media Skip the media pull and keep original URLs");
91
+ console.log(" --token API token (overrides JANT_API_TOKEN)");
92
+ console.log("");
93
+ console.log("Authentication:");
94
+ console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
95
+ console.log(" jant site export https://your-site.example");
96
+ console.log("");
97
+ console.log("Examples:");
98
+ console.log(
99
+ " jant site export https://your-site.example -o ./export.zip",
100
+ );
101
+ console.log(
102
+ " jant site export https://your-site.example -d ./jant-site && cd ./jant-site && hugo serve",
103
+ );
263
104
  }
264
105
 
265
106
  export async function run(argv) {
266
107
  const noPullMedia = argv.includes("--no-pull-media");
267
- const { values } = parseArgs({
268
- args: argv.filter((arg) => arg !== "--no-pull-media"),
108
+ const filteredArgv = argv.filter((arg) => arg !== "--no-pull-media");
109
+ const { values, positionals } = parseArgs({
110
+ args: filteredArgv,
111
+ allowPositionals: true,
269
112
  options: {
270
113
  directory: {
271
114
  type: "string",
@@ -279,49 +122,28 @@ export async function run(argv) {
279
122
  default: "jant-site-export.zip",
280
123
  },
281
124
  token: { type: "string" },
282
- url: { type: "string" },
283
125
  },
284
126
  });
285
127
 
286
128
  if (values.help) {
287
- console.log("Usage: jant site export [--url <url>] [options]");
288
- console.log("");
289
- console.log("Export a Jant site as a Hugo ZIP archive or directory.");
290
- console.log("");
291
- console.log("Modes:");
292
- console.log(
293
- " Local No --url; exports from the local Node database runtime",
294
- );
295
- console.log(
296
- ` Remote --url requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
297
- );
298
- console.log("");
299
- console.log("Options:");
300
- console.log(" --url Remote Jant site URL");
301
- console.log(
302
- " --output, -o Output ZIP path (default: jant-site-export.zip)",
303
- );
304
- console.log(
305
- " --directory, -d Export directly to a directory for hugo serve/debugging",
306
- );
307
- console.log(
308
- " --pull-media Download referenced media into static/media/ (default: on)",
309
- );
310
- console.log(
311
- " --no-pull-media Skip the media pull and keep original URLs",
312
- );
313
- console.log(" --token API token for remote export");
314
- console.log("");
315
- console.log("Authentication:");
316
- console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
317
- console.log(" jant site export --url https://your-site.com");
318
- console.log("");
319
- console.log("Examples:");
320
- console.log(" jant site export --directory ./jant-site");
321
- console.log(" cd ./jant-site && hugo serve");
129
+ printUsage();
322
130
  process.exit(0);
323
131
  }
324
132
 
133
+ const url = positionals[0];
134
+ if (!url) {
135
+ console.error("Error: site URL is required");
136
+ console.error("");
137
+ printUsage();
138
+ process.exit(1);
139
+ }
140
+ if (positionals.length > 1) {
141
+ console.error(
142
+ `Error: unexpected extra arguments: ${positionals.slice(1).join(" ")}`,
143
+ );
144
+ process.exit(1);
145
+ }
146
+
325
147
  if (values.directory && values.output !== "jant-site-export.zip") {
326
148
  console.error("Error: use either --output or --directory, not both");
327
149
  process.exit(1);
@@ -334,40 +156,29 @@ export async function run(argv) {
334
156
  const token = getCliApiToken(process.env, values.token);
335
157
  const pullMedia = values["pull-media"] ?? !noPullMedia;
336
158
 
337
- if (values.url && !token) {
159
+ if (!token) {
338
160
  console.error(
339
- `Error: remote export requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
161
+ `Error: site export requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
340
162
  );
341
163
  process.exit(1);
342
164
  }
343
165
 
344
- console.log(
345
- values.url
346
- ? `Exporting site from ${values.url}...`
347
- : "Exporting site from the local runtime...",
348
- );
166
+ console.log(`Exporting site from ${url}...`);
349
167
 
350
- const exported = values.url
351
- ? {
352
- zip: await exportRemoteSite(values.url, token),
353
- assetLoader: null,
354
- source: values.url,
355
- }
356
- : await exportLocalSite(process.env);
357
- let zip = exported.zip;
168
+ const zipBytes = await exportRemoteSite(url, token);
169
+ let zip = zipBytes;
358
170
  let pullStats = null;
359
171
 
360
172
  if (pullMedia) {
361
173
  console.log("Preparing pull-media export ZIP...");
362
174
  const pulled = await pullSiteExportZipBytes(zip, {
363
- assetLoader: exported.assetLoader,
175
+ assetLoader: null,
364
176
  logger: logPullProgress,
365
177
  });
366
178
  zip = pulled.zipBytes;
367
179
  pullStats = pulled.stats;
368
180
  }
369
181
 
370
- const source = exported.source;
371
182
  if (outputDirectory) {
372
183
  let existingEntries = [];
373
184
  try {
@@ -393,12 +204,12 @@ export async function run(argv) {
393
204
  mkdirSync(dirname(fullPath), { recursive: true });
394
205
  writeFileSync(fullPath, Buffer.from(bytes));
395
206
  }
396
- console.log(`Exported site from ${source} to ${values.directory}`);
207
+ console.log(`Exported site from ${url} to ${values.directory}`);
397
208
  console.log(`Preview with: cd ${values.directory} && hugo serve`);
398
209
  } else {
399
210
  console.log(`Writing ${values.output}...`);
400
211
  writeFileSync(output, Buffer.from(zip));
401
- console.log(`Exported site from ${source} to ${values.output}`);
212
+ console.log(`Exported site from ${url} to ${values.output}`);
402
213
  }
403
214
 
404
215
  if (pullStats) {
@@ -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
- const { createNodeCliRuntime } = await loadNodeRuntime();
103
- const runtime = await createNodeCliRuntime(nodeDatabase.bindings);
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 (!runtime.storage) {
116
+ if (!storage) {
115
117
  throw new Error("Snapshot export requires configured storage.");
116
118
  }
117
119
 
118
- const object = await runtime.storage.get(key);
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
- "If DATABASE_URL or DATA_DIR is set and no runtime flag is passed, this command uses the Node database runtime and the configured storage driver.",
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 = resolveCliRuntime(values);
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.length > 0) {
288
- console.log(`Downloading ${objects.length} referenced object(s)...`);
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
- for (const [index, object] of objects.entries()) {
292
- const relativeObjectPath = snapshotObjectPath(object.key);
293
- const absoluteObjectPath = join(scratchDir, relativeObjectPath);
294
- console.log(`[${index + 1}/${objects.length}] ${object.key}`);
295
- await context.downloadObject(object.key, absoluteObjectPath);
296
- const fileStat = await stat(absoluteObjectPath);
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",