@jant/core 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +689 -216
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +18 -60
- package/bin/commands/site/snapshot/import.js +22 -23
- package/bin/lib/d1-query.js +87 -2
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +10 -1
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +9 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
- package/dist/client/.vite/manifest.json +20 -20
- package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
- package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
- package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
- package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
- package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
- package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
- package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
- package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
- package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
- package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
- package/dist/index.js +2 -2
- package/dist/node.js +9 -5
- package/package.json +13 -3
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +130 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +611 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +15 -6
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
- package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +4 -7
- package/src/client/components/jant-settings-general.ts +17 -12
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/i18n/locales/settings/en.po +8 -8
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +8 -8
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +8 -8
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/resolve-config.test.ts +67 -0
- package/src/lib/__tests__/schemas.test.ts +27 -1
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/discover.ts +3 -1
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/hugo-markdown.ts +46 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/resolve-config.ts +58 -1
- package/src/lib/schemas.ts +50 -5
- package/src/lib/thread-fold.ts +3 -3
- package/src/lib/timeline.ts +1 -1
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/url.ts +20 -0
- package/src/lib/view.ts +2 -2
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +38 -0
- package/src/node/index.ts +2 -0
- package/src/node/request-handler.ts +3 -1
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/export.ts +3 -3
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/posts.ts +2 -0
- package/src/routes/api/public/posts.ts +21 -1
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/compose.tsx +4 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/settings.tsx +212 -70
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +17 -11
- package/src/routes/pages/featured.tsx +11 -5
- package/src/routes/pages/page.tsx +53 -31
- package/src/routes/pages/search.tsx +4 -2
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/node.ts +49 -0
- package/src/runtime/readiness.ts +15 -0
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/path.test.ts +56 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +74 -0
- package/src/services/bootstrap.ts +263 -44
- package/src/services/custom-url.ts +2 -2
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +27 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +0 -1
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +620 -71
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/mcp.ts +19 -1
- package/src/services/media.ts +8 -1
- package/src/services/path.ts +34 -1
- package/src/services/post.ts +214 -39
- package/src/services/site-admin.ts +3 -7
- package/src/services/site.ts +83 -31
- package/src/styles/ui.css +7 -1
- package/src/types/app-context.ts +16 -0
- package/src/types/bindings.ts +5 -0
- package/src/types/operations.ts +10 -0
- package/src/ui/dash/settings/GeneralContent.tsx +10 -8
- package/src/client/tiptap/exitable-marks.ts +0 -73
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { loadNodeRuntime } from "../lib/load-node-runtime.js";
|
|
3
|
+
import { openNodeDatabase } from "../lib/node-database.js";
|
|
4
|
+
import { bootstrapCliRuntime } from "../lib/runtime-target.js";
|
|
5
|
+
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(
|
|
8
|
+
"Usage: jant setup --email <address> --password-stdin [--site-name <name>] [--language <tag>] [--time-zone <zone>] [--site-id <id>]",
|
|
9
|
+
);
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log(
|
|
12
|
+
"Set up a self-hosted install without the browser: the owner account, the site, and onboarding.",
|
|
13
|
+
);
|
|
14
|
+
console.log("Does the same as the two /setup screens.");
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log("Options:");
|
|
17
|
+
console.log(" --email The owner's sign-in address");
|
|
18
|
+
console.log(
|
|
19
|
+
" --password-stdin Read the owner's password from standard input",
|
|
20
|
+
);
|
|
21
|
+
console.log(
|
|
22
|
+
" --site-name The site's name (default: the built-in fallback name)",
|
|
23
|
+
);
|
|
24
|
+
console.log(
|
|
25
|
+
" --language Language the site publishes in, as a BCP 47 tag (default: en)",
|
|
26
|
+
);
|
|
27
|
+
console.log(" --time-zone The site's IANA time zone (default: UTC)");
|
|
28
|
+
console.log(
|
|
29
|
+
" --site-id Create the site with this id instead of a random one",
|
|
30
|
+
);
|
|
31
|
+
console.log(
|
|
32
|
+
" --node Force Node runtime even if DATABASE_URL is unset",
|
|
33
|
+
);
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log("Safe to run on every start. An install that has finished setup");
|
|
36
|
+
console.log(
|
|
37
|
+
"is left unchanged, so a password changed since then is not reset.",
|
|
38
|
+
);
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("Example:");
|
|
41
|
+
console.log(
|
|
42
|
+
` printf '%s' "$OWNER_PASSWORD" | jant setup --email owner@example.com --password-stdin`,
|
|
43
|
+
);
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log(
|
|
46
|
+
"Node runtime and SITE_RESOLUTION_MODE=single-site only. `.env.node` next to",
|
|
47
|
+
);
|
|
48
|
+
console.log("your project (or in packages/core/) is auto-loaded.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a secret piped to the command, the way `docker login --password-stdin`
|
|
53
|
+
* does: everything on the stream, minus the one line break `echo` adds.
|
|
54
|
+
*
|
|
55
|
+
* @param stream - Where the secret arrives
|
|
56
|
+
* @returns The secret
|
|
57
|
+
*/
|
|
58
|
+
async function readPasswordFromStdin(stream) {
|
|
59
|
+
if (stream.isTTY) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"--password-stdin reads the password from a pipe. Example: printf '%s' \"$OWNER_PASSWORD\" | jant setup --email <address> --password-stdin",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const chunks = [];
|
|
66
|
+
for await (const chunk of stream) {
|
|
67
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
68
|
+
}
|
|
69
|
+
return Buffer.concat(chunks)
|
|
70
|
+
.toString("utf8")
|
|
71
|
+
.replace(/\r?\n$/, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const OUTCOME_MESSAGES = {
|
|
75
|
+
created: (siteId) => `Set up site ${siteId}.`,
|
|
76
|
+
resumed: (siteId) =>
|
|
77
|
+
`Finished setting up site ${siteId}, which an earlier run had started.`,
|
|
78
|
+
"already-set-up": (siteId) =>
|
|
79
|
+
`Site ${siteId} was already set up. Nothing changed.`,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export async function run(argv, io = { stdin: process.stdin }) {
|
|
83
|
+
const { values } = parseArgs({
|
|
84
|
+
args: argv,
|
|
85
|
+
options: {
|
|
86
|
+
email: { type: "string" },
|
|
87
|
+
help: { type: "boolean", short: "h" },
|
|
88
|
+
language: { type: "string" },
|
|
89
|
+
node: { type: "boolean", default: false },
|
|
90
|
+
"password-stdin": { type: "boolean", default: false },
|
|
91
|
+
"site-id": { type: "string" },
|
|
92
|
+
"site-name": { type: "string" },
|
|
93
|
+
"time-zone": { type: "string" },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (values.help) {
|
|
98
|
+
printHelp();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!values.email) {
|
|
103
|
+
throw new Error("Pass the owner's sign-in address with --email.");
|
|
104
|
+
}
|
|
105
|
+
if (!values["password-stdin"]) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Pass --password-stdin and pipe the owner's password in. The password is never read from an argument, where it would show up in the process list.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { runtime } = bootstrapCliRuntime(values);
|
|
112
|
+
if (runtime !== "node") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"jant setup runs against a Node database. Set DATABASE_URL or DATA_DIR, or pass --node.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const password = await readPasswordFromStdin(io.stdin);
|
|
119
|
+
const { setUpNodeInstance } = await loadNodeRuntime();
|
|
120
|
+
const nodeDatabase = await openNodeDatabase(process.env);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await setUpNodeInstance(nodeDatabase.bindings, {
|
|
124
|
+
email: values.email,
|
|
125
|
+
password,
|
|
126
|
+
siteId: values["site-id"],
|
|
127
|
+
siteName: values["site-name"],
|
|
128
|
+
siteLanguage: values.language,
|
|
129
|
+
timeZone: values["time-zone"],
|
|
130
|
+
});
|
|
131
|
+
console.log(OUTCOME_MESSAGES[result.outcome](result.siteId));
|
|
132
|
+
return result;
|
|
133
|
+
} finally {
|
|
134
|
+
await nodeDatabase.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import { mkdirSync, readdirSync
|
|
2
|
-
import {
|
|
1
|
+
import { createWriteStream, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { mkdir, mkdtemp, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
3
7
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { unzipSync } from "fflate";
|
|
5
8
|
import {
|
|
6
9
|
CLI_API_TOKEN_ENV_VAR,
|
|
7
10
|
getCliApiToken,
|
|
8
11
|
} from "../../lib/cli-api-token.js";
|
|
9
|
-
import {
|
|
12
|
+
import { pullSiteExportDirectory } from "../../lib/site-pull-media.js";
|
|
13
|
+
import { extractZipFile, writeDirectoryToZip } from "../../lib/zip-archive.js";
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Stream the site's export archive to a file. The site streams it too, and a
|
|
17
|
+
* site that bundles its media answers with gigabytes.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} url - Site URL
|
|
20
|
+
* @param {string} token - API token
|
|
21
|
+
* @param {string} destination - File to write
|
|
22
|
+
*/
|
|
23
|
+
async function downloadSiteArchive(url, token, destination) {
|
|
12
24
|
const response = await fetch(`${url.replace(/\/$/, "")}/api/export/hugo`, {
|
|
13
25
|
method: "POST",
|
|
14
26
|
headers: {
|
|
@@ -16,12 +28,16 @@ async function exportRemoteSite(url, token) {
|
|
|
16
28
|
},
|
|
17
29
|
});
|
|
18
30
|
|
|
19
|
-
if (!response.ok) {
|
|
31
|
+
if (!response.ok || !response.body) {
|
|
20
32
|
const text = await response.text();
|
|
21
33
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
37
|
+
await pipeline(
|
|
38
|
+
Readable.fromWeb(response.body),
|
|
39
|
+
createWriteStream(destination),
|
|
40
|
+
);
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
function describeProgressUrl(value) {
|
|
@@ -161,22 +177,8 @@ export async function run(argv) {
|
|
|
161
177
|
process.exit(1);
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const zipBytes = await exportRemoteSite(url, token);
|
|
167
|
-
let zip = zipBytes;
|
|
168
|
-
let pullStats = null;
|
|
169
|
-
|
|
170
|
-
if (pullMedia) {
|
|
171
|
-
console.log("Preparing pull-media export ZIP...");
|
|
172
|
-
const pulled = await pullSiteExportZipBytes(zip, {
|
|
173
|
-
assetLoader: null,
|
|
174
|
-
logger: logPullProgress,
|
|
175
|
-
});
|
|
176
|
-
zip = pulled.zipBytes;
|
|
177
|
-
pullStats = pulled.stats;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
+
// Fail before the download, not after it: pulling a real site's media
|
|
181
|
+
// takes minutes.
|
|
180
182
|
if (outputDirectory) {
|
|
181
183
|
let existingEntries = [];
|
|
182
184
|
try {
|
|
@@ -194,20 +196,55 @@ export async function run(argv) {
|
|
|
194
196
|
);
|
|
195
197
|
process.exit(1);
|
|
196
198
|
}
|
|
199
|
+
}
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
mkdirSync(dirname(fullPath), { recursive: true });
|
|
203
|
-
writeFileSync(fullPath, Buffer.from(bytes));
|
|
204
|
-
}
|
|
205
|
-
console.log(`Exported site from ${url} to ${values.directory}`);
|
|
206
|
-
console.log(`Preview with: cd ${values.directory} && hugo serve`);
|
|
207
|
-
} else {
|
|
201
|
+
console.log(`Exporting site from ${url}...`);
|
|
202
|
+
|
|
203
|
+
// Nothing to add: the site's archive is the export.
|
|
204
|
+
if (!pullMedia && !outputDirectory) {
|
|
208
205
|
console.log(`Writing ${values.output}...`);
|
|
209
|
-
|
|
206
|
+
const partial = `${output}.partial`;
|
|
207
|
+
try {
|
|
208
|
+
await downloadSiteArchive(url, token, partial);
|
|
209
|
+
await rename(partial, output);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
await rm(partial, { force: true });
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
210
214
|
console.log(`Exported site from ${url} to ${values.output}`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Unpacked to disk and media downloaded next to it, so the export never
|
|
219
|
+
// holds more than one media file in memory.
|
|
220
|
+
const tempDir = await mkdtemp(join(tmpdir(), "jant-site-export-"));
|
|
221
|
+
const workDir = outputDirectory ?? join(tempDir, "site");
|
|
222
|
+
let pullStats = null;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const archivePath = join(tempDir, "site.zip");
|
|
226
|
+
await downloadSiteArchive(url, token, archivePath);
|
|
227
|
+
await extractZipFile(archivePath, workDir);
|
|
228
|
+
await rm(archivePath, { force: true });
|
|
229
|
+
|
|
230
|
+
if (pullMedia) {
|
|
231
|
+
console.log("Pulling media...");
|
|
232
|
+
pullStats = await pullSiteExportDirectory(workDir, {
|
|
233
|
+
assetLoader: null,
|
|
234
|
+
logger: logPullProgress,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (outputDirectory) {
|
|
239
|
+
console.log(`Exported site from ${url} to ${values.directory}`);
|
|
240
|
+
console.log(`Preview with: cd ${values.directory} && hugo serve`);
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`Writing ${values.output}...`);
|
|
243
|
+
await writeDirectoryToZip(workDir, output);
|
|
244
|
+
console.log(`Exported site from ${url} to ${values.output}`);
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
211
248
|
}
|
|
212
249
|
|
|
213
250
|
if (pullStats) {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { stat } from "node:fs/promises";
|
|
3
2
|
import { resolve } from "node:path";
|
|
4
3
|
import { parseArgs } from "node:util";
|
|
5
4
|
import {
|
|
6
5
|
pullSiteExportDirectory,
|
|
7
|
-
|
|
6
|
+
pullSiteExportZipFile,
|
|
8
7
|
} from "../../lib/site-pull-media.js";
|
|
9
8
|
|
|
10
9
|
function describeProgressUrl(value) {
|
|
@@ -102,13 +101,10 @@ export async function run(argv) {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
const outputPath = resolve(process.cwd(), values.output || values.path);
|
|
105
|
-
const inputBytes = new Uint8Array(readFileSync(inputPath));
|
|
106
104
|
console.log(`Pulling media in ${values.path}...`);
|
|
107
|
-
const
|
|
105
|
+
const stats = await pullSiteExportZipFile(inputPath, outputPath, {
|
|
108
106
|
logger: logPullProgress,
|
|
109
107
|
});
|
|
110
|
-
console.log(`Writing ${values.output || values.path}...`);
|
|
111
|
-
writeFileSync(outputPath, Buffer.from(zipBytes));
|
|
112
108
|
console.log(
|
|
113
109
|
`Pulled media in ${values.path} -> ${values.output || values.path}`,
|
|
114
110
|
);
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readFile,
|
|
6
|
-
readdir,
|
|
7
|
-
rm,
|
|
8
|
-
writeFile,
|
|
9
|
-
} from "node:fs/promises";
|
|
1
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
10
5
|
import { tmpdir } from "node:os";
|
|
11
|
-
import { dirname, join,
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
7
|
import { parseArgs } from "node:util";
|
|
13
|
-
import {
|
|
14
|
-
import { queryD1 } from "../../../lib/d1-query.js";
|
|
8
|
+
import { writeDirectoryToZip } from "../../../lib/zip-archive.js";
|
|
9
|
+
import { D1_DUMP_PAGE_SIZE, queryD1 } from "../../../lib/d1-query.js";
|
|
15
10
|
import { loadNodeRuntime } from "../../../lib/load-node-runtime.js";
|
|
16
11
|
import { openNodeDatabase } from "../../../lib/node-database.js";
|
|
17
12
|
import {
|
|
@@ -23,6 +18,7 @@ import {
|
|
|
23
18
|
buildSnapshotStorageQuery,
|
|
24
19
|
collectSnapshotObjects,
|
|
25
20
|
getSnapshotSelectSql,
|
|
21
|
+
orderSnapshotPostRows,
|
|
26
22
|
SNAPSHOT_TABLES,
|
|
27
23
|
snapshotObjectPath,
|
|
28
24
|
} from "../../../lib/site-snapshot.js";
|
|
@@ -38,49 +34,6 @@ function isZipPath(filePath) {
|
|
|
38
34
|
return filePath.toLowerCase().endsWith(".zip");
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
async function readStorageBody(body) {
|
|
42
|
-
const reader = body.getReader();
|
|
43
|
-
const chunks = [];
|
|
44
|
-
let totalLength = 0;
|
|
45
|
-
|
|
46
|
-
for (;;) {
|
|
47
|
-
const { done, value } = await reader.read();
|
|
48
|
-
if (done) break;
|
|
49
|
-
chunks.push(value);
|
|
50
|
-
totalLength += value.length;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const bytes = new Uint8Array(totalLength);
|
|
54
|
-
let offset = 0;
|
|
55
|
-
for (const chunk of chunks) {
|
|
56
|
-
bytes.set(chunk, offset);
|
|
57
|
-
offset += chunk.length;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return bytes;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function readDirectoryEntries(rootDir) {
|
|
64
|
-
const entries = {};
|
|
65
|
-
|
|
66
|
-
async function walk(dir) {
|
|
67
|
-
const items = await readdir(dir, { withFileTypes: true });
|
|
68
|
-
for (const item of items) {
|
|
69
|
-
const fullPath = join(dir, item.name);
|
|
70
|
-
if (item.isDirectory()) {
|
|
71
|
-
await walk(fullPath);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const relativePath = relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
76
|
-
entries[relativePath] = new Uint8Array(await readFile(fullPath));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
await walk(rootDir);
|
|
81
|
-
return entries;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
37
|
async function assertWritableOutput(outputPath, force) {
|
|
85
38
|
if (!existsSync(outputPath)) {
|
|
86
39
|
return;
|
|
@@ -123,7 +76,12 @@ async function createNodeExportContext() {
|
|
|
123
76
|
}
|
|
124
77
|
|
|
125
78
|
await mkdir(dirname(filePath), { recursive: true });
|
|
126
|
-
|
|
79
|
+
// Straight to disk: a snapshot holds every media file, and collecting
|
|
80
|
+
// each one in memory first ran a 3 GB export to a 2 GB peak.
|
|
81
|
+
await pipeline(
|
|
82
|
+
Readable.fromWeb(object.body),
|
|
83
|
+
createWriteStream(filePath),
|
|
84
|
+
);
|
|
127
85
|
},
|
|
128
86
|
};
|
|
129
87
|
}
|
|
@@ -295,6 +253,9 @@ export async function run(argv) {
|
|
|
295
253
|
{
|
|
296
254
|
dialect: context.dialect,
|
|
297
255
|
source: runtime,
|
|
256
|
+
// D1 answers through Wrangler's buffered output; read it in pages.
|
|
257
|
+
pageSize: runtime === "node" ? undefined : D1_DUMP_PAGE_SIZE,
|
|
258
|
+
orderRowsByTable: { post: orderSnapshotPostRows },
|
|
298
259
|
tables: SNAPSHOT_TABLES,
|
|
299
260
|
selectSqlByTable: Object.fromEntries(
|
|
300
261
|
SNAPSHOT_TABLES.map((tableName) => [
|
|
@@ -344,10 +305,7 @@ export async function run(argv) {
|
|
|
344
305
|
|
|
345
306
|
if (shouldZip) {
|
|
346
307
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
347
|
-
|
|
348
|
-
level: 6,
|
|
349
|
-
});
|
|
350
|
-
await writeFile(outputPath, zipped);
|
|
308
|
+
await writeDirectoryToZip(scratchDir, outputPath);
|
|
351
309
|
if (process.env.SNAPSHOT_SUPPRESS_SUCCESS_LOG !== "true") {
|
|
352
310
|
console.log(
|
|
353
311
|
`Exported ${getCliRuntimeLabel(runtime)} snapshot to ${values.output}`,
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
mkdtemp,
|
|
5
|
-
readFile,
|
|
6
|
-
rm,
|
|
7
|
-
stat,
|
|
8
|
-
writeFile,
|
|
9
|
-
} from "node:fs/promises";
|
|
1
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
10
4
|
import { tmpdir } from "node:os";
|
|
11
|
-
import {
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
12
6
|
import { parseArgs } from "node:util";
|
|
13
|
-
import {
|
|
7
|
+
import { extractZipFile } from "../../../lib/zip-archive.js";
|
|
14
8
|
import { executeD1, queryD1 } from "../../../lib/d1-query.js";
|
|
15
9
|
import { loadNodeRuntime } from "../../../lib/load-node-runtime.js";
|
|
16
10
|
import { openNodeDatabase } from "../../../lib/node-database.js";
|
|
@@ -18,6 +12,8 @@ import { deleteR2Object, uploadR2Object } from "../../../lib/r2-query.js";
|
|
|
18
12
|
import {
|
|
19
13
|
assertSnapshotDialectMatches,
|
|
20
14
|
assertSnapshotMeta,
|
|
15
|
+
buildMediaProviderSql,
|
|
16
|
+
DEFER_FOREIGN_KEYS_SQL,
|
|
21
17
|
buildReplaceSql,
|
|
22
18
|
buildSnapshotStorageQuery,
|
|
23
19
|
collectSnapshotObjects,
|
|
@@ -58,11 +54,13 @@ async function createNodeImportContext() {
|
|
|
58
54
|
// the current site, which (a) is redundant with the bin-level resolveCliSite
|
|
59
55
|
// call below and (b) prints a generic "/setup first" error when the
|
|
60
56
|
// snapshot's own error path is more informative.
|
|
61
|
-
const { createStorageDriver } =
|
|
57
|
+
const { createStorageDriver, getConfiguredStorageDriver } =
|
|
58
|
+
await loadNodeRuntime();
|
|
62
59
|
const storage = createStorageDriver(nodeDatabase.bindings);
|
|
63
60
|
|
|
64
61
|
return {
|
|
65
62
|
dialect: nodeDatabase.database.dialect,
|
|
63
|
+
storageProvider: getConfiguredStorageDriver(nodeDatabase.bindings),
|
|
66
64
|
async close() {
|
|
67
65
|
await nodeDatabase.close();
|
|
68
66
|
},
|
|
@@ -77,8 +75,7 @@ async function createNodeImportContext() {
|
|
|
77
75
|
throw new Error("Snapshot import requires configured storage.");
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
await storage.put(key, bytes, {
|
|
78
|
+
await storage.put(key, Readable.toWeb(createReadStream(filePath)), {
|
|
82
79
|
contentType: contentType || undefined,
|
|
83
80
|
});
|
|
84
81
|
},
|
|
@@ -96,6 +93,8 @@ function createD1ImportContext(runtime, values) {
|
|
|
96
93
|
|
|
97
94
|
return {
|
|
98
95
|
dialect: "sqlite",
|
|
96
|
+
// Objects go up through the wrangler R2 binding below.
|
|
97
|
+
storageProvider: "r2",
|
|
99
98
|
async close() {},
|
|
100
99
|
async query(sql) {
|
|
101
100
|
return queryD1(sql, runtime, wranglerOptions);
|
|
@@ -141,14 +140,7 @@ async function materializeSnapshotInput(inputPath) {
|
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
const outputDir = await mkdtemp(join(tmpdir(), "jant-site-snapshot-import-"));
|
|
144
|
-
|
|
145
|
-
const files = unzipSync(bytes);
|
|
146
|
-
|
|
147
|
-
for (const [relativePath, data] of Object.entries(files)) {
|
|
148
|
-
const absolutePath = join(outputDir, relativePath);
|
|
149
|
-
await mkdir(dirname(absolutePath), { recursive: true });
|
|
150
|
-
await writeFile(absolutePath, data);
|
|
151
|
-
}
|
|
143
|
+
await extractZipFile(inputPath, outputDir);
|
|
152
144
|
|
|
153
145
|
return {
|
|
154
146
|
cleanup: async () => {
|
|
@@ -402,7 +394,14 @@ export async function run(argv) {
|
|
|
402
394
|
);
|
|
403
395
|
}
|
|
404
396
|
|
|
405
|
-
await context.execute(
|
|
397
|
+
await context.execute(
|
|
398
|
+
[
|
|
399
|
+
...(context.dialect === "postgres" ? [] : [DEFER_FOREIGN_KEYS_SQL]),
|
|
400
|
+
buildReplaceSql(targetSite.id),
|
|
401
|
+
dbSql,
|
|
402
|
+
buildMediaProviderSql(targetSite.id, context.storageProvider),
|
|
403
|
+
].join("\n"),
|
|
404
|
+
);
|
|
406
405
|
|
|
407
406
|
const keysToDelete = [...currentKeys].filter(
|
|
408
407
|
(key) => !snapshotKeys.has(key),
|
package/bin/lib/d1-query.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { WRANGLER_MAX_BUFFER, runLocalWrangler } from "./wrangler-cli.js";
|
|
2
5
|
import { extractWranglerJson } from "./wrangler-json.js";
|
|
3
6
|
|
|
4
7
|
const DEFAULT_RETRY_ATTEMPTS = 4;
|
|
5
8
|
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Byte ceiling for a statement batch sent as one `--command=` argument.
|
|
12
|
+
*
|
|
13
|
+
* Linux caps a single argv entry at 128 KiB (`MAX_ARG_STRLEN`) and fails the
|
|
14
|
+
* spawn with `E2BIG`; macOS has no per-argument cap, so an oversized batch
|
|
15
|
+
* works on a developer's Mac and dies in CI and in the demo rebuild, which is
|
|
16
|
+
* exactly how it first surfaced — a snapshot import whose `db.sql` had grown
|
|
17
|
+
* past the limit. Anything at or above this goes through `--file` instead.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_INLINE_COMMAND_BYTES = 96 * 1024;
|
|
20
|
+
|
|
7
21
|
function getD1Flag(runtime) {
|
|
8
22
|
return runtime === "d1-remote" ? "--remote" : "--local";
|
|
9
23
|
}
|
|
@@ -62,6 +76,46 @@ function sleepSync(ms) {
|
|
|
62
76
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
63
77
|
}
|
|
64
78
|
|
|
79
|
+
const WRANGLER_OUTPUT_EXCERPT_LENGTH = 2000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rows per read when dumping a D1 table: small enough that a page of long
|
|
83
|
+
* posts stays well inside {@link WRANGLER_MAX_BUFFER}.
|
|
84
|
+
*/
|
|
85
|
+
export const D1_DUMP_PAGE_SIZE = 200;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Say why a Wrangler call failed without echoing its whole output: on a
|
|
89
|
+
* failed `d1 execute` that output can be a site's rows, hundreds of KB of it.
|
|
90
|
+
*
|
|
91
|
+
* @param {string[]} args - The Wrangler arguments
|
|
92
|
+
* @param {Error & { code?: string, stdout?: string, stderr?: string }} error
|
|
93
|
+
* @returns {string} A message naming the command and the cause
|
|
94
|
+
* @example
|
|
95
|
+
* describeWranglerFailure(["d1", "execute", "DB"], enobufsError);
|
|
96
|
+
* // "`wrangler d1 execute DB` printed more than 67108864 bytes, …"
|
|
97
|
+
*/
|
|
98
|
+
export function describeWranglerFailure(args, error) {
|
|
99
|
+
const command = `wrangler ${args.slice(0, 3).join(" ")}`;
|
|
100
|
+
if (error?.code === "ENOBUFS") {
|
|
101
|
+
return `\`${command}\` printed more than ${WRANGLER_MAX_BUFFER} bytes, the most the CLI buffers.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stderr = String(error?.stderr ?? "").trim();
|
|
105
|
+
if (stderr) return `\`${command}\` failed: ${stderr}`;
|
|
106
|
+
|
|
107
|
+
const stdout = String(error?.stdout ?? "").trim();
|
|
108
|
+
if (stdout) {
|
|
109
|
+
const excerpt =
|
|
110
|
+
stdout.length > WRANGLER_OUTPUT_EXCERPT_LENGTH
|
|
111
|
+
? `${stdout.slice(0, WRANGLER_OUTPUT_EXCERPT_LENGTH)}… (${stdout.length} characters)`
|
|
112
|
+
: stdout;
|
|
113
|
+
return `\`${command}\` failed: ${excerpt}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `\`${command}\` failed: ${error?.message ?? "unknown error"}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
65
119
|
export function isRetryableWranglerD1Failure(output, error) {
|
|
66
120
|
const combined = `${output ?? ""}\n${error?.message ?? ""}`.toLowerCase();
|
|
67
121
|
return [
|
|
@@ -123,12 +177,43 @@ function runWrangler(args, options = {}) {
|
|
|
123
177
|
throw new Error(`Wrangler error: ${wranglerError}`);
|
|
124
178
|
}
|
|
125
179
|
|
|
126
|
-
throw new Error(
|
|
180
|
+
throw new Error(describeWranglerFailure(args, error), { cause: error });
|
|
127
181
|
}
|
|
128
182
|
}
|
|
129
183
|
}
|
|
130
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Whether a statement batch is too large to travel as one CLI argument.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} sql - SQL batch about to be sent to Wrangler
|
|
189
|
+
* @returns {boolean} true when it has to go through a file instead
|
|
190
|
+
* @example
|
|
191
|
+
* ```js
|
|
192
|
+
* exceedsInlineCommandLimit("SELECT 1;"); // false
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export function exceedsInlineCommandLimit(sql) {
|
|
196
|
+
return Buffer.byteLength(sql, "utf8") >= MAX_INLINE_COMMAND_BYTES;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function withTemporarySqlFile(sql, run) {
|
|
200
|
+
const directory = mkdtempSync(join(tmpdir(), "jant-d1-"));
|
|
201
|
+
try {
|
|
202
|
+
const filePath = join(directory, "batch.sql");
|
|
203
|
+
writeFileSync(filePath, sql);
|
|
204
|
+
return run(filePath);
|
|
205
|
+
} finally {
|
|
206
|
+
rmSync(directory, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
131
210
|
export function executeD1(sql, runtime, options = {}) {
|
|
211
|
+
if (exceedsInlineCommandLimit(sql)) {
|
|
212
|
+
return withTemporarySqlFile(sql, (filePath) =>
|
|
213
|
+
executeD1File(filePath, runtime, options),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
132
217
|
const args = appendWranglerContext(
|
|
133
218
|
[
|
|
134
219
|
"d1",
|
package/bin/lib/hugo-markdown.js
CHANGED
|
@@ -20,6 +20,7 @@ const FRONT_MATTER_KEY_ORDER = [
|
|
|
20
20
|
"type",
|
|
21
21
|
"draft",
|
|
22
22
|
"aliases",
|
|
23
|
+
"feed_id",
|
|
23
24
|
"build",
|
|
24
25
|
|
|
25
26
|
// Post payload
|
|
@@ -27,12 +28,15 @@ const FRONT_MATTER_KEY_ORDER = [
|
|
|
27
28
|
"status",
|
|
28
29
|
"visibility",
|
|
29
30
|
"summary_text",
|
|
31
|
+
"truncated",
|
|
30
32
|
"link_url",
|
|
31
33
|
"source_name",
|
|
32
34
|
"source_url",
|
|
33
35
|
"quote_text",
|
|
34
36
|
"rating",
|
|
35
37
|
"featured_at",
|
|
38
|
+
"featured_post_ids",
|
|
39
|
+
"featured_sort_at",
|
|
36
40
|
"pinned_at",
|
|
37
41
|
|
|
38
42
|
// Bookkeeping / attachments
|