@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
|
@@ -3,13 +3,13 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { executeD1 } from "../../lib/d1-query.js";
|
|
4
4
|
import { openNodeDatabase } from "../../lib/node-database.js";
|
|
5
5
|
import {
|
|
6
|
+
bootstrapCliRuntime,
|
|
6
7
|
getCliRuntimeLabel,
|
|
7
|
-
resolveCliRuntime,
|
|
8
8
|
} from "../../lib/runtime-target.js";
|
|
9
9
|
|
|
10
10
|
function formatUsage() {
|
|
11
11
|
console.log(
|
|
12
|
-
"Usage: jant db execute-file --file <path> [--local | --remote] [--config <file>] [--env <name>] [--database <binding>]",
|
|
12
|
+
"Usage: jant db execute-file --file <path> [--local | --remote | --node] [--config <file>] [--env <name>] [--database <binding>]",
|
|
13
13
|
);
|
|
14
14
|
console.log("");
|
|
15
15
|
console.log(
|
|
@@ -20,6 +20,9 @@ function formatUsage() {
|
|
|
20
20
|
console.log(" --file SQL file to execute");
|
|
21
21
|
console.log(" --local Force local D1 instead of DATABASE_URL");
|
|
22
22
|
console.log(" --remote Run against remote D1");
|
|
23
|
+
console.log(
|
|
24
|
+
" --node Force Node runtime even if DATABASE_URL is unset",
|
|
25
|
+
);
|
|
23
26
|
console.log(
|
|
24
27
|
" --config Wrangler config file (default: wrangler.toml)",
|
|
25
28
|
);
|
|
@@ -28,8 +31,12 @@ function formatUsage() {
|
|
|
28
31
|
console.log(" --persist-to Local D1 state directory override");
|
|
29
32
|
console.log("");
|
|
30
33
|
console.log(
|
|
31
|
-
"
|
|
34
|
+
"`.env.node` next to your project (or in packages/core/) is auto-loaded.",
|
|
35
|
+
);
|
|
36
|
+
console.log(
|
|
37
|
+
"If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
|
|
32
38
|
);
|
|
39
|
+
console.log("this command uses the Node database runtime.");
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
async function loadSqlFile(filePath) {
|
|
@@ -59,6 +66,7 @@ export async function run(argv) {
|
|
|
59
66
|
file: { type: "string" },
|
|
60
67
|
help: { type: "boolean", short: "h" },
|
|
61
68
|
local: { type: "boolean", default: false },
|
|
69
|
+
node: { type: "boolean", default: false },
|
|
62
70
|
"persist-to": { type: "string" },
|
|
63
71
|
remote: { type: "boolean", default: false },
|
|
64
72
|
},
|
|
@@ -73,7 +81,7 @@ export async function run(argv) {
|
|
|
73
81
|
throw new Error("Missing required --file <path> argument.");
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
const runtime =
|
|
84
|
+
const { runtime } = bootstrapCliRuntime(values);
|
|
77
85
|
const sql = await loadSqlFile(values.file);
|
|
78
86
|
|
|
79
87
|
if (runtime === "node") {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { rehearseD1Migrations } from "../../lib/migration-rehearsal.js";
|
|
3
3
|
import {
|
|
4
|
+
bootstrapCliRuntime,
|
|
4
5
|
getCliRuntimeLabel,
|
|
5
|
-
resolveCliRuntime,
|
|
6
6
|
} from "../../lib/runtime-target.js";
|
|
7
7
|
|
|
8
8
|
export async function run(argv) {
|
|
@@ -46,7 +46,7 @@ export async function run(argv) {
|
|
|
46
46
|
throw new Error("Missing required --fixture option.");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const runtime =
|
|
49
|
+
const { runtime } = bootstrapCliRuntime(values);
|
|
50
50
|
if (runtime === "node") {
|
|
51
51
|
throw new Error(
|
|
52
52
|
"Migration rehearsal only supports D1. Pass --local or --remote.",
|
package/bin/commands/export.js
CHANGED
|
@@ -5,8 +5,8 @@ import { queryD1 } from "../lib/d1-query.js";
|
|
|
5
5
|
import { openNodeDatabase } from "../lib/node-database.js";
|
|
6
6
|
import { dumpDatabaseToSql } from "../lib/sql-export.js";
|
|
7
7
|
import {
|
|
8
|
+
bootstrapCliRuntime,
|
|
8
9
|
getCliRuntimeLabel,
|
|
9
|
-
resolveCliRuntime,
|
|
10
10
|
} from "../lib/runtime-target.js";
|
|
11
11
|
|
|
12
12
|
function createD1QueryRunner(runtime) {
|
|
@@ -25,6 +25,7 @@ export async function run(argv) {
|
|
|
25
25
|
database: { type: "string", default: "DB" },
|
|
26
26
|
env: { type: "string" },
|
|
27
27
|
local: { type: "boolean", default: false },
|
|
28
|
+
node: { type: "boolean", default: false },
|
|
28
29
|
remote: { type: "boolean", default: false },
|
|
29
30
|
output: { type: "string", short: "o", default: "jant-export.sql" },
|
|
30
31
|
help: { type: "boolean", short: "h" },
|
|
@@ -34,7 +35,7 @@ export async function run(argv) {
|
|
|
34
35
|
|
|
35
36
|
if (values.help) {
|
|
36
37
|
console.log(
|
|
37
|
-
"Usage: jant db export [--local | --remote] [--output <file>] [--config <file>] [--env <name>] [--database <binding>]",
|
|
38
|
+
"Usage: jant db export [--local | --remote | --node] [--output <file>] [--config <file>] [--env <name>] [--database <binding>]",
|
|
38
39
|
);
|
|
39
40
|
console.log("");
|
|
40
41
|
console.log("Export the current database to a SQL file.");
|
|
@@ -44,6 +45,9 @@ export async function run(argv) {
|
|
|
44
45
|
console.log(
|
|
45
46
|
" --remote Export from remote D1 database (default: local)",
|
|
46
47
|
);
|
|
48
|
+
console.log(
|
|
49
|
+
" --node Force Node runtime even if DATABASE_URL is unset",
|
|
50
|
+
);
|
|
47
51
|
console.log(
|
|
48
52
|
" --output, -o Output file path (default: jant-export.sql)",
|
|
49
53
|
);
|
|
@@ -55,14 +59,18 @@ export async function run(argv) {
|
|
|
55
59
|
console.log(" --persist-to Local D1 state directory override");
|
|
56
60
|
console.log("");
|
|
57
61
|
console.log(
|
|
58
|
-
"
|
|
62
|
+
"`.env.node` next to your project (or in packages/core/) is auto-loaded.",
|
|
63
|
+
);
|
|
64
|
+
console.log(
|
|
65
|
+
"If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
|
|
59
66
|
);
|
|
67
|
+
console.log("this command uses the Node database runtime.");
|
|
60
68
|
console.log("");
|
|
61
69
|
console.log("Compatibility alias: jant export");
|
|
62
70
|
process.exit(0);
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
const runtime =
|
|
73
|
+
const { runtime } = bootstrapCliRuntime(values);
|
|
66
74
|
const output = values.output;
|
|
67
75
|
let sql;
|
|
68
76
|
|
|
@@ -18,8 +18,6 @@ import {
|
|
|
18
18
|
normalizeImportedBody,
|
|
19
19
|
rewriteMediaReferences,
|
|
20
20
|
} from "../lib/site-media-parser.js";
|
|
21
|
-
import { openNodeDatabase } from "../lib/node-database.js";
|
|
22
|
-
import { loadNodeRuntime } from "../lib/load-node-runtime.js";
|
|
23
21
|
import { parseFrontMatter as parseFrontMatterShared } from "../lib/hugo-markdown.js";
|
|
24
22
|
|
|
25
23
|
/**
|
|
@@ -437,6 +435,18 @@ function isAbsoluteUrl(value) {
|
|
|
437
435
|
return typeof value === "string" && /^https?:\/\//i.test(value);
|
|
438
436
|
}
|
|
439
437
|
|
|
438
|
+
/**
|
|
439
|
+
* True when `url` points to a different location than the import target —
|
|
440
|
+
* i.e. has a scheme (`https://`, `data:`) or is protocol-relative (`//cdn…`).
|
|
441
|
+
* False for relative paths (`/media/x`, `./x`, `x`), which always belong to
|
|
442
|
+
* the source site and must be rehosted. Used to filter the body-fallback
|
|
443
|
+
* upload when `--skip-remote-media` is set.
|
|
444
|
+
*/
|
|
445
|
+
function isAbsoluteImportUrl(value) {
|
|
446
|
+
if (typeof value !== "string") return false;
|
|
447
|
+
return /^([a-z][a-z0-9+.\-]*:|\/\/)/i.test(value);
|
|
448
|
+
}
|
|
449
|
+
|
|
440
450
|
/**
|
|
441
451
|
* Resolve a `media:` entry's `src` or `poster` reference to a local disk
|
|
442
452
|
* path when the export bundled the bytes under `static/`. Absolute URLs
|
|
@@ -641,19 +651,7 @@ async function buildImportedAttachments(
|
|
|
641
651
|
target,
|
|
642
652
|
siteConfig,
|
|
643
653
|
sourceRootDir,
|
|
644
|
-
options = {},
|
|
645
654
|
) {
|
|
646
|
-
if (
|
|
647
|
-
sourceRootDir &&
|
|
648
|
-
typeof sourceRootDir === "object" &&
|
|
649
|
-
!Array.isArray(sourceRootDir) &&
|
|
650
|
-
options &&
|
|
651
|
-
Object.keys(options).length === 0
|
|
652
|
-
) {
|
|
653
|
-
options = sourceRootDir;
|
|
654
|
-
sourceRootDir = null;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
655
|
const attachments = [];
|
|
658
656
|
let uploaded = 0;
|
|
659
657
|
|
|
@@ -668,10 +666,6 @@ async function buildImportedAttachments(
|
|
|
668
666
|
continue;
|
|
669
667
|
}
|
|
670
668
|
|
|
671
|
-
if (options.skipUploads) {
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
669
|
const normalized = await normalizeMediaSpec(
|
|
676
670
|
spec,
|
|
677
671
|
siteConfig,
|
|
@@ -1487,223 +1481,6 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1487
1481
|
};
|
|
1488
1482
|
}
|
|
1489
1483
|
|
|
1490
|
-
async function createLocalTarget(env = process.env) {
|
|
1491
|
-
const nodeDatabase = await openNodeDatabase(env);
|
|
1492
|
-
const { createNodeCliRuntime, resolveConfig } = await loadNodeRuntime();
|
|
1493
|
-
const bindings = nodeDatabase.bindings;
|
|
1494
|
-
const runtime = await createNodeCliRuntime(bindings);
|
|
1495
|
-
const allSettings = await runtime.services.settings.getAll();
|
|
1496
|
-
const appConfig = resolveConfig(bindings, allSettings);
|
|
1497
|
-
const summaryConfig = {
|
|
1498
|
-
maxParagraphs: appConfig.summaryMaxParagraphs,
|
|
1499
|
-
maxChars: appConfig.summaryMaxChars,
|
|
1500
|
-
};
|
|
1501
|
-
|
|
1502
|
-
return {
|
|
1503
|
-
async close() {
|
|
1504
|
-
await nodeDatabase.close();
|
|
1505
|
-
},
|
|
1506
|
-
async getSetupStatus() {
|
|
1507
|
-
return runtime.services.settings.isOnboardingComplete();
|
|
1508
|
-
},
|
|
1509
|
-
async updateSettings(updates) {
|
|
1510
|
-
await runtime.services.settings.setMany(updates);
|
|
1511
|
-
return { settings: updates };
|
|
1512
|
-
},
|
|
1513
|
-
async updateImportSettings(updates) {
|
|
1514
|
-
await runtime.services.settings.setMany(updates);
|
|
1515
|
-
return { success: true };
|
|
1516
|
-
},
|
|
1517
|
-
async listNavItems() {
|
|
1518
|
-
return runtime.services.navItems.list();
|
|
1519
|
-
},
|
|
1520
|
-
async createNavItem(data) {
|
|
1521
|
-
return runtime.services.navItems.create(data);
|
|
1522
|
-
},
|
|
1523
|
-
async deleteNavItem(id) {
|
|
1524
|
-
return runtime.services.navItems.delete(id);
|
|
1525
|
-
},
|
|
1526
|
-
async removeSiteAvatar() {
|
|
1527
|
-
return runtime.services.settings.removeAvatar(runtime.storage);
|
|
1528
|
-
},
|
|
1529
|
-
async uploadSiteAvatar(data) {
|
|
1530
|
-
if (!runtime.storage) {
|
|
1531
|
-
throw new Error("Local import requires configured storage.");
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
const avatarAsset = await readImportAsset({
|
|
1535
|
-
sourceUrl: data.avatarUrl,
|
|
1536
|
-
sourceFilePath: data.avatarFilePath,
|
|
1537
|
-
});
|
|
1538
|
-
if (!avatarAsset) {
|
|
1539
|
-
throw new Error(`Failed to read site avatar: ${data.avatarUrl}`);
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
let faviconIco;
|
|
1543
|
-
if (data.faviconUrl || data.faviconFilePath) {
|
|
1544
|
-
const faviconAsset = await readImportAsset({
|
|
1545
|
-
sourceUrl: data.faviconUrl,
|
|
1546
|
-
sourceFilePath: data.faviconFilePath,
|
|
1547
|
-
mimeType: "image/x-icon",
|
|
1548
|
-
originalName: "favicon.ico",
|
|
1549
|
-
});
|
|
1550
|
-
if (faviconAsset) {
|
|
1551
|
-
faviconIco = toArrayBuffer(faviconAsset.bytes);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
let appleTouchIcon;
|
|
1556
|
-
if (data.appleTouchUrl) {
|
|
1557
|
-
const appleTouchAsset = await readImportAsset({
|
|
1558
|
-
sourceUrl: data.appleTouchUrl,
|
|
1559
|
-
sourceFilePath: data.appleTouchFilePath,
|
|
1560
|
-
});
|
|
1561
|
-
if (appleTouchAsset) {
|
|
1562
|
-
appleTouchIcon = toArrayBuffer(appleTouchAsset.bytes);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
await runtime.services.settings.uploadAvatar(
|
|
1567
|
-
{
|
|
1568
|
-
file: createUploadFile(
|
|
1569
|
-
avatarAsset.filename,
|
|
1570
|
-
avatarAsset.contentType,
|
|
1571
|
-
avatarAsset.bytes,
|
|
1572
|
-
),
|
|
1573
|
-
faviconIco,
|
|
1574
|
-
appleTouchIcon,
|
|
1575
|
-
},
|
|
1576
|
-
{
|
|
1577
|
-
media: runtime.services.media,
|
|
1578
|
-
storage: runtime.storage,
|
|
1579
|
-
storageProvider: appConfig.storageDriver,
|
|
1580
|
-
maxFileSizeMB: appConfig.uploadMaxFileSize,
|
|
1581
|
-
},
|
|
1582
|
-
);
|
|
1583
|
-
|
|
1584
|
-
return { success: true };
|
|
1585
|
-
},
|
|
1586
|
-
async syncSiteAvatar(data) {
|
|
1587
|
-
await this.removeSiteAvatar();
|
|
1588
|
-
if (!data) {
|
|
1589
|
-
return { success: true };
|
|
1590
|
-
}
|
|
1591
|
-
return this.uploadSiteAvatar(data);
|
|
1592
|
-
},
|
|
1593
|
-
async listCollections() {
|
|
1594
|
-
return runtime.services.collections.list();
|
|
1595
|
-
},
|
|
1596
|
-
async listCollectionDirectoryItems() {
|
|
1597
|
-
return runtime.services.collections.listDirectoryItems();
|
|
1598
|
-
},
|
|
1599
|
-
async createCollection(data) {
|
|
1600
|
-
return runtime.services.collections.create(data);
|
|
1601
|
-
},
|
|
1602
|
-
async createCollectionDirectoryItem(data) {
|
|
1603
|
-
return runtime.services.collections.createDirectoryItem(data);
|
|
1604
|
-
},
|
|
1605
|
-
async moveCollectionDirectoryItem(id, after, before) {
|
|
1606
|
-
return runtime.services.collections.moveDirectoryItem(id, after, before);
|
|
1607
|
-
},
|
|
1608
|
-
async deleteCollectionDirectoryItem(id) {
|
|
1609
|
-
return runtime.services.collections.deleteDirectoryItem(id);
|
|
1610
|
-
},
|
|
1611
|
-
async createPost(data) {
|
|
1612
|
-
const { attachments, ...postData } = data;
|
|
1613
|
-
return runtime.services.posts.createWithAttachments(
|
|
1614
|
-
postData,
|
|
1615
|
-
attachments,
|
|
1616
|
-
{
|
|
1617
|
-
media: runtime.services.media,
|
|
1618
|
-
storage: runtime.storage,
|
|
1619
|
-
storageDriver: appConfig.storageDriver,
|
|
1620
|
-
maxFileSizeMB: appConfig.uploadMaxFileSize,
|
|
1621
|
-
},
|
|
1622
|
-
summaryConfig,
|
|
1623
|
-
);
|
|
1624
|
-
},
|
|
1625
|
-
async createAlias(path, targetSlug) {
|
|
1626
|
-
const post = await runtime.services.posts.getBySlug(targetSlug);
|
|
1627
|
-
if (!post) {
|
|
1628
|
-
throw new Error(`Post with slug "${targetSlug}" not found`);
|
|
1629
|
-
}
|
|
1630
|
-
return runtime.services.customUrls.create({
|
|
1631
|
-
path,
|
|
1632
|
-
targetType: "post",
|
|
1633
|
-
targetId: post.id,
|
|
1634
|
-
});
|
|
1635
|
-
},
|
|
1636
|
-
async uploadMedia(mediaSpec) {
|
|
1637
|
-
if (!runtime.storage) {
|
|
1638
|
-
throw new Error("Local import requires configured storage.");
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
const asset = await readMediaSpecAsset(mediaSpec);
|
|
1642
|
-
if (!asset) return null;
|
|
1643
|
-
|
|
1644
|
-
const originalName =
|
|
1645
|
-
mediaSpec.originalName ||
|
|
1646
|
-
asset.filename ||
|
|
1647
|
-
getFilenameFromUrl(mediaSpec.src) ||
|
|
1648
|
-
"file";
|
|
1649
|
-
const bytes = asset.bytes;
|
|
1650
|
-
const { id, filename, storageKey } =
|
|
1651
|
-
generateImportedStorageKey(originalName);
|
|
1652
|
-
const mimeType =
|
|
1653
|
-
mediaSpec.mimeType || asset.contentType || guessMimeType(originalName);
|
|
1654
|
-
let posterKey;
|
|
1655
|
-
|
|
1656
|
-
if (mediaSpec.poster) {
|
|
1657
|
-
const posterAsset = await readMediaSpecAsset(mediaSpec, "poster");
|
|
1658
|
-
if (posterAsset) {
|
|
1659
|
-
const posterName = posterAsset.filename || "poster.webp";
|
|
1660
|
-
const posterExt = extname(posterName) || ".webp";
|
|
1661
|
-
posterKey = storageKey.replace(/(\.[^.]+)?$/, `-poster${posterExt}`);
|
|
1662
|
-
await runtime.storage.put(posterKey, posterAsset.bytes, {
|
|
1663
|
-
contentType: posterAsset.contentType || guessMimeType(posterName),
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
await runtime.storage.put(storageKey, bytes, {
|
|
1669
|
-
contentType: mimeType,
|
|
1670
|
-
});
|
|
1671
|
-
|
|
1672
|
-
const createdMedia = await runtime.services.media.create({
|
|
1673
|
-
id,
|
|
1674
|
-
filename,
|
|
1675
|
-
originalName,
|
|
1676
|
-
mimeType,
|
|
1677
|
-
size: mediaSpec.size ?? bytes.byteLength,
|
|
1678
|
-
storageKey,
|
|
1679
|
-
provider: appConfig.storageDriver,
|
|
1680
|
-
width: mediaSpec.width ?? undefined,
|
|
1681
|
-
height: mediaSpec.height ?? undefined,
|
|
1682
|
-
alt: mediaSpec.alt ?? undefined,
|
|
1683
|
-
position: mediaSpec.position ?? undefined,
|
|
1684
|
-
blurhash: mediaSpec.blurhash ?? undefined,
|
|
1685
|
-
waveform: mediaSpec.waveform ?? undefined,
|
|
1686
|
-
posterKey,
|
|
1687
|
-
summary: mediaSpec.summary ?? undefined,
|
|
1688
|
-
chars: mediaSpec.chars ?? undefined,
|
|
1689
|
-
mediaKind: mediaSpec.kind ?? undefined,
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
|
-
return {
|
|
1693
|
-
id: createdMedia.id,
|
|
1694
|
-
url: getMediaPublicUrl(
|
|
1695
|
-
createdMedia.storageKey,
|
|
1696
|
-
createdMedia.provider,
|
|
1697
|
-
appConfig,
|
|
1698
|
-
),
|
|
1699
|
-
};
|
|
1700
|
-
},
|
|
1701
|
-
async checkPostSlugAvailability(slug) {
|
|
1702
|
-
return runtime.services.posts.checkSlugAvailability(slug);
|
|
1703
|
-
},
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
1484
|
/**
|
|
1708
1485
|
* Walk `content/` and classify each `_index.md` / `index.md` bundle by its
|
|
1709
1486
|
* front-matter `type`. Returns ordered root-post bundles (with child reply
|
|
@@ -1976,6 +1753,7 @@ function buildPostPayloadFromBundle(bundle, options) {
|
|
|
1976
1753
|
}
|
|
1977
1754
|
|
|
1978
1755
|
export const __test__ = {
|
|
1756
|
+
isAbsoluteImportUrl,
|
|
1979
1757
|
resolveImportUrl,
|
|
1980
1758
|
readMediaSpecAsset,
|
|
1981
1759
|
normalizeMediaSpec,
|
|
@@ -2001,77 +1779,89 @@ export const __test__ = {
|
|
|
2001
1779
|
buildPostPayloadFromBundle,
|
|
2002
1780
|
};
|
|
2003
1781
|
|
|
1782
|
+
function printImportUsage() {
|
|
1783
|
+
console.log("Usage: jant site import <url> [options]");
|
|
1784
|
+
console.log("");
|
|
1785
|
+
console.log("Import a Hugo export directory or ZIP into a Jant site.");
|
|
1786
|
+
console.log("");
|
|
1787
|
+
console.log("Arguments:");
|
|
1788
|
+
console.log(" <url> Jant site URL (required)");
|
|
1789
|
+
console.log("");
|
|
1790
|
+
console.log("Options:");
|
|
1791
|
+
console.log(
|
|
1792
|
+
" --path Path to export directory or ZIP file (default: .)",
|
|
1793
|
+
);
|
|
1794
|
+
console.log(" --dry-run Parse and validate without making API calls");
|
|
1795
|
+
console.log(
|
|
1796
|
+
" --skip-remote-media Skip uploading absolute-URL images found in body (relative paths and declared media still import)",
|
|
1797
|
+
);
|
|
1798
|
+
console.log(" --token API token (overrides JANT_API_TOKEN)");
|
|
1799
|
+
console.log("");
|
|
1800
|
+
console.log(
|
|
1801
|
+
"Import expects an empty target site and fails on slug or alias conflicts.",
|
|
1802
|
+
);
|
|
1803
|
+
console.log("");
|
|
1804
|
+
console.log("Authentication:");
|
|
1805
|
+
console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
|
|
1806
|
+
console.log(" jant site import https://your-site.example --path ./export");
|
|
1807
|
+
console.log("");
|
|
1808
|
+
console.log("Examples:");
|
|
1809
|
+
console.log(
|
|
1810
|
+
" jant site import https://your-site.example --path ./jant-site",
|
|
1811
|
+
);
|
|
1812
|
+
console.log(
|
|
1813
|
+
" jant site import https://your-site.example --path ./jant-site-export.zip",
|
|
1814
|
+
);
|
|
1815
|
+
console.log("");
|
|
1816
|
+
console.log("Compatibility alias: jant import-site");
|
|
1817
|
+
}
|
|
1818
|
+
|
|
2004
1819
|
export async function run(argv) {
|
|
2005
|
-
const { values } = parseArgs({
|
|
1820
|
+
const { values, positionals } = parseArgs({
|
|
2006
1821
|
args: argv,
|
|
1822
|
+
allowPositionals: true,
|
|
2007
1823
|
options: {
|
|
2008
|
-
url: { type: "string" },
|
|
2009
1824
|
token: { type: "string" },
|
|
2010
1825
|
path: { type: "string", default: "." },
|
|
2011
1826
|
"dry-run": { type: "boolean", default: false },
|
|
2012
|
-
"skip-media": { type: "boolean", default: false },
|
|
1827
|
+
"skip-remote-media": { type: "boolean", default: false },
|
|
2013
1828
|
help: { type: "boolean", short: "h" },
|
|
2014
1829
|
},
|
|
2015
1830
|
});
|
|
2016
1831
|
|
|
2017
1832
|
if (values.help) {
|
|
2018
|
-
|
|
2019
|
-
console.log("");
|
|
2020
|
-
console.log("Import a Hugo export directory or ZIP into a Jant instance.");
|
|
2021
|
-
console.log("");
|
|
2022
|
-
console.log("Modes:");
|
|
2023
|
-
console.log(
|
|
2024
|
-
" Local No --url; imports into the local Node database runtime",
|
|
2025
|
-
);
|
|
2026
|
-
console.log(
|
|
2027
|
-
` Remote --url requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
|
|
2028
|
-
);
|
|
2029
|
-
console.log("");
|
|
2030
|
-
console.log("Options:");
|
|
2031
|
-
console.log(" --url Target remote Jant instance URL");
|
|
2032
|
-
console.log(
|
|
2033
|
-
" --path Path to export directory or ZIP file (default: .)",
|
|
2034
|
-
);
|
|
2035
|
-
console.log(" --dry-run Parse and validate without making API calls");
|
|
2036
|
-
console.log(
|
|
2037
|
-
" --skip-media Skip remote media download/upload (embedded text attachments still import)",
|
|
2038
|
-
);
|
|
2039
|
-
console.log("");
|
|
2040
|
-
console.log(
|
|
2041
|
-
"Import expects an empty target site and fails on slug or alias conflicts.",
|
|
2042
|
-
);
|
|
2043
|
-
console.log("");
|
|
2044
|
-
console.log("Authentication:");
|
|
2045
|
-
console.log(` Set ${CLI_API_TOKEN_ENV_VAR} env var (recommended):`);
|
|
2046
|
-
console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
|
|
2047
|
-
console.log(" jant site import --url https://your-site.com");
|
|
2048
|
-
console.log("");
|
|
2049
|
-
console.log("Examples:");
|
|
2050
|
-
console.log(" jant site import --path ./jant-site");
|
|
2051
|
-
console.log(" jant site import --path ./jant-site-export.zip");
|
|
2052
|
-
console.log("");
|
|
2053
|
-
console.log("Compatibility alias: jant import-site");
|
|
1833
|
+
printImportUsage();
|
|
2054
1834
|
process.exit(0);
|
|
2055
1835
|
}
|
|
2056
1836
|
|
|
1837
|
+
const url = positionals[0];
|
|
1838
|
+
if (!url) {
|
|
1839
|
+
console.error("Error: site URL is required");
|
|
1840
|
+
console.error("");
|
|
1841
|
+
printImportUsage();
|
|
1842
|
+
process.exit(1);
|
|
1843
|
+
}
|
|
1844
|
+
if (positionals.length > 1) {
|
|
1845
|
+
console.error(
|
|
1846
|
+
`Error: unexpected extra arguments: ${positionals.slice(1).join(" ")}`,
|
|
1847
|
+
);
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const dryRun = values["dry-run"];
|
|
2057
1852
|
const token = getCliApiToken(process.env, values.token);
|
|
2058
|
-
if (
|
|
1853
|
+
if (!token && !dryRun) {
|
|
2059
1854
|
console.error(
|
|
2060
|
-
`Error:
|
|
1855
|
+
`Error: site import requires ${CLI_API_TOKEN_ENV_VAR} or --token (unless using --dry-run)`,
|
|
2061
1856
|
);
|
|
2062
1857
|
console.error("");
|
|
2063
1858
|
console.error(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
|
|
2064
1859
|
process.exit(1);
|
|
2065
1860
|
}
|
|
2066
1861
|
|
|
2067
|
-
const apiUrl =
|
|
2068
|
-
const
|
|
2069
|
-
const
|
|
2070
|
-
const target = dryRun
|
|
2071
|
-
? null
|
|
2072
|
-
: values.url
|
|
2073
|
-
? createRemoteTarget(apiUrl, token)
|
|
2074
|
-
: await createLocalTarget(process.env);
|
|
1862
|
+
const apiUrl = url.replace(/\/$/, "");
|
|
1863
|
+
const skipRemoteMedia = values["skip-remote-media"];
|
|
1864
|
+
const target = dryRun ? null : createRemoteTarget(apiUrl, token);
|
|
2075
1865
|
|
|
2076
1866
|
// 1. Read source — directory or ZIP
|
|
2077
1867
|
const inputPath = resolve(process.cwd(), values.path);
|
|
@@ -2121,7 +1911,7 @@ export async function run(argv) {
|
|
|
2121
1911
|
if (target) {
|
|
2122
1912
|
const setupError = await getIncompleteSetupError(
|
|
2123
1913
|
target,
|
|
2124
|
-
|
|
1914
|
+
`Target site at ${apiUrl}`,
|
|
2125
1915
|
);
|
|
2126
1916
|
if (setupError) {
|
|
2127
1917
|
console.error("");
|
|
@@ -2149,7 +1939,7 @@ export async function run(argv) {
|
|
|
2149
1939
|
`[dry-run] Would replace navigation with ${importedNav.items.length} items`,
|
|
2150
1940
|
);
|
|
2151
1941
|
}
|
|
2152
|
-
if (avatarImport
|
|
1942
|
+
if (avatarImport) {
|
|
2153
1943
|
if (avatarImport.mode === "remove") {
|
|
2154
1944
|
console.log("[dry-run] Would remove existing site avatar");
|
|
2155
1945
|
} else {
|
|
@@ -2204,7 +1994,7 @@ export async function run(argv) {
|
|
|
2204
1994
|
}
|
|
2205
1995
|
}
|
|
2206
1996
|
|
|
2207
|
-
if (avatarImport
|
|
1997
|
+
if (avatarImport) {
|
|
2208
1998
|
try {
|
|
2209
1999
|
await target.syncSiteAvatar(
|
|
2210
2000
|
avatarImport.mode === "set" ? avatarImport : null,
|
|
@@ -2349,7 +2139,7 @@ export async function run(argv) {
|
|
|
2349
2139
|
}
|
|
2350
2140
|
|
|
2351
2141
|
const rootResourceIds = [];
|
|
2352
|
-
if (!
|
|
2142
|
+
if (!dryRun && rootResourceSpecs.length > 0) {
|
|
2353
2143
|
const result = await uploadBundleResources(rootResourceSpecs, target);
|
|
2354
2144
|
mediaUploaded += result.uploaded;
|
|
2355
2145
|
if (result.urlMap.size > 0) {
|
|
@@ -2360,8 +2150,13 @@ export async function run(argv) {
|
|
|
2360
2150
|
|
|
2361
2151
|
// Fallback: rewrite any leftover in-body image URLs (covers hand-
|
|
2362
2152
|
// authored Hugo content where the exporter didn't declare resources).
|
|
2363
|
-
|
|
2364
|
-
|
|
2153
|
+
// `--skip-remote-media` filters out absolute URLs here so we only
|
|
2154
|
+
// rehost relative paths (the source site's own files).
|
|
2155
|
+
if (!dryRun) {
|
|
2156
|
+
const fallbackUrls = findImageUrls(rootBody).filter(
|
|
2157
|
+
(url) => !skipRemoteMedia || !isAbsoluteImportUrl(url),
|
|
2158
|
+
);
|
|
2159
|
+
const imageMedia = fallbackUrls.map((src) => ({ src }));
|
|
2365
2160
|
const uploadResult = await uploadMediaList(
|
|
2366
2161
|
imageMedia,
|
|
2367
2162
|
target,
|
|
@@ -2380,7 +2175,6 @@ export async function run(argv) {
|
|
|
2380
2175
|
target,
|
|
2381
2176
|
siteConfig,
|
|
2382
2177
|
sourceRootDir,
|
|
2383
|
-
{ skipUploads: skipMedia },
|
|
2384
2178
|
);
|
|
2385
2179
|
importedAttachments = attachmentResult.attachments;
|
|
2386
2180
|
mediaUploaded += attachmentResult.uploaded;
|
|
@@ -2396,7 +2190,7 @@ export async function run(argv) {
|
|
|
2396
2190
|
// These reference a `.md` artifact that holds the full body; the
|
|
2397
2191
|
// normalizer fetches the bytes (local disk first, then remote URL)
|
|
2398
2192
|
// and decodes them.
|
|
2399
|
-
if (!
|
|
2193
|
+
if (!dryRun) {
|
|
2400
2194
|
for (const textEntry of rootTextAttachmentEntries) {
|
|
2401
2195
|
const textAttachment = await normalizeTextAttachmentSpec(
|
|
2402
2196
|
textEntry,
|
|
@@ -2496,7 +2290,7 @@ export async function run(argv) {
|
|
|
2496
2290
|
}
|
|
2497
2291
|
|
|
2498
2292
|
const replyResourceIds = [];
|
|
2499
|
-
if (
|
|
2293
|
+
if (replyResourceSpecs.length > 0) {
|
|
2500
2294
|
const result = await uploadBundleResources(
|
|
2501
2295
|
replyResourceSpecs,
|
|
2502
2296
|
target,
|
|
@@ -2508,8 +2302,11 @@ export async function run(argv) {
|
|
|
2508
2302
|
replyResourceIds.push(...result.mediaIds);
|
|
2509
2303
|
}
|
|
2510
2304
|
|
|
2511
|
-
|
|
2512
|
-
const
|
|
2305
|
+
{
|
|
2306
|
+
const fallbackUrls = findImageUrls(replyBody).filter(
|
|
2307
|
+
(url) => !skipRemoteMedia || !isAbsoluteImportUrl(url),
|
|
2308
|
+
);
|
|
2309
|
+
const imageMedia = fallbackUrls.map((src) => ({ src }));
|
|
2513
2310
|
const uploadResult = await uploadMediaList(
|
|
2514
2311
|
imageMedia,
|
|
2515
2312
|
target,
|
|
@@ -2527,7 +2324,6 @@ export async function run(argv) {
|
|
|
2527
2324
|
target,
|
|
2528
2325
|
siteConfig,
|
|
2529
2326
|
sourceRootDir,
|
|
2530
|
-
{ skipUploads: skipMedia },
|
|
2531
2327
|
);
|
|
2532
2328
|
replyAttachments = attachmentResult.attachments;
|
|
2533
2329
|
mediaUploaded += attachmentResult.uploaded;
|
|
@@ -2535,15 +2331,13 @@ export async function run(argv) {
|
|
|
2535
2331
|
replyAttachments.push({ type: "media", mediaId });
|
|
2536
2332
|
}
|
|
2537
2333
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
if (textAttachment) replyAttachments.push(textAttachment);
|
|
2546
|
-
}
|
|
2334
|
+
for (const textEntry of replyTextAttachmentEntries) {
|
|
2335
|
+
const textAttachment = await normalizeTextAttachmentSpec(
|
|
2336
|
+
textEntry,
|
|
2337
|
+
siteConfig,
|
|
2338
|
+
sourceRootDir,
|
|
2339
|
+
);
|
|
2340
|
+
if (textAttachment) replyAttachments.push(textAttachment);
|
|
2547
2341
|
}
|
|
2548
2342
|
|
|
2549
2343
|
const replyMemberships = resolveCollectionMemberships(
|