fedipod 1.47.0 → 1.48.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/README.md +3 -3
- package/architecture.md +1 -1
- package/gateway.md +1 -1
- package/lib/server/embed.mjs +1 -1
- package/lib/session/README.md +2 -135
- package/lib/surface/surface.mjs +4 -3
- package/package.json +5 -3
- package/scripts/check-netlify-bundles.mjs +0 -4
- package/scripts/check-pod-calls.mjs +5 -108
- package/scripts/check-sizes.mjs +6 -3
- package/scripts/dependents.mjs +76 -0
- package/scripts/stage-site.mjs +10 -8
- package/scripts/start.mjs +2 -2
- package/scripts/test-dependents.mjs +37 -0
- package/web/app/dist/boot.js +59 -57
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +1 -1
- package/web/app/dist/sw.js.map +2 -2
- package/web/app/forum-support.mjs +2 -2
- package/web/app/oidc-session.mjs +2 -2
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
package/README.md
CHANGED
|
@@ -137,11 +137,11 @@ not change.
|
|
|
137
137
|
|
|
138
138
|
## Other ways to use FediPod
|
|
139
139
|
|
|
140
|
-
- [fediverse-session](
|
|
141
|
-
- [FediPod Server](
|
|
140
|
+
- [fediverse-session](https://github.com/jeff-zucker/fediverse-session) - an ESM library that supports both Solid-based and regular Fediverse accounts with methods to login, reply, boost, etc.
|
|
141
|
+
- [FediPod Server](https://github.com/jeff-zucker/fedipod-server): a full ActivityPub
|
|
142
142
|
server as a Community Solid Server component, giving every pod on the server
|
|
143
143
|
the option of a Fediverse account.
|
|
144
|
-
- [FediPod BB](
|
|
144
|
+
- [FediPod BB](https://github.com/jeff-zucker/fedipod-bb) - A Lemmy-like bulletin board forum with the structure stored on a group pod and users' data stored on their own pod or on their Fediverse server if they don't have a pod.
|
|
145
145
|
- [FediPod DeviceAgent](device-agent.md) a single-user AP server that lives partly in a gateway, partly on a local device, with all data stored on your pod.
|
|
146
146
|
- [FediPod Groups](groups.md): hosting a discussion group of Fediverse and Bluesky users from a pod.
|
|
147
147
|
- [FediPod Gateway](gateway.md): A thin, always-on door running on Netlify or any small host that provides a middleman between a pod and the single-user agents. (only needed with `BrowserAgent` and `DeviceAgent`)
|
package/architecture.md
CHANGED
|
@@ -27,7 +27,7 @@ hold). A gateway's admin may turn the hold off: then there is no copy, and
|
|
|
27
27
|
everything works on the pod, the browser and the gateway taking turns by the
|
|
28
28
|
pod's lease. See [the gateway](gateway.md).
|
|
29
29
|
|
|
30
|
-
[FediPod Server](
|
|
30
|
+
[FediPod Server](https://github.com/jeff-zucker/fedipod-server) puts the agent inside a
|
|
31
31
|
Community Solid Server, so anyone with a pod on that server can opt in to a
|
|
32
32
|
Fediverse account fed by the server itself.
|
|
33
33
|
|
package/gateway.md
CHANGED
|
@@ -308,7 +308,7 @@ Two ways are ready to use:
|
|
|
308
308
|
[netlify/README.md](netlify/README.md) for the deployment specifics.
|
|
309
309
|
- **On any box of your own**, with your own adapter around the same core.
|
|
310
310
|
- **Inside a Community Solid Server**, as the same door run in-process by the
|
|
311
|
-
CSS component — see [
|
|
311
|
+
CSS component — see [fedipod-server](https://github.com/jeff-zucker/fedipod-server).
|
|
312
312
|
|
|
313
313
|
### Offering accounts to other people
|
|
314
314
|
|
package/lib/server/embed.mjs
CHANGED
|
@@ -396,7 +396,7 @@ export async function startEmbeddedAgent({
|
|
|
396
396
|
basePath: uiPath,
|
|
397
397
|
mount,
|
|
398
398
|
publicOrigin: base,
|
|
399
|
-
scheme: new URL(base).protocol,
|
|
399
|
+
scheme: new URL(base).protocol.slice(0, -1),
|
|
400
400
|
});
|
|
401
401
|
|
|
402
402
|
// A delivery landing in the inbox — through the door, or from any other
|
package/lib/session/README.md
CHANGED
|
@@ -1,137 +1,4 @@
|
|
|
1
1
|
# fediverse-session
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
- **`login`** — sends the person to wherever their account signs in, their
|
|
7
|
-
Mastodon server or their pod, and brings them back to where they were.
|
|
8
|
-
- **`resume`** — on every page load, finishes a sign-in they are coming back
|
|
9
|
-
from. Otherwise does nothing.
|
|
10
|
-
- **`current`** — the signed-in account, or nothing.
|
|
11
|
-
- **`signOut`** — signs them out, at their server or pod as well as in the
|
|
12
|
-
browser.
|
|
13
|
-
- **`describe`** — says what an address is without signing in: a Mastodon
|
|
14
|
-
account or a pod account, and its handle.
|
|
15
|
-
|
|
16
|
-
Once signed in, the account gives you:
|
|
17
|
-
|
|
18
|
-
- **`post`** — publishes a post as them, public, unlisted or followers-only.
|
|
19
|
-
- **`reply`** — answers a post.
|
|
20
|
-
- **`timeline`** — their home timeline, newest first.
|
|
21
|
-
- **`outbox`** — their own posts, newest first. Pass `rdf: true` to also get
|
|
22
|
-
the real thing as RDF, for an app that wants to work with it as linked
|
|
23
|
-
data rather than as this library's own shape.
|
|
24
|
-
- **`follow`** — follows someone by handle.
|
|
25
|
-
- **`favourite`**, **`boost`** — as they say.
|
|
26
|
-
- **`profile`** — who they are as their server or pod shows them: name,
|
|
27
|
-
handle, picture, bio, counts, profile page, and the WebID for a pod
|
|
28
|
-
account.
|
|
29
|
-
- **`notice`** — a sentence to show them, or nothing. A browser-based
|
|
30
|
-
FediPod account gets: "Because you have a browser-based account, your
|
|
31
|
-
posts and interactions will only go out to the Fediverse when your browser
|
|
32
|
-
is opened to your account."
|
|
33
|
-
- **`handle`, `name`, `kind`** — who they are, and whether the account is on
|
|
34
|
-
Mastodon or a pod, for an app that wants to know.
|
|
35
|
-
- **`signOut`** — the same as above.
|
|
36
|
-
|
|
37
|
-
The app never asks which kind it has. A Mastodon account acts at once. A
|
|
38
|
-
pod account's actions are carried out by its own agent, at once for a
|
|
39
|
-
DeviceAgent or Server account, and when fedipod.net is next open for a
|
|
40
|
-
browser-based one.
|
|
41
|
-
|
|
42
|
-
Three files, no dependencies. It runs in a page and in a service worker.
|
|
43
|
-
The one exception: `outbox({ rdf: true })` needs the `jsonld` package, and
|
|
44
|
-
only loads it when that flag is actually used.
|
|
45
|
-
|
|
46
|
-
[The demo](https://jeff-zucker.github.io/FediPod/) shows the sign-in and
|
|
47
|
-
the profile that comes back.
|
|
48
|
-
|
|
49
|
-
## Get it
|
|
50
|
-
|
|
51
|
-
From a CDN, with nothing to install:
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
import { fediAccount } from 'https://cdn.jsdelivr.net/npm/fediverse-session@0.2.1/fedi-account.mjs';
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Or from npm:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install fediverse-session
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
```js
|
|
64
|
-
import { fediAccount } from 'fediverse-session';
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Use
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
import { fediAccount } from 'fediverse-session';
|
|
71
|
-
const accounts = fediAccount({ dbName: 'my-app', clientName: 'My app' });
|
|
72
|
-
|
|
73
|
-
await accounts.resume(); // on every page load
|
|
74
|
-
|
|
75
|
-
signInButton.onclick = () => accounts.login(addressField.value);
|
|
76
|
-
|
|
77
|
-
const me = await accounts.current(); // null when nobody is signed in
|
|
78
|
-
if (me?.notice) show(me.notice);
|
|
79
|
-
await me.post({ text: 'Hello from my app' });
|
|
80
|
-
for (const p of await me.timeline({ limit: 20 })) render(p);
|
|
81
|
-
for (const p of await me.outbox({ limit: 20 })) render(p);
|
|
82
|
-
await me.follow('@aisha@her.server');
|
|
83
|
-
await me.reply(p.url, 'Well said');
|
|
84
|
-
await me.signOut();
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## The details
|
|
88
|
-
|
|
89
|
-
- **`fediAccount({ dbName, clientName })`** — sets the library up for your
|
|
90
|
-
app, once. `dbName` keeps this app's sign-ins apart from another app's on
|
|
91
|
-
the same origin. `clientName` is what the person sees on the consent
|
|
92
|
-
screen.
|
|
93
|
-
- **`login(address, { returnTo, redirectUri })`** — `@kwame@mastodon.social`
|
|
94
|
-
goes to Mastodon; `@mei@fedipod.net`, or a WebID, goes to the pod. They
|
|
95
|
-
come back to this page unless `redirectUri` says otherwise, and are
|
|
96
|
-
returned to where they were unless `returnTo` says otherwise.
|
|
97
|
-
- **`post({ text, inReplyTo, visibility })`** — `visibility` is `public`,
|
|
98
|
-
`unlisted` or `followers`.
|
|
99
|
-
- **`reply(post, text)`** — the post is named by its address or its id.
|
|
100
|
-
- **`timeline({ limit })`** — each post as `{ id, url, author: { id, handle,
|
|
101
|
-
name }, html, published, inReplyTo }`.
|
|
102
|
-
- **`outbox({ limit, rdf })`** — the same shape as `timeline`, but only this
|
|
103
|
-
account's own posts. With `rdf: true`, the returned array also carries
|
|
104
|
-
`.rdf`: the real outbox parsed into an RDF/JS quad array — for a Mastodon
|
|
105
|
-
account too, since a Mastodon server is itself an ActivityPub server with
|
|
106
|
-
a real actor and outbox, same as a pod's. It comes back empty on a server
|
|
107
|
-
that requires a signed request just to read that document, which some
|
|
108
|
-
do. Reading it needs the `jsonld` package available to your app; without
|
|
109
|
-
`rdf: true` nothing changes and nothing extra loads.
|
|
110
|
-
- **`fetch`** — the raw authenticated fetch, for anything the above does not
|
|
111
|
-
cover.
|
|
112
|
-
- **`actor`** — the account's ActivityPub id.
|
|
113
|
-
|
|
114
|
-
Every action answers `{ sent: true }` when the server did it, or `{ queued:
|
|
115
|
-
true }` when the account's own agent will carry it out.
|
|
116
|
-
|
|
117
|
-
## What it cannot do
|
|
118
|
-
|
|
119
|
-
- **An address whose server is neither a Mastodon-family server nor a Solid
|
|
120
|
-
pod** is refused with a sentence naming the host.
|
|
121
|
-
- **A browser-based FediPod account acts when its browser is open.** Posts,
|
|
122
|
-
follows and replies wait at the account's outbox until fedipod.net is next
|
|
123
|
-
open in some browser, and its timeline is the one that browser last built.
|
|
124
|
-
The account's `notice` says so; show it.
|
|
125
|
-
- **A Mastodon account has no pod.** An app that keeps things on a pod has
|
|
126
|
-
nowhere to keep them for such a person.
|
|
127
|
-
|
|
128
|
-
## Where the sign-in goes, and what stays in the browser
|
|
129
|
-
|
|
130
|
-
A Mastodon account signs in at its own server, which asks the person to
|
|
131
|
-
approve your app once. The token it grants stays in their browser and is
|
|
132
|
-
handed back to the server at sign-out.
|
|
133
|
-
|
|
134
|
-
A pod account signs in at the pod's login provider. The key that proves the
|
|
135
|
-
sign-in is made in the browser and cannot be read out of it, even by a
|
|
136
|
-
script on the page. The person stays signed in across browser restarts
|
|
137
|
-
until they sign out, and sign-out is reported to the provider.
|
|
3
|
+
fediverse-session lives in its own repository:
|
|
4
|
+
https://github.com/jeff-zucker/fediverse-session
|
package/lib/surface/surface.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { MastoApi } from '../client/masto/index.mjs';
|
|
12
13
|
import { C2S } from '../client/c2s.mjs';
|
|
13
14
|
import { makeC2sAuth } from '../client/oidc-auth.mjs';
|
|
@@ -75,8 +76,8 @@ const hasCredential = (home) => fs.existsSync(path.join(home, 'credential.json')
|
|
|
75
76
|
// guards the operator's door (basePath) instead of the whole surface.
|
|
76
77
|
const SESSION_FILES = {
|
|
77
78
|
'/oauth/session/signin.mjs': 'web/admin/oauth-signin.mjs',
|
|
78
|
-
'/oauth/session/fedi-login.mjs': '
|
|
79
|
-
'/oauth/session/oidc-session.mjs': '
|
|
79
|
+
'/oauth/session/fedi-login.mjs': fileURLToPath(import.meta.resolve('fediverse-session/fedi-login.mjs')),
|
|
80
|
+
'/oauth/session/oidc-session.mjs': fileURLToPath(import.meta.resolve('fediverse-session/oidc-session.mjs')),
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
// `extraRoutes` are the routes a host process adds after the owner's — the
|
|
@@ -194,7 +195,7 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
194
195
|
const sessionFile = SESSION_FILES[p];
|
|
195
196
|
if (sessionFile) {
|
|
196
197
|
let body;
|
|
197
|
-
try { body = fs.readFileSync(path.
|
|
198
|
+
try { body = fs.readFileSync(path.resolve(projectRoot, sessionFile)); } catch { return json(res, 404, { error: 'not here' }); }
|
|
198
199
|
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' });
|
|
199
200
|
res.end(body); return;
|
|
200
201
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.0",
|
|
4
4
|
"description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"install-service": "node bin/fedipod.mjs install-service",
|
|
56
56
|
"uninstall-service": "node bin/fedipod.mjs uninstall-service",
|
|
57
57
|
"prepublishOnly": "npm test",
|
|
58
|
-
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/hold-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node
|
|
58
|
+
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/hold-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node --test claude/smoke-tests/forum-signup-form.test.mjs claude/smoke-tests/door-verify.test.mjs && node scripts/check-sizes.mjs && node scripts/test-dependents.mjs && node claude/validation/browser-agent/all.mjs",
|
|
59
59
|
"verify-ui": "node scripts/verify-ui.mjs",
|
|
60
60
|
"build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
|
|
61
61
|
"build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"@rdfjs/term-set": "^2.0.3",
|
|
77
77
|
"@solid/access-token-verifier": "^2.1.1",
|
|
78
78
|
"clownface": "^2.0.3",
|
|
79
|
+
"fediverse-session": "^0.2.1",
|
|
79
80
|
"jose": "^6.2.8",
|
|
80
81
|
"json-canon": "^1.0.1",
|
|
81
82
|
"jsonld": "^9.0.0",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"package-lock.json": "npm:empty-npm-package@1.0.0"
|
|
97
98
|
},
|
|
98
99
|
"devDependencies": {
|
|
99
|
-
"esbuild": "^0.28.2"
|
|
100
|
+
"esbuild": "^0.28.2",
|
|
101
|
+
"fedipod-bb": "^0.4.1"
|
|
100
102
|
}
|
|
101
103
|
}
|
|
@@ -18,10 +18,6 @@ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fn-bundles-'));
|
|
|
18
18
|
let fails = 0;
|
|
19
19
|
const check = (ok, msg) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${msg}`); if (!ok) fails++; };
|
|
20
20
|
try {
|
|
21
|
-
// The forum the front names (netlify/forum-support.mjs) imports fedipod by
|
|
22
|
-
// name; in this checkout that must be the checkout itself, or the bundle
|
|
23
|
-
// carries a registry copy beside the Gateway's own code.
|
|
24
|
-
execFileSync(process.execPath, [path.join(root, 'packages/fedipod-bb/scripts/link-fedipod.mjs')], { cwd: root, stdio: 'inherit' });
|
|
25
21
|
execFileSync('netlify', ['functions:build', '--src', path.join(root, 'netlify/functions'), '--functions', path.join(tmp, 'zips')],
|
|
26
22
|
{ cwd: root, stdio: 'ignore' });
|
|
27
23
|
const manifest = JSON.parse(fs.readFileSync(path.join(tmp, 'zips/manifest.json'), 'utf8'));
|
|
@@ -179,116 +179,13 @@ if (!failures) pass('every in-scope caller reaches the pod through a named opera
|
|
|
179
179
|
pass('the library imports and runs against a stub with nothing but rdflib on the path');
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// ---- 8.
|
|
182
|
+
// ---- 8. the app binds the session library to its own database ----
|
|
183
|
+
// The library (fediverse-session) checks itself in its own repository; what
|
|
184
|
+
// is FediPod's is the name every signed-in browser holds its session under.
|
|
183
185
|
{
|
|
184
|
-
const SESSION = path.join(root, 'lib/session');
|
|
185
|
-
const files = fs.readdirSync(SESSION).filter((f) => f.endsWith('.mjs'));
|
|
186
|
-
let bad = 0;
|
|
187
|
-
for (const f of files) {
|
|
188
|
-
const src = code(read(path.join(SESSION, f)));
|
|
189
|
-
for (const m of src.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
|
|
190
|
-
if (!m[1].startsWith('./')) { fail(`lib/session/${f} imports ${m[1]} — the session library imports nothing but its own files`); bad++; }
|
|
191
|
-
}
|
|
192
|
-
if (/fedipod/i.test(src)) { fail(`lib/session/${f} names FediPod in code — the database and client names are the caller's`); bad++; }
|
|
193
|
-
}
|
|
194
|
-
const { solidOidcSession } = await import(path.join(SESSION, 'oidc-session.mjs'));
|
|
195
|
-
const oidc = solidOidcSession({ dbName: 'x', clientName: 'y' });
|
|
196
|
-
for (const k of ['beginLogin', 'completeLogin', 'getSession', 'signOut']) {
|
|
197
|
-
if (typeof oidc[k] !== 'function') { fail(`solidOidcSession() returned no ${k}`); bad++; }
|
|
198
|
-
}
|
|
199
|
-
// An address at a Gateway resolves to the POD's login, through the alias
|
|
200
|
-
// WebFinger names; an address on a Mastodon server is refused in words.
|
|
201
|
-
const { fediLogin, parseAddress } = await import(path.join(SESSION, 'fedi-login.mjs'));
|
|
202
|
-
const jrd = (host, aliases, self) => ({ ok: true, json: async () => ({ aliases, links: [{ rel: 'self', type: 'application/activity+json', href: self }] }) });
|
|
203
|
-
const stub = async (url) => {
|
|
204
|
-
if (url.startsWith('https://front.example/.well-known/webfinger')) return jrd('front.example', ['https://mei.pod.example/anything/ap/actor'], 'https://front.example/u/mei/ap/actor');
|
|
205
|
-
if (url.startsWith('https://masto.example/.well-known/webfinger')) return jrd('masto.example', ['https://masto.example/@kwame'], 'https://masto.example/users/kwame');
|
|
206
|
-
if (url === 'https://mei.pod.example/.well-known/openid-configuration') return { ok: true, json: async () => ({ issuer: 'https://pod.example/', authorization_endpoint: 'https://pod.example/.oidc/auth' }) };
|
|
207
|
-
// A pod whose host answers no discovery: the actor names the WebID, and
|
|
208
|
-
// the WebID document names the issuer.
|
|
209
|
-
if (url.startsWith('https://server.example/.well-known/webfinger')) return jrd('server.example', [], 'https://server.example/aisha/anything/ap/actor');
|
|
210
|
-
if (url === 'https://server.example/aisha/anything/ap/actor') return { ok: true, json: async () => ({ id: url, alsoKnownAs: ['https://server.example/aisha/profile/card#me'] }) };
|
|
211
|
-
if (url === 'https://server.example/aisha/profile/card#me') return { ok: true, json: async () => ([{ '@id': url, 'http://www.w3.org/ns/solid/terms#oidcIssuer': [{ '@id': 'https://idp.example/' }] }]) };
|
|
212
|
-
if (url === 'https://idp.example/.well-known/openid-configuration') return { ok: true, json: async () => ({ issuer: 'https://idp.example/', authorization_endpoint: 'https://idp.example/auth' }) };
|
|
213
|
-
return { ok: false, json: async () => ({}) };
|
|
214
|
-
};
|
|
215
|
-
const login = fediLogin({ dbName: 'x', fetch: stub });
|
|
216
|
-
const found = await login.resolve('@mei@front.example').catch((e) => ({ error: e.message }));
|
|
217
|
-
if (found.issuer !== 'https://pod.example/' || found.origin !== 'https://mei.pod.example' || found.actor !== 'https://front.example/u/mei/ap/actor') {
|
|
218
|
-
fail(`a fronted address did not resolve to its pod's login: ${JSON.stringify(found)}`); bad++;
|
|
219
|
-
}
|
|
220
|
-
const viaWebId = await login.resolve('@aisha@server.example').catch((e) => ({ error: e.message }));
|
|
221
|
-
if (viaWebId.issuer !== 'https://idp.example/' || viaWebId.webId !== 'https://server.example/aisha/profile/card#me') {
|
|
222
|
-
fail(`an address on a host without discovery did not resolve through its WebID: ${JSON.stringify(viaWebId)}`); bad++;
|
|
223
|
-
}
|
|
224
|
-
const typedWebId = await login.resolve('https://server.example/aisha/profile/card#me').catch((e) => ({ error: e.message }));
|
|
225
|
-
if (typedWebId.issuer !== 'https://idp.example/') { fail(`a typed WebID did not resolve: ${JSON.stringify(typedWebId)}`); bad++; }
|
|
226
|
-
const refused = await login.resolve('kwame@masto.example').then(() => '', (e) => e.message);
|
|
227
|
-
if (!/masto\.example is not a Solid pod/.test(refused)) { fail(`a Mastodon address was not refused in words: ${refused || 'resolved'}`); bad++; }
|
|
228
|
-
if (parseAddress('nonsense') !== null || parseAddress('@mei@Front.Example')?.at !== '@mei@front.example') { fail('parseAddress does not read @you@host'); bad++; }
|
|
229
|
-
// The account library tells a Mastodon address from a pod address and
|
|
230
|
-
// speaks to each: the Mastodon API for one, the outbox door for the other.
|
|
231
|
-
const { fediAccount, BROWSER_ACCOUNT_NOTICE } = await import(path.join(SESSION, 'fedi-account.mjs'));
|
|
232
|
-
const seen = [];
|
|
233
|
-
const acctStub = async (url, init = {}) => {
|
|
234
|
-
seen.push(`${init.method || 'GET'} ${url}`);
|
|
235
|
-
const ok = (body, headers = {}) => ({ ok: true, status: 200, headers: { get: (k) => headers[k] || null }, json: async () => body, text: async () => JSON.stringify(body) });
|
|
236
|
-
if (url.startsWith('https://masto.example/.well-known/webfinger')) return jrd('masto.example', ['https://masto.example/@kwame'], 'https://masto.example/users/kwame');
|
|
237
|
-
if (url === 'https://masto.example/api/v1/instance') return ok({ uri: 'masto.example', title: 'Masto' });
|
|
238
|
-
if (url === 'https://masto.example/api/v1/statuses' && init.method === 'POST') return ok({ id: '77', url: 'https://masto.example/@kwame/77' });
|
|
239
|
-
if (url.startsWith('https://masto.example/api/v1/timelines/home')) return ok([{ id: '1', url: 'https://masto.example/@aisha/1', created_at: '2026-09-20T10:00:00Z', content: '<p>hi</p>', account: { acct: 'aisha', display_name: 'Aisha', url: 'https://masto.example/@aisha' } }]);
|
|
240
|
-
if (url.startsWith('https://masto.example/api/v1/accounts/lookup')) return ok({ id: '9' });
|
|
241
|
-
if (url === 'https://masto.example/api/v1/accounts/9/follow') return ok({ following: true });
|
|
242
|
-
if (url === 'https://front.example/api/v1/instance' || url === 'https://mei.pod.example/api/v1/instance') return { ok: false, status: 404, json: async () => ({}) };
|
|
243
|
-
return stub(url, init);
|
|
244
|
-
};
|
|
245
|
-
const accounts = fediAccount({ dbName: 'x', fetch: acctStub, storage: undefined });
|
|
246
|
-
const kwame = await accounts.describe('@kwame@masto.example').catch((e) => ({ error: e.message }));
|
|
247
|
-
if (kwame.kind !== 'mastodon' || kwame.host !== 'masto.example') { fail(`a Mastodon address was not recognised: ${JSON.stringify(kwame)}`); bad++; }
|
|
248
|
-
const mei = await accounts.describe('@mei@front.example').catch((e) => ({ error: e.message }));
|
|
249
|
-
if (mei.kind !== 'pod' || mei.root !== 'https://mei.pod.example/anything/' || mei.issuer !== 'https://pod.example/') { fail(`a pod address was not recognised: ${JSON.stringify(mei)}`); bad++; }
|
|
250
|
-
// The Mastodon account acts through its server's API.
|
|
251
|
-
const store = { getItem: (k) => store.m.get(k) ?? null, setItem: (k, v) => store.m.set(k, String(v)), removeItem: (k) => store.m.delete(k), m: new Map() };
|
|
252
|
-
store.setItem('x:masto:account', JSON.stringify({ host: 'masto.example', token: 't', handle: '@kwame@masto.example', name: 'Kwame', actor: 'https://masto.example/@kwame' }));
|
|
253
|
-
const me = await fediAccount({ dbName: 'x', fetch: acctStub, storage: store }).current();
|
|
254
|
-
const posted = await me.post({ text: 'hello' }).catch((e) => ({ error: e.message }));
|
|
255
|
-
const tl = await me.timeline().catch((e) => ({ error: e.message }));
|
|
256
|
-
const followed = await me.follow('@aisha@masto.example').catch((e) => ({ error: e.message }));
|
|
257
|
-
if (me.kind !== 'mastodon' || me.notice !== null || posted.id !== '77' || tl[0]?.author?.handle !== '@aisha@masto.example' || followed.followed !== '@aisha@masto.example') {
|
|
258
|
-
fail(`the Mastodon account did not act through its API: ${JSON.stringify({ posted, tl, followed })}`); bad++;
|
|
259
|
-
}
|
|
260
|
-
// The pod account acts through its outbox door and reads its own records;
|
|
261
|
-
// a sealed key on the pod marks it browser-based, which sets the notice.
|
|
262
|
-
const doorSeen = [];
|
|
263
|
-
const podFetch = async (url, init = {}) => {
|
|
264
|
-
doorSeen.push(`${init.method || 'GET'} ${url}`);
|
|
265
|
-
if (url === 'https://front.example/u/mei/ap/outbox') return { ok: true, status: 202, headers: { get: (k) => (k === 'location' ? 'https://front.example/u/mei/ap/notes/n1' : null) }, text: async () => '' };
|
|
266
|
-
if (url === 'https://mei.pod.example/anything/ap-state/keys.json') return { ok: true, status: 200, json: async () => ({ v: 1, kdf: 'PBKDF2-SHA256', ct: 'x', salt: 'y' }) };
|
|
267
|
-
if (url === 'https://mei.pod.example/anything/ap-state/statuses.json') return { ok: true, status: 200, json: async () => ([{ noteId: 'https://x.example/n/1', actor: 'https://x.example/u/tamara', content: '<p>one</p>', published: '2026-09-20T09:00:00Z', kind: 'timeline' }, { noteId: 'https://x.example/n/2', actor: 'https://x.example/u/tamara', content: '<p>dm</p>', published: '2026-09-20T09:30:00Z', kind: 'timeline', direct: true }]) };
|
|
268
|
-
if (url === 'https://mei.pod.example/anything/ap-state/actors.json') return { ok: true, status: 200, json: async () => ({ 'https://x.example/u/tamara': { preferredUsername: 'tamara', name: 'Tamara' } }) };
|
|
269
|
-
return { ok: false, status: 404, json: async () => ({}), text: async () => '' };
|
|
270
|
-
};
|
|
271
|
-
const session = { webId: 'https://mei.pod.example/profile/card#me', fetch: podFetch, signOut: async () => {} };
|
|
272
|
-
const stubWithCard = async (url, init = {}) => {
|
|
273
|
-
if (url === 'https://mei.pod.example/profile/card#me') return { ok: true, json: async () => ([{ '@id': url, 'https://www.w3.org/ns/activitystreams#outbox': [{ '@id': 'https://front.example/u/mei/ap/outbox' }], 'http://xmlns.com/foaf/0.1/account': [{ '@id': 'https://front.example/u/mei/ap/actor' }] }]) };
|
|
274
|
-
if (url === 'https://front.example/u/mei/ap/actor') return { ok: true, json: async () => ({ id: url, preferredUsername: 'mei', name: 'Mei', followers: 'https://front.example/u/mei/ap/followers' }) };
|
|
275
|
-
return acctStub(url, init);
|
|
276
|
-
};
|
|
277
|
-
const podStore = { getItem: (k) => podStore.m.get(k) ?? null, setItem: (k, v) => podStore.m.set(k, String(v)), removeItem: (k) => podStore.m.delete(k), m: new Map() };
|
|
278
|
-
// Reach the pod account the way resume() would, with the session handed in.
|
|
279
|
-
const meiAcct = await fediAccount({ dbName: 'x', fetch: stubWithCard, storage: podStore })
|
|
280
|
-
._podAccount(session, { handle: '@mei@front.example', actor: 'https://front.example/u/mei/ap/actor', root: 'https://mei.pod.example/anything/' });
|
|
281
|
-
const p2 = await meiAcct.post({ text: 'hello <b>' }).catch((e) => ({ error: e.message }));
|
|
282
|
-
const t2 = await meiAcct.timeline().catch((e) => ({ error: e.message }));
|
|
283
|
-
const f2 = await meiAcct.follow('@tamara@x.example').catch((e) => ({ error: e.message }));
|
|
284
|
-
const doorBodies = doorSeen.filter((s) => s.startsWith('POST https://front.example/u/mei/ap/outbox')).length;
|
|
285
|
-
if (meiAcct.kind !== 'pod' || meiAcct.notice !== BROWSER_ACCOUNT_NOTICE || !p2.queued || p2.id !== 'https://front.example/u/mei/ap/notes/n1'
|
|
286
|
-
|| t2.length !== 1 || t2[0].author.handle !== '@tamara@x.example' || !f2.queued || doorBodies !== 2) {
|
|
287
|
-
fail(`the pod account did not act through its door: ${JSON.stringify({ notice: meiAcct.notice, p2, t2, f2, doorBodies })}`); bad++;
|
|
288
|
-
}
|
|
289
186
|
const bound = code(read(path.join(root, 'web/app/oidc-session.mjs')));
|
|
290
|
-
if (!/dbName:\s*'fedipod-oidc'/.test(bound))
|
|
291
|
-
|
|
187
|
+
if (!/dbName:\s*'fedipod-oidc'/.test(bound)) fail("web/app/oidc-session.mjs must bind dbName 'fedipod-oidc' — every signed-in browser holds its session under that name");
|
|
188
|
+
else pass("the app binds the session library to its own database name");
|
|
292
189
|
}
|
|
293
190
|
|
|
294
191
|
// ---- 9. core is a library: nothing in it reaches the DeviceAgent, and every entry loads alone ----
|
package/scripts/check-sizes.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// check-sizes.mjs — no runtime source file over 1,000 lines. The split of
|
|
3
3
|
// 2026-09-11 made every module readable one part at a time; this keeps it so.
|
|
4
|
-
// Run by `npm test
|
|
4
|
+
// Run by `npm test`, and on the forum's and the Server's sources (named as
|
|
5
|
+
// arguments) by scripts/test-dependents.mjs. Exits 1 naming the offenders.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/check-sizes.mjs [extra source folder ...]
|
|
5
8
|
import fs from 'node:fs';
|
|
6
9
|
import path from 'node:path';
|
|
7
10
|
import { fileURLToPath } from 'node:url';
|
|
8
11
|
|
|
9
12
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
13
|
const LIMIT = 1000;
|
|
11
|
-
const ROOTS = ['lib', 'bin', 'web/app', 'web/front', 'web/admin', 'netlify/functions',
|
|
14
|
+
const ROOTS = ['lib', 'bin', 'web/app', 'web/front', 'web/admin', 'netlify/functions', ...process.argv.slice(2).map(d => path.resolve(d))];
|
|
12
15
|
const FILES = ['run-agent.mjs'];
|
|
13
16
|
const SKIP = /\/(dist|site|node_modules|vendor|client|shims)\//u;
|
|
14
17
|
const EXT = /\.(mjs|js|ts)$/u;
|
|
@@ -21,7 +24,7 @@ const walk = (dir, out = []) => {
|
|
|
21
24
|
}
|
|
22
25
|
return out;
|
|
23
26
|
};
|
|
24
|
-
const files = [...FILES.map(f => path.join(root, f)), ...ROOTS.flatMap(r => (fs.existsSync(path.
|
|
27
|
+
const files = [...FILES.map(f => path.join(root, f)), ...ROOTS.flatMap(r => (fs.existsSync(path.resolve(root, r)) ? walk(path.resolve(root, r)) : []))];
|
|
25
28
|
const over = files
|
|
26
29
|
.map(f => ({ f: path.relative(root, f), n: fs.readFileSync(f, 'utf8').split('\n').length }))
|
|
27
30
|
.filter(x => x.n > LIMIT)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// dependents.mjs — the packages built on fedipod, each in a repository of its
|
|
2
|
+
// own, and where this machine keeps each one. A checkout beside FediPod
|
|
3
|
+
// (../fedipod-bb and so on) is used as it stands, edits and all; failing that,
|
|
4
|
+
// a shallow clone of its main branch under .dependents/, which is what CI and
|
|
5
|
+
// anyone else's clone get.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
|
|
13
|
+
// `fedipod`: whether its tests import fedipod, and so run against this checkout.
|
|
14
|
+
// `sizes`: its source folder, held to FediPod's size limit.
|
|
15
|
+
export const DEPENDENTS = {
|
|
16
|
+
'fedipod-server': {
|
|
17
|
+
repo: 'https://github.com/jeff-zucker/fedipod-server.git', fedipod: true, sizes: 'src',
|
|
18
|
+
tests: [['npm', 'test'], ['npm', 'run', 'test:e2e'], ['npm', 'run', 'test:e2e:suffix']],
|
|
19
|
+
},
|
|
20
|
+
'fedipod-bb': {
|
|
21
|
+
repo: 'https://github.com/jeff-zucker/fedipod-bb.git', fedipod: true, sizes: 'src',
|
|
22
|
+
tests: [['npm', 'test']],
|
|
23
|
+
},
|
|
24
|
+
'fediverse-session': {
|
|
25
|
+
repo: 'https://github.com/jeff-zucker/fediverse-session.git', fedipod: false,
|
|
26
|
+
tests: [['npm', 'test']],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const nameOf = (dir) => {
|
|
31
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name; } catch { return null; }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** The folder holding this dependent: the checkout beside FediPod, else .dependents/<name>. */
|
|
35
|
+
export function dependentDir(name) {
|
|
36
|
+
const beside = path.resolve(root, '..', name);
|
|
37
|
+
return nameOf(beside) === name ? beside : path.join(root, '.dependents', name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The dependent's folder, cloned from its repository first when there is none. */
|
|
41
|
+
export function fetchDependent(name, { log = console.log } = {}) {
|
|
42
|
+
const dir = dependentDir(name);
|
|
43
|
+
if (nameOf(dir) === name) return dir;
|
|
44
|
+
log(`dependents: cloning ${DEPENDENTS[name].repo} into ${path.relative(root, dir)}`);
|
|
45
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
46
|
+
execFileSync('git', ['clone', '--quiet', '--depth', '1', DEPENDENTS[name].repo, dir], { stdio: 'inherit' });
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Its packages installed, when it has any and they are not there yet. */
|
|
51
|
+
export function installDependent(dir, { log = console.log } = {}) {
|
|
52
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
53
|
+
const wants = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).length > 0;
|
|
54
|
+
if (!wants || fs.existsSync(path.join(dir, 'node_modules'))) return;
|
|
55
|
+
const lock = fs.existsSync(path.join(dir, 'package-lock.json'));
|
|
56
|
+
log(`dependents: npm ${lock ? 'ci' : 'install'} in ${dir}`);
|
|
57
|
+
execFileSync('npm', [lock ? 'ci' : 'install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'inherit' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Its `fedipod` pointed at this checkout, replacing whatever npm put there. */
|
|
61
|
+
export function linkFedipod(dir) {
|
|
62
|
+
const link = path.join(dir, 'node_modules', 'fedipod');
|
|
63
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
|
|
64
|
+
if (real(link) === real(root)) return;
|
|
65
|
+
fs.rmSync(link, { recursive: true, force: true });
|
|
66
|
+
fs.mkdirSync(path.dirname(link), { recursive: true });
|
|
67
|
+
fs.symlinkSync(root, link, 'dir');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The dependent made ready to run against this checkout. */
|
|
71
|
+
export function readyDependent(name, opts = {}) {
|
|
72
|
+
const dir = fetchDependent(name, opts);
|
|
73
|
+
installDependent(dir, opts);
|
|
74
|
+
if (DEPENDENTS[name].fedipod) linkFedipod(dir);
|
|
75
|
+
return dir;
|
|
76
|
+
}
|
package/scripts/stage-site.mjs
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
// source is the hardest kind of thing to see in the browser build.
|
|
14
14
|
import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
|
+
import { readyDependent } from './dependents.mjs';
|
|
16
17
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
18
|
const site = path.join(root, 'web/app/site');
|
|
18
19
|
fs.rmSync(site, { recursive: true, force: true }); fs.mkdirSync(path.join(site, 'app'), { recursive: true });
|
|
19
|
-
const cp = (from, to) => fs.copyFileSync(path.
|
|
20
|
+
const cp = (from, to) => fs.copyFileSync(path.resolve(root, from), path.join(site, to));
|
|
20
21
|
cp('web/app/index.html', 'index.html');
|
|
21
22
|
cp('web/app/dist/boot.js', 'boot.js');
|
|
22
23
|
cp('web/app/dist/sw.js', 'sw.js');
|
|
@@ -267,21 +268,22 @@ fs.writeFileSync(path.join(site, '_redirects'), [
|
|
|
267
268
|
// own /u/<handle>/ or a pod named outright. Its script is a module file of
|
|
268
269
|
// its own, so it is served under the same `script-src 'self'` as the app.
|
|
269
270
|
fs.mkdirSync(path.join(site, 'bb'), { recursive: true });
|
|
270
|
-
for (const f of ['index.html', 'bb.js', 'read.mjs', 'masto.mjs', 'pod.mjs', 'markdown.mjs', 'seen.mjs', 'mine.mjs', 'private.mjs', 'oidc-session.mjs']) cp(`
|
|
271
|
+
for (const f of ['index.html', 'bb.js', 'read.mjs', 'masto.mjs', 'pod.mjs', 'markdown.mjs', 'seen.mjs', 'mine.mjs', 'private.mjs', 'oidc-session.mjs']) cp(`node_modules/fedipod-bb/site/${f}`, `bb/${f}`);
|
|
271
272
|
cp('web/admin/tokens.css', 'bb/tokens.css'); // the site's shared palette, size and family
|
|
272
|
-
cp('
|
|
273
|
-
// The account library's demo page, with the library beside it, under /demo
|
|
273
|
+
cp('node_modules/fediverse-session/oidc-session.mjs', 'bb/solid-oidc-session.mjs'); // signing in to a pod: the library the forum's binding names
|
|
274
|
+
// The account library's demo page, with the library beside it, under /demo/:
|
|
275
|
+
// both from the library's own repository, since the package carries no demo.
|
|
276
|
+
const sessionRepo = readyDependent('fediverse-session');
|
|
274
277
|
fs.mkdirSync(path.join(site, 'demo'), { recursive: true });
|
|
275
|
-
for (const f of ['fedi-account.mjs', 'fedi-login.mjs', 'oidc-session.mjs']) cp(
|
|
278
|
+
for (const f of ['fedi-account.mjs', 'fedi-login.mjs', 'oidc-session.mjs']) cp(path.join(sessionRepo, f), `demo/${f}`);
|
|
276
279
|
cp('web/admin/tokens.css', 'demo/tokens.css');
|
|
277
280
|
// The page a Mastodon app sends its person to (lib/gateway/masto-gateway.mjs),
|
|
278
281
|
// with the pod sign-in library and the palette beside it, as the demo has.
|
|
279
282
|
fs.mkdirSync(path.join(site, 'app-signin'), { recursive: true });
|
|
280
283
|
for (const f of ['index.html', 'app-signin.mjs']) cp(`web/app-signin/${f}`, `app-signin/${f}`);
|
|
281
|
-
for (const f of ['fedi-login.mjs', 'oidc-session.mjs']) cp(`
|
|
284
|
+
for (const f of ['fedi-login.mjs', 'oidc-session.mjs']) cp(`node_modules/fediverse-session/${f}`, `app-signin/${f}`);
|
|
282
285
|
cp('web/admin/tokens.css', 'app-signin/tokens.css');
|
|
283
|
-
|
|
284
|
-
fs.readFileSync(path.join(root, 'lib/session/demo.html'), 'utf8').replace('../../web/admin/tokens.css', './tokens.css'));
|
|
286
|
+
cp(path.join(sessionRepo, 'demo.html'), 'demo/index.html');
|
|
285
287
|
// Each file the forum page loads is named with a hash of its content. A
|
|
286
288
|
// browser holding the last build cannot serve half of it back: the page is
|
|
287
289
|
// revalidated (no-cache below) and every url under it changes with its bytes.
|
package/scripts/start.mjs
CHANGED
|
@@ -7,7 +7,7 @@ console.log(`FediPod is not started from this directory.
|
|
|
7
7
|
In a browser: open a Gateway such as https://fedipod.net and create an
|
|
8
8
|
account. Nothing is installed.
|
|
9
9
|
A Gateway of your own: deploy this repository to Netlify — netlify/README.md
|
|
10
|
-
A pod server with FediPod built in:
|
|
11
|
-
A forum:
|
|
10
|
+
A pod server with FediPod built in: https://github.com/jeff-zucker/fedipod-server
|
|
11
|
+
A forum: https://github.com/jeff-zucker/fedipod-bb, or "A forum" on a
|
|
12
12
|
Gateway's sign-up page
|
|
13
13
|
`);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// test-dependents.mjs — every package built on fedipod, tested against this
|
|
3
|
+
// checkout: the Server, the forum and the session library, each from the
|
|
4
|
+
// folder dependents.mjs finds it in. Run by `npm test` before the browser rigs,
|
|
5
|
+
// which boot their scratch pod server from the Server's folder. A dependent
|
|
6
|
+
// that cannot be fetched, installed or run fails the whole run; none is ever
|
|
7
|
+
// skipped. Exits 1, the failures named in its last lines.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/test-dependents.mjs [name ...]
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync, execFileSync } from 'node:child_process';
|
|
12
|
+
import { DEPENDENTS, readyDependent, root } from './dependents.mjs';
|
|
13
|
+
|
|
14
|
+
const names = process.argv.slice(2).length ? process.argv.slice(2) : Object.keys(DEPENDENTS);
|
|
15
|
+
const failed = [];
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
const spec = DEPENDENTS[name];
|
|
18
|
+
if (!spec) { failed.push(`${name}: not a dependent`); continue; }
|
|
19
|
+
let dir;
|
|
20
|
+
try { dir = readyDependent(name); }
|
|
21
|
+
catch (e) { failed.push(`${name}: could not be made ready (${e.message})`); continue; }
|
|
22
|
+
console.log(`\n== ${name} (${dir}) against this checkout`);
|
|
23
|
+
for (const cmd of spec.tests) {
|
|
24
|
+
const r = spawnSync(cmd[0], cmd.slice(1), { cwd: dir, stdio: 'inherit' });
|
|
25
|
+
if (r.status !== 0) failed.push(`${name}: \`${cmd.join(' ')}\` failed (exit ${r.status ?? r.signal})`);
|
|
26
|
+
}
|
|
27
|
+
if (spec.sizes) {
|
|
28
|
+
try { execFileSync(process.execPath, [path.join(root, 'scripts/check-sizes.mjs'), path.join(dir, spec.sizes)], { stdio: 'inherit' }); }
|
|
29
|
+
catch { failed.push(`${name}: a source file is over the size limit`); }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (failed.length) {
|
|
33
|
+
console.error(`\ntest-dependents: ${failed.length} FAILED`);
|
|
34
|
+
for (const f of failed) console.error(` ${f}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
console.log(`\ntest-dependents: ${names.join(', ')} pass against this checkout`);
|