@meith/web 0.21.1 → 0.22.0
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/app/(board)/notifications/preferences/page.tsx +15 -1
- package/app/(board)/unsubscribe/page.tsx +5 -3
- package/app/admin/api/marketplace/screenshot/route.ts +0 -8
- package/app/admin/users/mail/page.tsx +1 -1
- package/app/layout.tsx +1 -3
- package/app/manifest.ts +1 -3
- package/bin/forum-web.mjs +1 -182
- package/bin/forum-web.test.ts +0 -22
- package/next.config.mjs +14 -29
- package/package.json +47 -47
- package/proxy.ts +0 -14
- package/src/components/account/notification-forms.tsx +45 -0
- package/src/components/admin/admin-nav.tsx +0 -4
- package/src/components/admin/marketplace-listing.tsx +0 -9
- package/src/components/auth/passkey-client.ts +0 -5
- package/src/components/auth/register-form.tsx +18 -0
- package/src/components/content/composer-ids.ts +0 -1
- package/src/components/content/markdown-editor.tsx +0 -7
- package/src/components/install/install-form.tsx +3 -1
- package/src/components/shell/demo-banner.tsx +0 -8
- package/src/config.ts +0 -11
- package/src/server/admin-actions.ts +0 -5
- package/src/server/antispam.ts +27 -14
- package/src/server/auth-actions.ts +12 -1
- package/src/server/auth-config.ts +0 -11
- package/src/server/auth-events.ts +0 -5
- package/src/server/brand-assets.ts +2 -6
- package/src/server/container.ts +0 -8
- package/src/server/cookies.ts +0 -24
- package/src/server/demo-uploads.ts +0 -11
- package/src/server/demo.ts +0 -21
- package/src/server/federation-actions.ts +0 -6
- package/src/server/federation.ts +0 -7
- package/src/server/feed-routes.ts +0 -5
- package/src/server/i18n.ts +1 -3
- package/src/server/install.ts +13 -2
- package/src/server/location-header.ts +0 -30
- package/src/server/mail-send.ts +0 -6
- package/src/server/marketplace-actions.ts +0 -6
- package/src/server/marketplace-admin.ts +0 -33
- package/src/server/navigation.ts +0 -5
- package/src/server/notification-actions.ts +25 -0
- package/src/server/onboarding.ts +0 -5
- package/src/server/plugin-admin-actions.ts +0 -11
- package/src/server/plugin-admin.ts +0 -3
- package/src/server/plugin-host.ts +15 -6
- package/src/server/plugin-pages.ts +3 -68
- package/src/server/plugin-routes.ts +1 -1
- package/src/server/plugin-runtime.ts +78 -0
- package/src/server/presence.ts +3 -34
- package/src/server/relations.ts +1 -3
- package/src/server/reply-core.ts +1 -0
- package/src/server/request-fingerprint.ts +0 -1
- package/src/server/search-page.ts +0 -4
- package/src/server/session-actions.ts +0 -1
- package/src/server/settings.ts +0 -11
- package/src/server/subscription-actions.ts +12 -4
- package/src/server/syndication.ts +0 -4
- package/src/server/theme-admin.ts +2 -6
- package/src/server/thread-core.ts +6 -1
- package/src/server/two-factor-actions.ts +0 -5
- package/src/server/two-factor.ts +0 -6
- package/src/server/upgrade-notice.ts +1 -1
- package/src/view/account-copy.ts +5 -0
- package/src/view/auth-copy.ts +2 -0
- package/src/view/editor-toolbar.ts +0 -1
- package/src/view/install-copy.ts +3 -0
- package/src/view/navigation.ts +0 -4
- package/src/view/notification-menu.ts +0 -2
- package/src/view/notifications.ts +1 -0
- package/src/view/pager.ts +0 -6
- package/src/view/panel-nav.ts +0 -1
- package/src/view/security-activity.ts +0 -5
- package/src/view/subscriptions.ts +1 -0
- package/src/view/two-factor.ts +0 -5
- package/src/view/usercp.ts +1 -3
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AnnouncementsOptInForm,
|
|
6
|
+
NotificationPreferencesForm,
|
|
7
|
+
} from '@/components/account/notification-forms'
|
|
5
8
|
import { PushDeviceForm } from '@/components/account/push-device-form'
|
|
6
9
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
7
10
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
11
|
+
import { getContainer } from '@/server/container'
|
|
8
12
|
import { getActor } from '@/server/context'
|
|
9
13
|
import { getTranslator, tr } from '@/server/i18n'
|
|
10
14
|
import { audiencesForActor } from '@/server/notification-audience'
|
|
@@ -34,6 +38,9 @@ export default async function NotificationPreferencesPage({
|
|
|
34
38
|
await Promise.all(audiences.map((audience) => service.preferences(userId, audience)))
|
|
35
39
|
).flat()
|
|
36
40
|
|
|
41
|
+
const { memberSettings } = getContainer()
|
|
42
|
+
const settings = memberSettings === null ? null : await memberSettings.read(userId)
|
|
43
|
+
|
|
37
44
|
const push = await pushAvailability()
|
|
38
45
|
const view = buildPreferencesView(rows, await getTranslator())
|
|
39
46
|
const notice = notificationNotice(query, await getTranslator())
|
|
@@ -60,6 +67,13 @@ export default async function NotificationPreferencesPage({
|
|
|
60
67
|
push={push.enabled}
|
|
61
68
|
copy={notificationFormsCopy(await getTranslator())}
|
|
62
69
|
/>
|
|
70
|
+
|
|
71
|
+
{settings !== null && (
|
|
72
|
+
<AnnouncementsOptInForm
|
|
73
|
+
optedIn={settings.massMailOptInAt !== null}
|
|
74
|
+
copy={notificationFormsCopy(await getTranslator())}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
63
77
|
</PanelPage>
|
|
64
78
|
)
|
|
65
79
|
}
|
|
@@ -60,9 +60,11 @@ export default async function UnsubscribePage({
|
|
|
60
60
|
<UnsubscribeConfirmForm
|
|
61
61
|
token={token}
|
|
62
62
|
description={
|
|
63
|
-
claim.scope === '
|
|
64
|
-
? await tr('page.unsubscribe.
|
|
65
|
-
:
|
|
63
|
+
claim.scope === 'mass-mail'
|
|
64
|
+
? await tr('page.unsubscribe.massMailScope')
|
|
65
|
+
: claim.scope === 'email'
|
|
66
|
+
? await tr('page.unsubscribe.emailScope')
|
|
67
|
+
: await tr('page.unsubscribe.targetScope')
|
|
66
68
|
}
|
|
67
69
|
copy={followFormCopy(translator)}
|
|
68
70
|
/>
|
|
@@ -17,14 +17,6 @@ function fail(status: number, message: string): Response {
|
|
|
17
17
|
})
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Streams one marketplace screenshot to the admin panel. The board fetches
|
|
22
|
-
* it, not the browser — this is the "proxied" half of "screenshots proxied
|
|
23
|
-
* or linked from the feed's own host only" (see docs/marketplace.md and the
|
|
24
|
-
* MEI-80 issue). `key` and `index` are looked up against the currently
|
|
25
|
-
* cached feed rather than trusted as a URL themselves, so nothing this route
|
|
26
|
-
* fetches is ever chosen by the caller.
|
|
27
|
-
*/
|
|
28
20
|
export async function GET(request: NextRequest): Promise<Response> {
|
|
29
21
|
try {
|
|
30
22
|
await requireAdmin()
|
|
@@ -58,7 +58,7 @@ export default async function AdminMassMailPage() {
|
|
|
58
58
|
<p>{await tr('page.before-press-it')}</p>
|
|
59
59
|
<ul className="flex list-disc flex-col gap-1 pl-4">
|
|
60
60
|
<li>{translator.t('adminUsers.massMailQueued')}</li>
|
|
61
|
-
<li>{translator.t('adminUsers.
|
|
61
|
+
<li>{translator.t('adminUsers.massMailOptIn')}</li>
|
|
62
62
|
<li>{translator.t('adminUsers.massMailContinues')}</li>
|
|
63
63
|
<li>{await tr('page.email-cannot-be-unsent')}</li>
|
|
64
64
|
</ul>
|
package/app/layout.tsx
CHANGED
|
@@ -41,9 +41,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
41
41
|
name = settings.get('board.name').trim() || BOARD_TITLE
|
|
42
42
|
description = settings.get('board.description').trim() || undefined
|
|
43
43
|
origin = resolveBoardUrl({ environment: env, settings }).url || origin
|
|
44
|
-
} catch {
|
|
45
|
-
/* ignore */
|
|
46
|
-
}
|
|
44
|
+
} catch {}
|
|
47
45
|
|
|
48
46
|
return {
|
|
49
47
|
title: {
|
package/app/manifest.ts
CHANGED
|
@@ -16,9 +16,7 @@ export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
|
|
16
16
|
const settings = await getSettings()
|
|
17
17
|
name = settings.get('board.name').trim() || BOARD_TITLE
|
|
18
18
|
description = settings.get('board.description').trim()
|
|
19
|
-
} catch {
|
|
20
|
-
/* ignore */
|
|
21
|
-
}
|
|
19
|
+
} catch {}
|
|
22
20
|
|
|
23
21
|
const { browserThemeColor } = await getThemeRuntimeStyle()
|
|
24
22
|
|
package/bin/forum-web.mjs
CHANGED
|
@@ -1,81 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* `forum-web` — the bin that makes `@meith/web` runnable as a plain
|
|
4
|
-
* dependency of an external board workspace (see docs/development.md,
|
|
5
|
-
* "Consuming the board from a workspace").
|
|
6
|
-
*
|
|
7
|
-
* A Next.js app is not consumable as a bare dependency: `next dev|build|start`
|
|
8
|
-
* need to run with the app's own directory as the project root, and the
|
|
9
|
-
* board-config seam (`@board/config` / `@board/plugins`, see
|
|
10
|
-
* docs/architecture.md) is a pair of tsconfig path aliases that inside this
|
|
11
|
-
* monorepo point at `apps/community`'s own files. Neither survives npm
|
|
12
|
-
* installing this package into somebody else's workspace unchanged.
|
|
13
|
-
*
|
|
14
|
-
* So on every invocation this bin:
|
|
15
|
-
*
|
|
16
|
-
* 1. Copies this package's own Next app sources into `.meith/app/` inside
|
|
17
|
-
* the invoking workspace (the current working directory) — gitignored,
|
|
18
|
-
* regenerated every time, never a merge target.
|
|
19
|
-
* 2. Writes `.meith/app/tsconfig.json`, a fresh tsconfig whose `paths`
|
|
20
|
-
* point `@board/config` and `@board/plugins` at *this workspace's own*
|
|
21
|
-
* `community.config.ts` / `community.plugins.ts` — the same seam, wired
|
|
22
|
-
* to a different pair of files. tsconfig `paths` are a compiler/bundler
|
|
23
|
-
* alias, not a package boundary, so nothing stops one from naming a
|
|
24
|
-
* path two directories up; that is the whole trick.
|
|
25
|
-
* 3. Runs `next dev|build|start` with `.meith/app/` as the project root.
|
|
26
|
-
*
|
|
27
|
-
* `.meith/app/` sits exactly two directories below the workspace root
|
|
28
|
-
* (`<root>/.meith/app`) on purpose: `next.config.mjs` (copied verbatim, see
|
|
29
|
-
* below) computes its own workspace root as two directories up from itself,
|
|
30
|
-
* so materializing at that exact depth keeps that computation correct
|
|
31
|
-
* without touching the file. This bin now also passes that root explicitly,
|
|
32
|
-
* as `FORUM_WORKSPACE_ROOT`, so the depth is what keeps the *copied file's*
|
|
33
|
-
* own default honest rather than what the build depends on.
|
|
34
|
-
*
|
|
35
|
-
* `--at-root` materializes into the workspace root itself instead
|
|
36
|
-
* (`<root>`, depth zero), which is the one thing Vercel's Next.js preset
|
|
37
|
-
* needs and `.meith/app` cannot give it: the build artefact at
|
|
38
|
-
* `<root>/.next`, where that builder looks and where, for a Next.js project,
|
|
39
|
-
* it cannot be told to look elsewhere. Nothing about the seam changes — the
|
|
40
|
-
* generated tsconfig's `paths` name `./community.config.ts` instead of
|
|
41
|
-
* `../../community.config.ts`, and every other path in here is computed from
|
|
42
|
-
* `appDir` rather than assumed. Because that mode writes framework-owned
|
|
43
|
-
* names (`app/`, `src/`, `next.config.mjs`, ...) into a directory the board
|
|
44
|
-
* also keeps its own files in, ownership there is decided per *file* — the
|
|
45
|
-
* files it wrote last time, recorded in `.meith/materialized.json`, plus any
|
|
46
|
-
* whose contents are already byte for byte what it would write. Everything
|
|
47
|
-
* else is the board's: never removed, never overwritten, and a collision
|
|
48
|
-
* stops the build naming every file involved. A board can therefore keep
|
|
49
|
-
* `public/ads.txt` beside the shipped `public/sw.js`.
|
|
50
|
-
*
|
|
51
|
-
* `GENERATED_ENTRIES` are the exception, and have to be: `tsconfig.json` and
|
|
52
|
-
* `next-env.d.ts` are written from scratch rather than copied, so there is no
|
|
53
|
-
* shipped file to compare a board's own against and nothing to tell one apart
|
|
54
|
-
* from a stale one this bin wrote. Both are replaced without asking, so a
|
|
55
|
-
* board cannot keep its own compiler options at the root of an `--at-root`
|
|
56
|
-
* workspace. See docs/development.md, "Consuming the board from a workspace".
|
|
57
|
-
*
|
|
58
|
-
* This assumes a *hoisted* `node_modules` (npm, yarn classic, or pnpm with
|
|
59
|
-
* `node-linker=hoisted`) — see docs/development.md for why.
|
|
60
|
-
*
|
|
61
|
-
* `boards/stock` (docker/Dockerfile) is this bin's one *in-repo* consumer,
|
|
62
|
-
* and it has neither a hoisted `node_modules` nor a two-directories-up
|
|
63
|
-
* workspace root — it is a workspace member of this monorepo's own
|
|
64
|
-
* (non-hoisted) pnpm install, nested two directories deeper again. Two
|
|
65
|
-
* environment variables, set only by that workspace's own `build`/`dev`
|
|
66
|
-
* scripts, cover the difference without changing anything for a real
|
|
67
|
-
* external board, which sets neither itself: `FORUM_WORKSPACE_ROOT`
|
|
68
|
-
* (apps/community/next.config.mjs) points tracing at this repository's real
|
|
69
|
-
* root, and `FORUM_ALIASES_FROM` (`monorepoAliases()` below) carries this
|
|
70
|
-
* repository's own `@meith/*` tsconfig aliases into the generated tsconfig,
|
|
71
|
-
* since packages here resolve each other through those aliases rather than
|
|
72
|
-
* through real `dependencies` entries a hoisted `node_modules` would need.
|
|
73
|
-
* `FORUM_WORKSPACE_ROOT` is always exported onward from here, defaulting to
|
|
74
|
-
* the invoking workspace's own root when that workspace did not set it, so
|
|
75
|
-
* that everything downstream — the copied `next.config.mjs`, and the
|
|
76
|
-
* `@source` rebase below — reads one answer rather than each re-deriving it
|
|
77
|
-
* from where it happens to sit.
|
|
78
|
-
*/
|
|
79
2
|
import { spawn } from 'node:child_process'
|
|
80
3
|
import {
|
|
81
4
|
cpSync,
|
|
@@ -105,12 +28,6 @@ for (const name of ['FORUM_WORKSPACE_ROOT', 'FORUM_ALIASES_FROM']) {
|
|
|
105
28
|
}
|
|
106
29
|
if (!process.env.FORUM_WORKSPACE_ROOT) process.env.FORUM_WORKSPACE_ROOT = workspaceRoot
|
|
107
30
|
|
|
108
|
-
/**
|
|
109
|
-
* Files this package ships (see its `files` allowlist) that belong inside the
|
|
110
|
-
* materialized app. Board files (community.config.ts, board.plugins.json,
|
|
111
|
-
* community.plugins.ts) are never copied — they are read in place, from the
|
|
112
|
-
* workspace, through the generated tsconfig instead.
|
|
113
|
-
*/
|
|
114
31
|
const APP_ENTRIES = [
|
|
115
32
|
'app',
|
|
116
33
|
'src',
|
|
@@ -141,15 +58,6 @@ function walkFiles(dir, prefix, into) {
|
|
|
141
58
|
return into
|
|
142
59
|
}
|
|
143
60
|
|
|
144
|
-
/**
|
|
145
|
-
* Every file `--at-root` is about to write into the workspace root, as posix
|
|
146
|
-
* relative paths: the shipped entries expanded file by file, plus the two
|
|
147
|
-
* this bin generates. Ownership is decided per file rather than per
|
|
148
|
-
* top-level name so that a board can keep its own files inside a directory
|
|
149
|
-
* the framework also writes into — `public/ads.txt` beside the shipped
|
|
150
|
-
* `public/sw.js`. See docs/development.md, "Consuming the board from a
|
|
151
|
-
* workspace".
|
|
152
|
-
*/
|
|
153
61
|
function intendedRootFiles() {
|
|
154
62
|
const files = []
|
|
155
63
|
for (const entry of APP_ENTRIES) {
|
|
@@ -174,13 +82,6 @@ function absoluteRootPath(rel) {
|
|
|
174
82
|
return join(workspaceRoot, ...rel.split('/'))
|
|
175
83
|
}
|
|
176
84
|
|
|
177
|
-
/**
|
|
178
|
-
* What this bin would leave at `rel`, which is the shipped file's own bytes
|
|
179
|
-
* for everything except `globals.css` — that one is rewritten in place after
|
|
180
|
-
* the copy (`rewriteGlobalsCssSourcePaths`), so comparing a materialized
|
|
181
|
-
* tree against the package source would report the one file this bin always
|
|
182
|
-
* edits as though the board had edited it.
|
|
183
|
-
*/
|
|
184
85
|
function materializedContent(rel) {
|
|
185
86
|
const source = readFileSync(join(packageRoot, ...rel.split('/')))
|
|
186
87
|
if (rel !== GLOBALS_CSS) return source
|
|
@@ -213,17 +114,6 @@ function pruneEmptyDirectories(rel) {
|
|
|
213
114
|
}
|
|
214
115
|
}
|
|
215
116
|
|
|
216
|
-
/**
|
|
217
|
-
* `--at-root` only; every other mode owns `.meith/app` outright and replaces
|
|
218
|
-
* it wholesale. A file this bin is about to write is its own to replace when
|
|
219
|
-
* the record says it wrote that file before, or when what is on disk is byte
|
|
220
|
-
* for byte what it would write anyway — the second case being a checkout
|
|
221
|
-
* that committed a materialized file, where no record exists and refusing
|
|
222
|
-
* would fail the deploy over a file identical to the one being written.
|
|
223
|
-
* Anything else is the board's, and the board's is never overwritten —
|
|
224
|
-
* except `GENERATED_ENTRIES`, which are written rather than copied and so
|
|
225
|
-
* can satisfy neither test; those are replaced unconditionally.
|
|
226
|
-
*/
|
|
227
117
|
function claimRootFiles(intended) {
|
|
228
118
|
const owned = new Set(readMaterializedRecord())
|
|
229
119
|
|
|
@@ -247,12 +137,6 @@ function claimRootFiles(intended) {
|
|
|
247
137
|
}
|
|
248
138
|
}
|
|
249
139
|
|
|
250
|
-
/**
|
|
251
|
-
* Files the previous run wrote and this one will not: the framework stopped
|
|
252
|
-
* shipping them. They were this bin's own, so they go — nothing the board
|
|
253
|
-
* added is in the record, which is why removal is driven from it rather than
|
|
254
|
-
* from the directory.
|
|
255
|
-
*/
|
|
256
140
|
function removeStaleRootFiles(intended) {
|
|
257
141
|
const keeping = new Set(intended)
|
|
258
142
|
for (const rel of readMaterializedRecord()) {
|
|
@@ -267,15 +151,6 @@ function recordRootFiles(intended) {
|
|
|
267
151
|
writeFileSync(MATERIALIZED_RECORD, `${JSON.stringify({ files: intended }, null, 2)}\n`)
|
|
268
152
|
}
|
|
269
153
|
|
|
270
|
-
/**
|
|
271
|
-
* Files the board itself put under a directory the framework owns outright.
|
|
272
|
-
* Nothing here removes them — ownership is per file, so they simply survive —
|
|
273
|
-
* but a scaffolded board gitignores those directories as a unit, so a route
|
|
274
|
-
* added under `app/` works locally, is never committed, and is absent from a
|
|
275
|
-
* deploy built out of the checkout. Warned about rather than refused: the
|
|
276
|
-
* file is the author's to keep, and this is the only moment anything looks at
|
|
277
|
-
* it. `public/` is excluded because that one is shared on purpose.
|
|
278
|
-
*/
|
|
279
154
|
function warnAboutStrayBoardFiles(intended) {
|
|
280
155
|
const written = new Set(intended)
|
|
281
156
|
const owned = APP_ENTRIES.filter((entry) => !SHARED_ENTRIES.includes(entry))
|
|
@@ -297,7 +172,7 @@ function warnAboutStrayBoardFiles(intended) {
|
|
|
297
172
|
'They are left exactly as they are. A scaffolded board gitignores these directories as ' +
|
|
298
173
|
'a unit, though, so nothing above is committed, and a deploy that builds from the ' +
|
|
299
174
|
'checkout will not see it. A board extends the forum through plugins and themes ' +
|
|
300
|
-
'(docs/
|
|
175
|
+
'(docs/customization/plugins.md, docs/customization/themes.md), not by adding files here.',
|
|
301
176
|
)
|
|
302
177
|
}
|
|
303
178
|
|
|
@@ -309,24 +184,6 @@ function toPosixRelative(from, to) {
|
|
|
309
184
|
const SOURCE_LINE = /[ \t]*@source "((?:\.\.\/)+)([^"]+)";\n?/g
|
|
310
185
|
const BOARD_PACKAGES = ['node_modules', '@meith']
|
|
311
186
|
|
|
312
|
-
/**
|
|
313
|
-
* MEI-131: every `@source` in globals.css names a directory of *this*
|
|
314
|
-
* repository — `themes`, `plugins`, `packages/ui/src`. Rebasing them onto the
|
|
315
|
-
* board's workspace root is right for the one in-repo consumer (boards/stock,
|
|
316
|
-
* whose FORUM_WORKSPACE_ROOT is this repo) and wrong for every scaffolded
|
|
317
|
-
* board, where none of those directories exist: the same code is installed
|
|
318
|
-
* under `node_modules/@meith` instead. Tailwind does not fail on a `@source`
|
|
319
|
-
* that resolves to nothing — it builds green and emits no utilities for
|
|
320
|
-
* anything it could not scan — so a board deployed this way served a page
|
|
321
|
-
* with no styling at all.
|
|
322
|
-
*
|
|
323
|
-
* A source whose target is really there is kept and rebased as before. One
|
|
324
|
-
* that is not is dropped in favour of a single source over the installed
|
|
325
|
-
* packages, which covers the themes, `@meith/ui` and the plugins together.
|
|
326
|
-
* Tailwind ignores `node_modules` when detecting sources itself, but honours
|
|
327
|
-
* it when named here, and follows the symlinks a pnpm install leaves — both
|
|
328
|
-
* confirmed against the version this app pins.
|
|
329
|
-
*/
|
|
330
187
|
export function rebaseGlobalsCssSources(css, cssDir, workspaceRoot, exists = existsSync) {
|
|
331
188
|
const rebased = []
|
|
332
189
|
let missing = false
|
|
@@ -359,29 +216,6 @@ function rewriteGlobalsCssSourcePaths() {
|
|
|
359
216
|
)
|
|
360
217
|
}
|
|
361
218
|
|
|
362
|
-
/**
|
|
363
|
-
* A board outside this monorepo has a hoisted `node_modules` (see the module
|
|
364
|
-
* comment), so plain bare-specifier resolution reaches every `@meith/*`
|
|
365
|
-
* package this app's dependency graph needs, and `transpilePackages`
|
|
366
|
-
* (apps/community/next.config.mjs) is what lets Next compile the `.ts`
|
|
367
|
-
* source that resolution lands on. A board built *inside* this monorepo's
|
|
368
|
-
* own pnpm install has no such hoisting — packages here resolve each other
|
|
369
|
-
* through tsconfig path aliases straight to source (docs/architecture.md,
|
|
370
|
-
* "The board-config seam"; docs/development.md, "The workspace") rather than
|
|
371
|
-
* through real `dependencies` entries, so plain node_modules resolution
|
|
372
|
-
* finds nothing for most of them.
|
|
373
|
-
*
|
|
374
|
-
* `FORUM_ALIASES_FROM`, set only by docker/Dockerfile for `boards/stock`,
|
|
375
|
-
* names that other tsconfig (this repository's own `tsconfig.base.json`).
|
|
376
|
-
* When set, every `@meith/*` alias it declares is copied into the
|
|
377
|
-
* materialized app's own generated tsconfig, rebased to be relative to
|
|
378
|
-
* `.meith/app` — the same aliases apps/community's own tsconfig.json
|
|
379
|
-
* hand-maintains for exactly this reason. Unset (the default, and the only
|
|
380
|
-
* path a real external board ever takes), this is a no-op. `@board/config`
|
|
381
|
-
* and `@board/plugins` are excluded from the copy — they are this
|
|
382
|
-
* workspace's own seam, wired below to *this* board's files, never to
|
|
383
|
-
* whatever apps/community's own tsconfig happens to alias them to.
|
|
384
|
-
*/
|
|
385
219
|
function monorepoAliases() {
|
|
386
220
|
const configFile = process.env.FORUM_ALIASES_FROM
|
|
387
221
|
if (!configFile) return {}
|
|
@@ -398,14 +232,6 @@ function monorepoAliases() {
|
|
|
398
232
|
return aliases
|
|
399
233
|
}
|
|
400
234
|
|
|
401
|
-
/**
|
|
402
|
-
* Replaces each shipped entry (`APP_ENTRIES`) but never `rm -rf`s the whole
|
|
403
|
-
* `.meith/app` directory: `.next` lives there too once a build has run, and
|
|
404
|
-
* `forum-web start` needs that build still on disk after this same function
|
|
405
|
-
* re-materializes the sources ahead of launching the standalone server. The
|
|
406
|
-
* `next-env.d.ts` it writes only needs to exist, not be complete — next
|
|
407
|
-
* regenerates it with the right content on first run.
|
|
408
|
-
*/
|
|
409
235
|
function materialize() {
|
|
410
236
|
const boardConfig = join(workspaceRoot, 'community.config.ts')
|
|
411
237
|
const boardPlugins = join(workspaceRoot, 'community.plugins.ts')
|
|
@@ -475,13 +301,6 @@ function materialize() {
|
|
|
475
301
|
}
|
|
476
302
|
}
|
|
477
303
|
|
|
478
|
-
/**
|
|
479
|
-
* Resolved from this package's own directory rather than the workspace root:
|
|
480
|
-
* `next` is `@meith/web`'s dependency, not necessarily the workspace
|
|
481
|
-
* manifest's, and resolving from here finds it either way — hoisted to the
|
|
482
|
-
* workspace root (npm, yarn classic) or nested under this package's own
|
|
483
|
-
* `node_modules` (pnpm's default, non-hoisted layout).
|
|
484
|
-
*/
|
|
485
304
|
function resolveNextBin() {
|
|
486
305
|
const require = createRequire(join(packageRoot, 'package.json'))
|
|
487
306
|
try {
|
package/bin/forum-web.test.ts
CHANGED
|
@@ -36,14 +36,6 @@ describe('rebaseGlobalsCssSources', () => {
|
|
|
36
36
|
expect(rewritten).toContain('.foo { color: red; }')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* Against the real shipped file, not a fixture: this rebase now runs on
|
|
41
|
-
* every materialization including the default one, where it must be a
|
|
42
|
-
* no-op. Every `@source` in that file is written relative to the workspace
|
|
43
|
-
* root, and this only stays a no-op while that holds — a `@source` meaning
|
|
44
|
-
* anything else (`../fonts`, for `src/fonts`) would be silently retargeted
|
|
45
|
-
* at the workspace root, and fails here instead.
|
|
46
|
-
*/
|
|
47
39
|
const shippedGlobalsCss = readFileSync(
|
|
48
40
|
new URL('../src/styles/globals.css', import.meta.url),
|
|
49
41
|
'utf8',
|
|
@@ -87,13 +79,6 @@ describe('rebaseGlobalsCssSources', () => {
|
|
|
87
79
|
).toBe(css)
|
|
88
80
|
})
|
|
89
81
|
|
|
90
|
-
/**
|
|
91
|
-
* MEI-131. A scaffolded board has none of the directories these sources
|
|
92
|
-
* name — the same code is installed under `node_modules/@meith` — and
|
|
93
|
-
* Tailwind emits no utilities for a source it cannot scan while still
|
|
94
|
-
* exiting 0, so the failure is a board that serves an unstyled page rather
|
|
95
|
-
* than a build that stops.
|
|
96
|
-
*/
|
|
97
82
|
describe('in a board, where none of those directories exist', () => {
|
|
98
83
|
const inBoard = (path: string) => path.includes('node_modules')
|
|
99
84
|
|
|
@@ -157,13 +142,6 @@ describe('rebaseGlobalsCssSources', () => {
|
|
|
157
142
|
})
|
|
158
143
|
})
|
|
159
144
|
|
|
160
|
-
/**
|
|
161
|
-
* `forum-web start --at-root` materializes and then stops, because there is
|
|
162
|
-
* no standalone build for it to exec — which makes it the whole
|
|
163
|
-
* materialization path in under a tenth of a second, with no `next build`
|
|
164
|
-
* anywhere near it. Materialization is the part that has been wrong; the
|
|
165
|
-
* build is not.
|
|
166
|
-
*/
|
|
167
145
|
const BIN = fileURLToPath(new URL('./forum-web.mjs', import.meta.url))
|
|
168
146
|
|
|
169
147
|
function materializeAtRoot(dir: string) {
|
package/next.config.mjs
CHANGED
|
@@ -1,33 +1,13 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
|
|
5
5
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Two directories up from this file's own location by default — correct
|
|
9
|
-
* where `forum-web` materializes at `.meith/app` and where this file sits in
|
|
10
|
-
* `apps/community` unmaterialized. `boards/stock` and `forum-web --at-root`
|
|
11
|
-
* are both at other depths, and neither relies on this default:
|
|
12
|
-
* `FORUM_WORKSPACE_ROOT` is set for the first by that board's own scripts
|
|
13
|
-
* and passed on by `forum-web` in every case, so this fallback only applies
|
|
14
|
-
* when the file is read outside that bin entirely (see
|
|
15
|
-
* docs/architecture.md, "The stock board"; docs/development.md, "Consuming
|
|
16
|
-
* the board from a workspace").
|
|
17
|
-
*/
|
|
18
7
|
const workspaceRoot = process.env.FORUM_WORKSPACE_ROOT
|
|
19
8
|
? path.resolve(process.env.FORUM_WORKSPACE_ROOT)
|
|
20
9
|
: path.join(here, '../../')
|
|
21
10
|
|
|
22
|
-
/**
|
|
23
|
-
* The relative equivalent of `workspaceRoot`, for `outputFileTracingIncludes`
|
|
24
|
-
* below — see docs/development.md, "Consuming the board from a workspace",
|
|
25
|
-
* for why that option needs a path relative to this file rather than an
|
|
26
|
-
* absolute one. It is `.` rather than the empty string when this file already
|
|
27
|
-
* sits at the workspace root, which is what `forum-web build --at-root`
|
|
28
|
-
* materializes (same section) — an empty prefix would make the glob below
|
|
29
|
-
* read as an absolute path and match nothing.
|
|
30
|
-
*/
|
|
31
11
|
const upToWorkspaceRoot = path.relative(here, workspaceRoot).split(path.sep).join('/') || '.'
|
|
32
12
|
|
|
33
13
|
const loadedEnvFiles = []
|
|
@@ -42,6 +22,8 @@ if (process.env.NODE_ENV !== 'production' && loadedEnvFiles.length > 0) {
|
|
|
42
22
|
console.log(`- Environments: ${loadedEnvFiles.join(', ')} (${workspaceRoot})`)
|
|
43
23
|
}
|
|
44
24
|
|
|
25
|
+
const FRAMEWORK_PACKAGES = ['next', 'react', 'react-dom']
|
|
26
|
+
|
|
45
27
|
const nextConfig = {
|
|
46
28
|
...(process.env.VERCEL ? {} : { output: 'standalone' }),
|
|
47
29
|
poweredByHeader: false,
|
|
@@ -58,11 +40,9 @@ const nextConfig = {
|
|
|
58
40
|
'nodemailer',
|
|
59
41
|
],
|
|
60
42
|
outputFileTracingRoot: workspaceRoot,
|
|
61
|
-
/** See docs/development.md, "Consuming the board from a workspace", for why this is set. */
|
|
62
43
|
turbopack: {
|
|
63
44
|
root: workspaceRoot,
|
|
64
45
|
},
|
|
65
|
-
/** Works around a gap in Next's own tracing — see docs/development.md, "Consuming the board from a workspace". */
|
|
66
46
|
outputFileTracingIncludes: {
|
|
67
47
|
'/**': [
|
|
68
48
|
`${upToWorkspaceRoot}/node_modules/.pnpm/@swc+helpers@0.5.23/node_modules/@swc/helpers/**/*`,
|
|
@@ -72,12 +52,6 @@ const nextConfig = {
|
|
|
72
52
|
images: {
|
|
73
53
|
unoptimized: true,
|
|
74
54
|
},
|
|
75
|
-
/**
|
|
76
|
-
* Every `@meith/*` package this app's dependency graph reaches, not just the
|
|
77
|
-
* ones apps/community imports directly — see docs/development.md,
|
|
78
|
-
* "Consuming the board from a workspace", and docs/release.md, "They ship
|
|
79
|
-
* TypeScript source, deliberately".
|
|
80
|
-
*/
|
|
81
55
|
transpilePackages: [
|
|
82
56
|
'@meith/accounts',
|
|
83
57
|
'@meith/admin',
|
|
@@ -146,4 +120,15 @@ const nextConfig = {
|
|
|
146
120
|
},
|
|
147
121
|
}
|
|
148
122
|
|
|
123
|
+
const boardManifestFile = path.join(workspaceRoot, 'package.json')
|
|
124
|
+
if (existsSync(boardManifestFile)) {
|
|
125
|
+
const boardManifest = JSON.parse(readFileSync(boardManifestFile, 'utf8'))
|
|
126
|
+
for (const name of Object.keys(boardManifest.dependencies ?? {})) {
|
|
127
|
+
if (FRAMEWORK_PACKAGES.includes(name)) continue
|
|
128
|
+
if (nextConfig.serverExternalPackages.includes(name)) continue
|
|
129
|
+
if (nextConfig.transpilePackages.includes(name)) continue
|
|
130
|
+
nextConfig.transpilePackages.push(name)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
149
134
|
export default nextConfig
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,52 +43,52 @@
|
|
|
43
43
|
"typescript": "7.0.2",
|
|
44
44
|
"@types/react": "^19",
|
|
45
45
|
"@types/react-dom": "^19",
|
|
46
|
-
"@meith/admin": "0.
|
|
47
|
-
"@meith/antispam": "0.
|
|
48
|
-
"@meith/
|
|
49
|
-
"@meith/
|
|
50
|
-
"@meith/attachments": "0.
|
|
51
|
-
"@meith/authorization": "0.
|
|
52
|
-
"@meith/avatars": "0.
|
|
53
|
-
"@meith/core": "0.
|
|
54
|
-
"@meith/
|
|
55
|
-
"@meith/
|
|
56
|
-
"@meith/drivers": "0.
|
|
57
|
-
"@meith/
|
|
58
|
-
"@meith/
|
|
59
|
-
"@meith/
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/
|
|
63
|
-
"@meith/install": "0.
|
|
64
|
-
"@meith/
|
|
65
|
-
"@meith/marketplace": "0.
|
|
66
|
-
"@meith/
|
|
67
|
-
"@meith/messages": "0.
|
|
68
|
-
"@meith/
|
|
69
|
-
"@meith/
|
|
70
|
-
"@meith/
|
|
71
|
-
"@meith/plugin-kit": "0.
|
|
72
|
-
"@meith/
|
|
73
|
-
"@meith/
|
|
74
|
-
"@meith/profile-fields": "0.
|
|
75
|
-
"@meith/relations": "0.
|
|
76
|
-
"@meith/
|
|
77
|
-
"@meith/
|
|
78
|
-
"@meith/
|
|
79
|
-
"@meith/
|
|
80
|
-
"@meith/signatures": "0.
|
|
81
|
-
"@meith/
|
|
82
|
-
"@meith/
|
|
83
|
-
"@meith/theme-clubhouse": "0.
|
|
84
|
-
"@meith/theme-default": "0.
|
|
85
|
-
"@meith/theme-
|
|
86
|
-
"@meith/theme-
|
|
87
|
-
"@meith/theme-
|
|
88
|
-
"@meith/threads": "0.
|
|
89
|
-
"@meith/ui": "0.
|
|
90
|
-
"@meith/
|
|
91
|
-
"@meith/upgrade": "0.
|
|
46
|
+
"@meith/admin": "0.22.0",
|
|
47
|
+
"@meith/antispam": "0.22.0",
|
|
48
|
+
"@meith/api": "0.22.0",
|
|
49
|
+
"@meith/accounts": "0.22.0",
|
|
50
|
+
"@meith/attachments": "0.22.0",
|
|
51
|
+
"@meith/authorization": "0.22.0",
|
|
52
|
+
"@meith/avatars": "0.22.0",
|
|
53
|
+
"@meith/core": "0.22.0",
|
|
54
|
+
"@meith/drafts": "0.22.0",
|
|
55
|
+
"@meith/db": "0.22.0",
|
|
56
|
+
"@meith/drivers": "0.22.0",
|
|
57
|
+
"@meith/demo": "0.22.0",
|
|
58
|
+
"@meith/groups": "0.22.0",
|
|
59
|
+
"@meith/forums": "0.22.0",
|
|
60
|
+
"@meith/import": "0.22.0",
|
|
61
|
+
"@meith/events": "0.22.0",
|
|
62
|
+
"@meith/i18n": "0.22.0",
|
|
63
|
+
"@meith/install": "0.22.0",
|
|
64
|
+
"@meith/markdown": "0.22.0",
|
|
65
|
+
"@meith/marketplace": "0.22.0",
|
|
66
|
+
"@meith/mail": "0.22.0",
|
|
67
|
+
"@meith/messages": "0.22.0",
|
|
68
|
+
"@meith/notifications": "0.22.0",
|
|
69
|
+
"@meith/plugin-dues": "0.22.0",
|
|
70
|
+
"@meith/moderation": "0.22.0",
|
|
71
|
+
"@meith/plugin-kit": "0.22.0",
|
|
72
|
+
"@meith/posts": "0.22.0",
|
|
73
|
+
"@meith/polls": "0.22.0",
|
|
74
|
+
"@meith/profile-fields": "0.22.0",
|
|
75
|
+
"@meith/relations": "0.22.0",
|
|
76
|
+
"@meith/reputation": "0.22.0",
|
|
77
|
+
"@meith/runtime": "0.22.0",
|
|
78
|
+
"@meith/search": "0.22.0",
|
|
79
|
+
"@meith/settings": "0.22.0",
|
|
80
|
+
"@meith/signatures": "0.22.0",
|
|
81
|
+
"@meith/subscriptions": "0.22.0",
|
|
82
|
+
"@meith/theme-kit": "0.22.0",
|
|
83
|
+
"@meith/theme-clubhouse": "0.22.0",
|
|
84
|
+
"@meith/theme-default": "0.22.0",
|
|
85
|
+
"@meith/theme-phasebook": "0.22.0",
|
|
86
|
+
"@meith/theme-midnight": "0.22.0",
|
|
87
|
+
"@meith/theme-raidframe": "0.22.0",
|
|
88
|
+
"@meith/threads": "0.22.0",
|
|
89
|
+
"@meith/ui": "0.22.0",
|
|
90
|
+
"@meith/tasks": "0.22.0",
|
|
91
|
+
"@meith/upgrade": "0.22.0"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"dev": "next dev",
|
package/proxy.ts
CHANGED
|
@@ -48,20 +48,6 @@ interface Policy {
|
|
|
48
48
|
readonly value: string
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* Passes the request through, and hands a reader something to be counted by.
|
|
53
|
-
*
|
|
54
|
-
* The cookie is minted here because the Edge is the only place that can set one
|
|
55
|
-
* on an ordinary page response — a render cannot. The row it stands for is
|
|
56
|
-
* written by the render, which has the database this runtime does not.
|
|
57
|
-
*
|
|
58
|
-
* Whether the client *returned* the cookie is the load-bearing part, and it
|
|
59
|
-
* cannot be read downstream: Next reflects a cookie set here into the request
|
|
60
|
-
* the render sees, so a first-ever visit is indistinguishable from a returning
|
|
61
|
-
* one by cookie alone. Hence the header — set when the cookie is new, deleted
|
|
62
|
-
* otherwise, so a crawler that never keeps one is never counted and a client
|
|
63
|
-
* cannot supply its own.
|
|
64
|
-
*/
|
|
65
51
|
function withPath(req: NextRequest, policy: Policy): NextResponse {
|
|
66
52
|
const fresh = !req.cookies.has(GUEST_COOKIE) && !req.cookies.has(DEV_GUEST_COOKIE)
|
|
67
53
|
|