@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
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { C as
|
|
1
|
+
import { At as MAX_PINNED_POSTS, Ft as NAV_ITEM_TYPES, Ht as SORT_ORDERS, Ln as url_exports, Nt as MEDIA_KINDS, Tt as FORMATS, Ut as STATUSES, kt as MAX_MEDIA_ATTACHMENTS, qt as TEXT_ATTACHMENT_CONTENT_FORMATS, v as markdown_exports, z as time_exports } from "./github-sync-Gw4orAHk.js";
|
|
2
|
+
import { C as toNavItemView, D as toSearchResultView, E as toPostViews, S as toMediaView, T as toPostView, b as toArchiveGroups, n as createApp, v as defaultFeedRenderer, w as toNavItemViews, x as toArchiveGroupsWithMedia, y as createMediaContext } from "./app-ZcaI1kPN.js";
|
|
3
3
|
export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as
|
|
1
|
+
import { K as getConfiguredStorageDriver, Q as getEnvString, R as now, U as getConfiguredSingleSiteOrigin, W as getConfiguredSingleSitePathPrefix, ct as getPort, dt as getTelegramWebhookSecret, et as getHostedControlPlaneBaseUrl, ft as shouldTrustProxy, lt as getSiteResolutionMode, s as createExportService, ut as getTelegramBotPool } from "./github-sync-Gw4orAHk.js";
|
|
2
|
+
import { A as getFontThemeCssVariables, F as isAssetPath, M as buildThemeStyle, N as BUILTIN_COLOR_THEMES, O as BUILTIN_FONT_THEMES, P as getPublicAssetBasePath, _ as schema_exports, a as setUpNodeInstance, c as resolveDatabaseDialect, d as setMyCommands, f as setWebhook, g as createNodeDatabase, h as sqliteSchemaBundle, i as createNodeRequestRuntime, j as resolveCjkFontProfile, k as getCjkFontCssVariables, l as resolveConfig, m as pgSchemaBundle, n as createApp, o as createSiteService, p as createStorageDriver, r as createNodeCliRuntime, s as getHostBasedStartupConfigurationIssues, u as getWebhookUrl } from "./app-ZcaI1kPN.js";
|
|
3
3
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import Database from "better-sqlite3";
|
|
@@ -440,7 +440,11 @@ async function createNodeRequestHandler(options) {
|
|
|
440
440
|
const env = options?.env ?? getProcessBindings();
|
|
441
441
|
const hostBasedStartupIssues = getHostBasedStartupConfigurationIssues(env);
|
|
442
442
|
if (hostBasedStartupIssues.length > 0) throw new Error(`Host-based startup configuration is invalid:\n- ${hostBasedStartupIssues.map((issue) => issue.message).join("\n- ")}`);
|
|
443
|
-
const { bindings, close } = await createNodeBindings(env);
|
|
443
|
+
const { bindings: databaseBindings, close } = await createNodeBindings(env);
|
|
444
|
+
const bindings = {
|
|
445
|
+
...databaseBindings,
|
|
446
|
+
NODE_STARTED_AT: now()
|
|
447
|
+
};
|
|
444
448
|
try {
|
|
445
449
|
await assertNodeSiteResolutionReady(bindings);
|
|
446
450
|
const assetRoot = options?.assetRoot === void 0 ? resolveNodeAssetRoot() : options.assetRoot;
|
|
@@ -527,7 +531,7 @@ async function createNodeRequestHandler(options) {
|
|
|
527
531
|
async function start(env = process.env, app) {
|
|
528
532
|
const handler = await createNodeRequestHandler({
|
|
529
533
|
env,
|
|
530
|
-
app: async () => app ?? (await import("./app-
|
|
534
|
+
app: async () => app ?? (await import("./app-ZcaI1kPN.js").then((n) => n.t)).createApp()
|
|
531
535
|
});
|
|
532
536
|
const hostname = resolveHost(env);
|
|
533
537
|
const port = resolvePort(env);
|
|
@@ -572,4 +576,4 @@ async function start(env = process.env, app) {
|
|
|
572
576
|
});
|
|
573
577
|
}
|
|
574
578
|
//#endregion
|
|
575
|
-
export { BUILTIN_COLOR_THEMES, BUILTIN_FONT_THEMES, buildThemeStyle, createApp, createExportService, createNodeBindings, createNodeCliRuntime, createNodeRequestHandler, createNodeRequestRuntime, createStorageDriver, getCjkFontCssVariables, getFontThemeCssVariables, migrate, resolveCjkFontProfile, resolveConfig, start };
|
|
579
|
+
export { BUILTIN_COLOR_THEMES, BUILTIN_FONT_THEMES, buildThemeStyle, createApp, createExportService, createNodeBindings, createNodeCliRuntime, createNodeRequestHandler, createNodeRequestRuntime, createStorageDriver, getCjkFontCssVariables, getConfiguredStorageDriver, getFontThemeCssVariables, migrate, resolveCjkFontProfile, resolveConfig, setUpNodeInstance, start };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jant/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "A modern, open-source microblogging platform built on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -38,10 +38,16 @@
|
|
|
38
38
|
"@lingui/react": "^5.9.3",
|
|
39
39
|
"@tailwindcss/typography": "^0.5.19",
|
|
40
40
|
"@tiptap/core": "^3.27.3",
|
|
41
|
+
"@tiptap/extension-bold": "^3.31.3",
|
|
41
42
|
"@tiptap/extension-code-block": "^3.27.3",
|
|
43
|
+
"@tiptap/extension-hard-break": "^3.31.3",
|
|
42
44
|
"@tiptap/extension-image": "^3.27.3",
|
|
45
|
+
"@tiptap/extension-italic": "^3.31.3",
|
|
43
46
|
"@tiptap/extension-link": "^3.27.3",
|
|
47
|
+
"@tiptap/extension-list": "^3.31.3",
|
|
48
|
+
"@tiptap/extension-paragraph": "^3.31.3",
|
|
44
49
|
"@tiptap/extension-placeholder": "^3.27.3",
|
|
50
|
+
"@tiptap/extension-strike": "^3.31.3",
|
|
45
51
|
"@tiptap/extension-table": "^3.27.3",
|
|
46
52
|
"@tiptap/markdown": "^3.27.3",
|
|
47
53
|
"@tiptap/pm": "^3.27.3",
|
|
@@ -51,6 +57,7 @@
|
|
|
51
57
|
"better-auth": "1.6.27",
|
|
52
58
|
"better-sqlite3": "^12.8.0",
|
|
53
59
|
"blurhash": "^2.0.5",
|
|
60
|
+
"client-zip": "^2.5.1",
|
|
54
61
|
"drizzle-orm": "^0.45.1",
|
|
55
62
|
"emoji-mart": "^5.6.0",
|
|
56
63
|
"fflate": "^0.8.2",
|
|
@@ -73,6 +80,8 @@
|
|
|
73
80
|
"unist-util-visit": "^5.1.0",
|
|
74
81
|
"uqr": "^0.1.3",
|
|
75
82
|
"yaml": "^2.8.2",
|
|
83
|
+
"yauzl": "^3.4.0",
|
|
84
|
+
"yazl": "^3.3.1",
|
|
76
85
|
"zod": "^4.3.6"
|
|
77
86
|
},
|
|
78
87
|
"peerDependencies": {
|
|
@@ -95,13 +104,14 @@
|
|
|
95
104
|
"@types/better-sqlite3": "^7.6.13",
|
|
96
105
|
"@types/node": "^25.5.0",
|
|
97
106
|
"@types/pg": "^8.20.0",
|
|
107
|
+
"@types/yauzl": "^3.4.0",
|
|
108
|
+
"@types/yazl": "^3.3.1",
|
|
98
109
|
"cn-font-split": "^7.4.1",
|
|
99
110
|
"drizzle-kit": "^0.31.10",
|
|
100
111
|
"glob": "^13.0.6",
|
|
101
112
|
"happy-dom": "^20.8.4",
|
|
102
113
|
"sharp": "^0.34.5",
|
|
103
114
|
"tailwindcss": "^4.2.2",
|
|
104
|
-
"tsx": "^4.21.0",
|
|
105
115
|
"typescript": "^6.0.2",
|
|
106
116
|
"unplugin-swc": "^1.5.9",
|
|
107
117
|
"vite": "^8.0.0",
|
|
@@ -136,7 +146,7 @@
|
|
|
136
146
|
"build:client": "vite build --config vite.config.client.ts",
|
|
137
147
|
"build:site": "vite build --config vite.config.site.ts",
|
|
138
148
|
"build:lib": "node -e \"const{rmSync,readdirSync,existsSync}=require('fs');if(existsSync('dist'))readdirSync('dist').filter(f=>f!=='client').forEach(f=>rmSync('dist/'+f,{recursive:true,force:true}))\" && vite build --config vite.config.worker.ts",
|
|
139
|
-
"brand:generate": "
|
|
149
|
+
"brand:generate": "node dev/run-script.mjs dev/scripts/generate-brand-assets.ts",
|
|
140
150
|
"typecheck": "tsc -b --noEmit",
|
|
141
151
|
"db:generate": "drizzle-kit generate",
|
|
142
152
|
"db:generate:pg": "drizzle-kit generate --config drizzle.config.pg.ts",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every TypeScript dev script loads, and loads the way its task runs it.
|
|
3
|
+
*
|
|
4
|
+
* `db-node-rebuild-demo` and `db-node-load-demo` ran their scripts with tsx,
|
|
5
|
+
* which hands Node `src/` untransformed. When `lib/filter-dimensions.ts` began
|
|
6
|
+
* declaring its names with the Lingui macro (539cd1bf), both scripts died on
|
|
7
|
+
* their import lines — and stayed broken for a month, because nothing in CI
|
|
8
|
+
* ever loaded a dev script. The scripts now run through `dev/run-script.mjs`,
|
|
9
|
+
* which compiles `src/` the way the builds do. This file holds both halves:
|
|
10
|
+
* no task goes around the runner, and every script loads through it.
|
|
11
|
+
*
|
|
12
|
+
* Loading is not running. A dev script's work sits behind its default export,
|
|
13
|
+
* so importing one evaluates every module it reaches — where a missing file, an
|
|
14
|
+
* untransformed macro, or an undefined `define` global fails — without
|
|
15
|
+
* resetting a database. That contract is checked here too, before anything is
|
|
16
|
+
* loaded, because a script that broke it would do its work inside this test.
|
|
17
|
+
*
|
|
18
|
+
* What loading cannot see: a named import the module no longer exports. Vite's
|
|
19
|
+
* runner leaves it `undefined` until the script calls it, so a renamed export
|
|
20
|
+
* is a type error, and `tsconfig.dev.json` is what catches it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import {
|
|
25
|
+
mkdtempSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join, resolve } from "node:path";
|
|
33
|
+
import { parse } from "smol-toml";
|
|
34
|
+
import ts from "typescript";
|
|
35
|
+
import { describe, expect, it } from "vitest";
|
|
36
|
+
|
|
37
|
+
const CORE_DIR = resolve(import.meta.dirname, "../..");
|
|
38
|
+
const REPO_ROOT = resolve(CORE_DIR, "../..");
|
|
39
|
+
const RUNNER = "dev/run-script.mjs";
|
|
40
|
+
|
|
41
|
+
/** Every TypeScript dev script, relative to the core package. */
|
|
42
|
+
const SCRIPTS = readdirSync(resolve(CORE_DIR, "dev/scripts"))
|
|
43
|
+
.filter((name) => name.endsWith(".ts"))
|
|
44
|
+
.sort()
|
|
45
|
+
.map((name) => `dev/scripts/${name}`);
|
|
46
|
+
|
|
47
|
+
/** A dev script path as a command names it, with the word before it. */
|
|
48
|
+
const SCRIPT_INVOCATION = /(\S+)\s+(\S*dev\/scripts\/[\w.-]+\.ts)\b/g;
|
|
49
|
+
|
|
50
|
+
interface MiseConfig {
|
|
51
|
+
tasks?: Record<string, { run?: string | string[] }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every shell command the repo defines: mise tasks and core's package scripts.
|
|
56
|
+
*
|
|
57
|
+
* @returns Each command with the name of the task or script that runs it
|
|
58
|
+
*/
|
|
59
|
+
function collectCommands(): { name: string; command: string }[] {
|
|
60
|
+
const mise = parse(
|
|
61
|
+
readFileSync(resolve(REPO_ROOT, "mise.toml"), "utf8"),
|
|
62
|
+
) as MiseConfig;
|
|
63
|
+
const pkg = JSON.parse(
|
|
64
|
+
readFileSync(resolve(CORE_DIR, "package.json"), "utf8"),
|
|
65
|
+
) as { scripts?: Record<string, string> };
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
...Object.entries(mise.tasks ?? {}).flatMap(([name, task]) =>
|
|
69
|
+
[task.run ?? []].flat().map((command) => ({
|
|
70
|
+
name: `mise task ${name}`,
|
|
71
|
+
command,
|
|
72
|
+
})),
|
|
73
|
+
),
|
|
74
|
+
...Object.entries(pkg.scripts ?? {}).map(([name, command]) => ({
|
|
75
|
+
name: `package script ${name}`,
|
|
76
|
+
command,
|
|
77
|
+
})),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Top-level statements in a dev script that do work when it is imported.
|
|
83
|
+
*
|
|
84
|
+
* Declarations are allowed, initializers included — resolving a path at module
|
|
85
|
+
* scope is fine. A statement of its own at the top level is not: that is where
|
|
86
|
+
* `await main();` lives.
|
|
87
|
+
*
|
|
88
|
+
* @param script - Script path relative to the core package
|
|
89
|
+
* @returns `path:line` and source text for each offending statement
|
|
90
|
+
*/
|
|
91
|
+
function findTopLevelWork(script: string): string[] {
|
|
92
|
+
const source = ts.createSourceFile(
|
|
93
|
+
script,
|
|
94
|
+
readFileSync(resolve(CORE_DIR, script), "utf8"),
|
|
95
|
+
ts.ScriptTarget.Latest,
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return source.statements
|
|
100
|
+
.filter(
|
|
101
|
+
(statement) =>
|
|
102
|
+
!ts.isImportDeclaration(statement) &&
|
|
103
|
+
!ts.isExportDeclaration(statement) &&
|
|
104
|
+
!ts.isExportAssignment(statement) &&
|
|
105
|
+
!ts.isFunctionDeclaration(statement) &&
|
|
106
|
+
!ts.isClassDeclaration(statement) &&
|
|
107
|
+
!ts.isVariableStatement(statement) &&
|
|
108
|
+
!ts.isTypeAliasDeclaration(statement) &&
|
|
109
|
+
!ts.isInterfaceDeclaration(statement) &&
|
|
110
|
+
!ts.isEnumDeclaration(statement),
|
|
111
|
+
)
|
|
112
|
+
.map((statement) => {
|
|
113
|
+
const { line } = source.getLineAndCharacterOfPosition(
|
|
114
|
+
statement.getStart(),
|
|
115
|
+
);
|
|
116
|
+
return `${script}:${line + 1}: ${statement.getText()}`;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe("dev scripts", () => {
|
|
121
|
+
it("runs every TypeScript dev script through the runner", () => {
|
|
122
|
+
const invocations = collectCommands().flatMap(({ name, command }) =>
|
|
123
|
+
[...command.matchAll(SCRIPT_INVOCATION)].map(([, runner, script]) => ({
|
|
124
|
+
name,
|
|
125
|
+
runner,
|
|
126
|
+
script,
|
|
127
|
+
})),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(invocations.map(({ script }) => script)).toEqual(
|
|
131
|
+
expect.arrayContaining([
|
|
132
|
+
"dev/scripts/bootstrap-node-dev.ts",
|
|
133
|
+
"dev/scripts/clean-node-dev.ts",
|
|
134
|
+
"dev/scripts/reset-node-dev.ts",
|
|
135
|
+
"dev/scripts/import-node-demo-site-export.ts",
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
138
|
+
expect(
|
|
139
|
+
invocations.filter(({ runner }) => !runner.endsWith(RUNNER)),
|
|
140
|
+
).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("keeps each script's work behind its default export", () => {
|
|
144
|
+
expect(SCRIPTS.flatMap(findTopLevelWork)).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it(
|
|
148
|
+
"loads every script and everything it imports",
|
|
149
|
+
{ timeout: 150_000 },
|
|
150
|
+
() => {
|
|
151
|
+
// Loading evaluates each script. One that works at its top level would
|
|
152
|
+
// do that work here, so it is refused before anything runs.
|
|
153
|
+
expect(SCRIPTS.flatMap(findTopLevelWork)).toEqual([]);
|
|
154
|
+
|
|
155
|
+
// From the repo root, where `brand-export` runs: SWC once resolved its
|
|
156
|
+
// Lingui plugin against the working directory and failed from there.
|
|
157
|
+
const fromRoot = (path: string) => `packages/core/${path}`;
|
|
158
|
+
const result = spawnSync(
|
|
159
|
+
process.execPath,
|
|
160
|
+
[fromRoot(RUNNER), "--check", ...SCRIPTS.map(fromRoot)],
|
|
161
|
+
{ cwd: REPO_ROOT, encoding: "utf8", timeout: 120_000 },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(result.error).toBeUndefined();
|
|
165
|
+
expect(result.status, result.stderr).toBe(0);
|
|
166
|
+
expect(result.stdout.trim().split("\n")).toEqual(
|
|
167
|
+
SCRIPTS.map((script) => `Loaded ${fromRoot(script)}`),
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
it(
|
|
173
|
+
"gives a script the NODE_ENV it was started with",
|
|
174
|
+
{ timeout: 150_000 },
|
|
175
|
+
() => {
|
|
176
|
+
// Vite sets NODE_ENV=development when it starts, and a script's child
|
|
177
|
+
// processes inherit it: `db-node-load-demo`'s build produced Lit's
|
|
178
|
+
// development bundle that way.
|
|
179
|
+
const dir = mkdtempSync(join(tmpdir(), "jant-dev-script-"));
|
|
180
|
+
try {
|
|
181
|
+
const script = join(dir, "print-node-env.ts");
|
|
182
|
+
writeFileSync(
|
|
183
|
+
script,
|
|
184
|
+
"export default function main(): void {\n console.log(String(process.env.NODE_ENV));\n}\n",
|
|
185
|
+
);
|
|
186
|
+
const env = { ...process.env };
|
|
187
|
+
delete env.NODE_ENV;
|
|
188
|
+
|
|
189
|
+
const result = spawnSync(process.execPath, [RUNNER, script], {
|
|
190
|
+
cwd: CORE_DIR,
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
env,
|
|
193
|
+
timeout: 120_000,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.status, result.stderr).toBe(0);
|
|
197
|
+
expect(result.stdout.trim()).toBe("undefined");
|
|
198
|
+
} finally {
|
|
199
|
+
rmSync(dir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A collection on an exported site lists its Threads in Jant's order.
|
|
3
|
+
*
|
|
4
|
+
* The theme's `collection-members` partial restates the order Jant's
|
|
5
|
+
* collection page and feed read from the database: pins in this collection
|
|
6
|
+
* first, then `newest`, `oldest`, or `rating_desc`, which read the whole
|
|
7
|
+
* Thread (its activity, its earliest published post, its highest rating),
|
|
8
|
+
* not only the root. This builds the export with the real Hugo binary and
|
|
9
|
+
* compares every page and feed with what Jant's services return.
|
|
10
|
+
*
|
|
11
|
+
* Skips when `hugo` is not on PATH, like the other Hugo build tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { rm } from "node:fs/promises";
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
16
|
+
import { createTestDatabase, DEFAULT_TEST_SITE_ID } from "./helpers/db.js";
|
|
17
|
+
import { makeSiteConfig } from "./helpers/export-fixtures.js";
|
|
18
|
+
import {
|
|
19
|
+
buildHugoSite,
|
|
20
|
+
hugoAvailable,
|
|
21
|
+
readFeedSlugs,
|
|
22
|
+
readThreadSlugs,
|
|
23
|
+
} from "./helpers/hugo-site.js";
|
|
24
|
+
import { createCollectionService } from "../services/collection.js";
|
|
25
|
+
import { createExportService } from "../services/export.js";
|
|
26
|
+
import { createMediaService } from "../services/media.js";
|
|
27
|
+
import { createPathService } from "../services/path.js";
|
|
28
|
+
import { createPostService } from "../services/post.js";
|
|
29
|
+
import {
|
|
30
|
+
resolveCollectionSortOrder,
|
|
31
|
+
supportsCollectionRatingSort,
|
|
32
|
+
} from "../lib/collection-sort.js";
|
|
33
|
+
import type { Database } from "../db/index.js";
|
|
34
|
+
import type { CreatePost } from "../types/operations.js";
|
|
35
|
+
import type { CollectionSortOrder } from "../types.js";
|
|
36
|
+
|
|
37
|
+
const SITE_URL = "https://example.com";
|
|
38
|
+
const at = (iso: string) => Math.floor(Date.parse(iso) / 1000);
|
|
39
|
+
|
|
40
|
+
/** One collection per order, over the same Threads, plus the rating fallback. */
|
|
41
|
+
const COLLECTIONS: { slug: string; sortOrder: CollectionSortOrder }[] = [
|
|
42
|
+
{ slug: "by-newest", sortOrder: "newest" },
|
|
43
|
+
{ slug: "by-oldest", sortOrder: "oldest" },
|
|
44
|
+
{ slug: "by-rating", sortOrder: "rating_desc" },
|
|
45
|
+
// One rated Thread: too few to rank, so the page falls back to newest.
|
|
46
|
+
{ slug: "few-rated", sortOrder: "rating_desc" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
describe.skipIf(!hugoAvailable)("collection order on an exported site", () => {
|
|
50
|
+
let siteDir: string;
|
|
51
|
+
const expected = new Map<string, string[]>();
|
|
52
|
+
const built = new Map<string, string[]>();
|
|
53
|
+
const expectedFeed = new Map<string, string[]>();
|
|
54
|
+
const builtFeed = new Map<string, string[]>();
|
|
55
|
+
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
const { db: testDb } = createTestDatabase();
|
|
58
|
+
const db = testDb as unknown as Database;
|
|
59
|
+
const siteId = DEFAULT_TEST_SITE_ID;
|
|
60
|
+
const paths = createPathService(db, siteId);
|
|
61
|
+
const posts = createPostService(db, { slugIdLength: 5 }, siteId);
|
|
62
|
+
const collections = createCollectionService(db, siteId, paths);
|
|
63
|
+
const media = createMediaService(db, siteId);
|
|
64
|
+
|
|
65
|
+
const collectionIds = new Map<string, string>();
|
|
66
|
+
for (const { slug, sortOrder } of COLLECTIONS) {
|
|
67
|
+
const created = await collections.create({
|
|
68
|
+
slug,
|
|
69
|
+
title: slug,
|
|
70
|
+
sortOrder,
|
|
71
|
+
});
|
|
72
|
+
collectionIds.set(slug, created.id);
|
|
73
|
+
}
|
|
74
|
+
const inCollections = (slugs: string[], pinnedAt: number | null = null) =>
|
|
75
|
+
slugs.map((slug) => ({
|
|
76
|
+
collectionId: collectionIds.get(slug) as string,
|
|
77
|
+
pinnedAt,
|
|
78
|
+
}));
|
|
79
|
+
const ordered = ["by-newest", "by-oldest", "by-rating"];
|
|
80
|
+
|
|
81
|
+
const slugById = new Map<string, string>();
|
|
82
|
+
async function post(slug: string, data: Omit<CreatePost, "slug">) {
|
|
83
|
+
const created = await posts.create({
|
|
84
|
+
slug,
|
|
85
|
+
bodyMarkdown: `Body of ${slug}.`,
|
|
86
|
+
status: "published",
|
|
87
|
+
...data,
|
|
88
|
+
createdAt: data.createdAt ?? data.publishedAt,
|
|
89
|
+
});
|
|
90
|
+
slugById.set(created.id, slug);
|
|
91
|
+
return created;
|
|
92
|
+
}
|
|
93
|
+
const reply = (
|
|
94
|
+
root: { id: string },
|
|
95
|
+
slug: string,
|
|
96
|
+
data: Omit<CreatePost, "slug" | "replyToId" | "format"> = {},
|
|
97
|
+
) => post(slug, { format: "note", replyToId: root.id, ...data });
|
|
98
|
+
|
|
99
|
+
// A reply moves the Thread's activity to June.
|
|
100
|
+
const replied = await post("a-replied", {
|
|
101
|
+
format: "note",
|
|
102
|
+
publishedAt: at("2025-01-01T00:00:00Z"),
|
|
103
|
+
collectionEntries: inCollections([...ordered, "few-rated"]),
|
|
104
|
+
});
|
|
105
|
+
await reply(replied, "a-reply", {
|
|
106
|
+
publishedAt: at("2025-06-01T00:00:00Z"),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Pinned in every ordered collection, rated on the root.
|
|
110
|
+
await post("b-pinned-rated", {
|
|
111
|
+
format: "note",
|
|
112
|
+
publishedAt: at("2025-02-01T00:00:00Z"),
|
|
113
|
+
rating: 3,
|
|
114
|
+
collectionEntries: [
|
|
115
|
+
...inCollections(ordered, at("2025-07-01T00:00:00Z")),
|
|
116
|
+
...inCollections(["few-rated"]),
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Rated only on a reply: the Thread's rating is its highest.
|
|
121
|
+
const replyRated = await post("c-reply-rated", {
|
|
122
|
+
format: "note",
|
|
123
|
+
publishedAt: at("2025-03-01T00:00:00Z"),
|
|
124
|
+
collectionEntries: inCollections(ordered),
|
|
125
|
+
});
|
|
126
|
+
await reply(replyRated, "c-rated-reply", {
|
|
127
|
+
publishedAt: at("2025-03-02T00:00:00Z"),
|
|
128
|
+
rating: 5,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Two Threads published in the same second: the ID decides.
|
|
132
|
+
await post("d-tied", {
|
|
133
|
+
format: "note",
|
|
134
|
+
publishedAt: at("2025-04-01T12:00:00Z"),
|
|
135
|
+
rating: 4,
|
|
136
|
+
collectionEntries: inCollections(ordered),
|
|
137
|
+
});
|
|
138
|
+
await post("e-tied", {
|
|
139
|
+
format: "note",
|
|
140
|
+
publishedAt: at("2025-04-01T12:00:00Z"),
|
|
141
|
+
collectionEntries: inCollections([...ordered, "few-rated"]),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// A reply dated before every other post: `oldest` reads the Thread's
|
|
145
|
+
// earliest published post, so this Thread comes first though its root
|
|
146
|
+
// is one of the later ones.
|
|
147
|
+
const early = await post("f-early-reply", {
|
|
148
|
+
format: "note",
|
|
149
|
+
publishedAt: at("2025-03-15T00:00:00Z"),
|
|
150
|
+
collectionEntries: inCollections(ordered),
|
|
151
|
+
});
|
|
152
|
+
await reply(early, "f-reply-before-root", {
|
|
153
|
+
publishedAt: at("2024-11-01T00:00:00Z"),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Pinned more recently than b, with a quiet reply that doesn't move the
|
|
157
|
+
// Thread's activity.
|
|
158
|
+
const quiet = await post("g-pinned-quiet", {
|
|
159
|
+
format: "note",
|
|
160
|
+
publishedAt: at("2025-05-01T00:00:00Z"),
|
|
161
|
+
collectionEntries: inCollections(ordered, at("2025-08-01T00:00:00Z")),
|
|
162
|
+
});
|
|
163
|
+
await reply(quiet, "g-quiet-reply", {
|
|
164
|
+
publishedAt: at("2025-09-01T00:00:00Z"),
|
|
165
|
+
quietReply: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Neither is on the page.
|
|
169
|
+
await post("h-private", {
|
|
170
|
+
format: "note",
|
|
171
|
+
visibility: "private",
|
|
172
|
+
publishedAt: at("2025-01-15T00:00:00Z"),
|
|
173
|
+
collectionEntries: inCollections(ordered),
|
|
174
|
+
});
|
|
175
|
+
await post("i-draft", {
|
|
176
|
+
format: "note",
|
|
177
|
+
status: "draft",
|
|
178
|
+
collectionEntries: inCollections(ordered),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const reader = { status: "published" as const, excludePrivate: true };
|
|
182
|
+
for (const { slug, sortOrder } of COLLECTIONS) {
|
|
183
|
+
const ids = [collectionIds.get(slug) as string];
|
|
184
|
+
// What the page shows, as the collection page route decides it.
|
|
185
|
+
const rated = await posts.countCollectionThreadRootsUpToForCollections(
|
|
186
|
+
ids,
|
|
187
|
+
{ ...reader, hasRating: true },
|
|
188
|
+
2,
|
|
189
|
+
);
|
|
190
|
+
const pageOrder = resolveCollectionSortOrder(
|
|
191
|
+
undefined,
|
|
192
|
+
sortOrder,
|
|
193
|
+
supportsCollectionRatingSort(rated),
|
|
194
|
+
);
|
|
195
|
+
const rootIds = await posts.listCollectionThreadRootIdsForCollections(
|
|
196
|
+
ids,
|
|
197
|
+
{ ...reader, sortOrder: pageOrder },
|
|
198
|
+
);
|
|
199
|
+
expected.set(
|
|
200
|
+
slug,
|
|
201
|
+
rootIds.map((id) => slugById.get(id) as string),
|
|
202
|
+
);
|
|
203
|
+
// What the feed carries, as the collection feed route asks for it.
|
|
204
|
+
const entries = await posts.listCollectionFeedEntriesForCollections(ids, {
|
|
205
|
+
...reader,
|
|
206
|
+
ignoreCollectionPinnedSort: true,
|
|
207
|
+
limit: 50,
|
|
208
|
+
});
|
|
209
|
+
expectedFeed.set(
|
|
210
|
+
slug,
|
|
211
|
+
entries.map((entry) => slugById.get(entry.post.id) as string),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A small page size, so the pages are read across pagination.
|
|
216
|
+
siteDir = await buildHugoSite(
|
|
217
|
+
await createExportService(
|
|
218
|
+
{ posts, paths, collections, media },
|
|
219
|
+
makeSiteConfig({ siteUrl: SITE_URL, pageSize: 3 }),
|
|
220
|
+
{ storage: null, bundleMedia: false },
|
|
221
|
+
).generateHugoFiles(),
|
|
222
|
+
);
|
|
223
|
+
for (const { slug } of COLLECTIONS) {
|
|
224
|
+
built.set(slug, await readThreadSlugs(siteDir, slug));
|
|
225
|
+
builtFeed.set(slug, await readFeedSlugs(siteDir, slug, SITE_URL));
|
|
226
|
+
}
|
|
227
|
+
}, 60_000);
|
|
228
|
+
|
|
229
|
+
afterAll(async () => {
|
|
230
|
+
if (siteDir) await rm(siteDir, { recursive: true, force: true });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it.each(COLLECTIONS.map(({ slug }) => slug))(
|
|
234
|
+
"%s lists its Threads in Jant's order",
|
|
235
|
+
(slug) => {
|
|
236
|
+
expect(built.get(slug)).toEqual(expected.get(slug));
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
it.each(COLLECTIONS.map(({ slug }) => slug))(
|
|
241
|
+
"%s feed carries Jant's feed order",
|
|
242
|
+
(slug) => {
|
|
243
|
+
expect(builtFeed.get(slug)).toEqual(expectedFeed.get(slug));
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Equal lists prove nothing if the fixture can't tell the orders apart.
|
|
248
|
+
it("runs on a fixture where each rule changes the order", () => {
|
|
249
|
+
// Pins first, most recent first.
|
|
250
|
+
expect(expected.get("by-newest")?.slice(0, 2)).toEqual([
|
|
251
|
+
"g-pinned-quiet",
|
|
252
|
+
"b-pinned-rated",
|
|
253
|
+
]);
|
|
254
|
+
// The reply dated before every other post puts its Thread first.
|
|
255
|
+
expect(expected.get("by-oldest")?.slice(2, 4)).toEqual([
|
|
256
|
+
"f-early-reply",
|
|
257
|
+
"a-replied",
|
|
258
|
+
]);
|
|
259
|
+
// The rating on a reply ranks its Thread above a root rated lower.
|
|
260
|
+
expect(expected.get("by-rating")?.slice(2, 4)).toEqual([
|
|
261
|
+
"c-reply-rated",
|
|
262
|
+
"d-tied",
|
|
263
|
+
]);
|
|
264
|
+
// One rated Thread: the page falls back to newest.
|
|
265
|
+
expect(expected.get("few-rated")).toEqual([
|
|
266
|
+
"a-replied",
|
|
267
|
+
"e-tied",
|
|
268
|
+
"b-pinned-rated",
|
|
269
|
+
]);
|
|
270
|
+
// The feed ignores pins, and a quiet reply doesn't move its Thread up.
|
|
271
|
+
expect(expectedFeed.get("by-oldest")?.slice(0, 2)).toEqual([
|
|
272
|
+
"a-replied",
|
|
273
|
+
"g-pinned-quiet",
|
|
274
|
+
]);
|
|
275
|
+
});
|
|
276
|
+
});
|