@sempods/apps 0.0.0-stage → 0.7.0-dev.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/CHANGELOG.md +205 -0
- package/LICENSE +19 -0
- package/README.md +50 -2
- package/bin/sempods-apps.mjs +36 -0
- package/instructions/app-workflow.md +210 -0
- package/instructions/publish.md +148 -0
- package/instructions/setup.md +112 -0
- package/package.json +59 -3
- package/scripts/build-site.mjs +181 -0
- package/scripts/check.mjs +260 -0
- package/scripts/configure-site.mjs +118 -0
- package/scripts/create.mjs +83 -0
- package/scripts/dev.mjs +19 -0
- package/scripts/lib/apps.mjs +164 -0
- package/scripts/lib/files.mjs +57 -0
- package/scripts/lib/generate.mjs +175 -0
- package/scripts/lib/html.mjs +3 -0
- package/scripts/lib/json.mjs +42 -0
- package/scripts/lib/merge.mjs +159 -0
- package/scripts/lib/overview.mjs +105 -0
- package/scripts/lib/paths.mjs +33 -0
- package/scripts/lib/pnpm.mjs +30 -0
- package/scripts/lib/sdk.mjs +45 -0
- package/scripts/lib/shared.mjs +221 -0
- package/scripts/migrate.mjs +550 -0
- package/scripts/new-app.mjs +166 -0
- package/scripts/regenerate.mjs +41 -0
- package/scripts/sdk-update.mjs +220 -0
- package/scripts/update.mjs +152 -0
- package/shared/files/0221b70340152ba3cc615342254603b29b89c4d7d944d7b3b3e647873bdcefa0 +11 -0
- package/shared/files/1c29eed0a0297c742ca5814de9081ff8efce3b2da5d9e1bdf90a403873da4aa9 +61 -0
- package/shared/files/31ede0d1a035d8531100ebfd751510c772ab0871309a0b9752a77fb96187eb53 +25 -0
- package/shared/files/393b09ecd18889d9e1fdf2a6df12d1f6c193fc7fcd89ba808ad94df521bad8db +41 -0
- package/shared/files/4660ca1887261ae658c7f587190e2b71ac91d15b1b6c4cf04dec823fdd0da654 +22 -0
- package/shared/files/528d7544990228d6d66fa62423775338fe1716326fc68371fb2665aee86e39b0 +6 -0
- package/shared/files/676e6bf33b44044fb4c62c9fe5441729bd47f2d5077ff904e57a4a1b658859ac +38 -0
- package/shared/files/8b7567319e18694aeca8722107281a760e3100239c6d0f548acb71e1806fa172 +28 -0
- package/shared/files/9945f9bd573e5af8aad2e65cc8b4284fbb1d7e8b96c4efdd42eeb49132fb4327 +4 -0
- package/shared/files/b911698dc3cf3227d34d0ad6727f1c4408791203c9665de99ab959331a1fb07d +1 -0
- package/shared/files/c2a4b026a87f6c8e7473e7fc456c6020b63c7e675087f6b58013ed5163c2f6ee +23 -0
- package/shared/files/d3ca3a13c6524a2ee9382977ddc0539790813225f366306024a58c957a4716ee +11 -0
- package/shared/files/e13474647ebaf26d042d76f4917203d209a2b14c0d2af8d12139ea328530823e +78 -0
- package/shared/files/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +0 -0
- package/shared/files/f2fb601d5ebc4ca72274ac9d3fcf7b7a96e27958bb92cd065a9165b501ee94d7 +57 -0
- package/shared/files/f9e2544290ee8418484e47194444ee58c2ea675682871ae8a8727d1edb17978e +5 -0
- package/shared/files/ff9f774d9732fdc7d704ef37ddc40ef507a1bab7ee3c1db5b1a6a8f26e6e4b37 +35 -0
- package/shared/snapshot.json +25 -0
- package/skeleton/app/.oxlintrc.json +14 -0
- package/skeleton/app/NOTES.md +19 -0
- package/skeleton/app/index.html +16 -0
- package/skeleton/app/package.json +28 -0
- package/skeleton/app/public/favicon.svg +1 -0
- package/skeleton/app/public/icon-192.png +0 -0
- package/skeleton/app/public/icon-512.png +0 -0
- package/skeleton/app/src/App.tsx +73 -0
- package/skeleton/app/src/NewVersionNotice.tsx +63 -0
- package/skeleton/app/src/index.css +37 -0
- package/skeleton/app/src/main.tsx +18 -0
- package/skeleton/app/src/pwa.ts +18 -0
- package/skeleton/app/src/start.test.ts +15 -0
- package/skeleton/app/src/start.ts +9 -0
- package/skeleton/app/tsconfig.app.json +27 -0
- package/skeleton/app/tsconfig.json +8 -0
- package/skeleton/app/tsconfig.node.json +23 -0
- package/skeleton/app/tsconfig.test.json +10 -0
- package/skeleton/app/vite.config.ts +13 -0
- package/skills/app-workflow/SKILL.md +22 -0
- package/skills/update/SKILL.md +69 -0
- package/update-policy.json +35 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Publish the apps
|
|
2
|
+
|
|
3
|
+
All apps of the repository are published together as one static site. Each
|
|
4
|
+
app lives under its own path (`https://<host>/<id>/`), and an overview at `/`
|
|
5
|
+
links them. Read this guide before the first deployment, together with the
|
|
6
|
+
installed SDK's `deployment.md` and `pwa.md`
|
|
7
|
+
(`node_modules/@sempods/app-sdk/docs/`).
|
|
8
|
+
|
|
9
|
+
## 1. Choose the addresses once
|
|
10
|
+
|
|
11
|
+
Ask the owner for:
|
|
12
|
+
|
|
13
|
+
- **the published origin** (production), for example `https://apps.example.org`
|
|
14
|
+
or `https://<site>.netlify.app`. It becomes part of every app's identity,
|
|
15
|
+
`did:web:<host>:<id>`, with the callback `https://<host>/<id>/callback`.
|
|
16
|
+
Changing it later changes every identity: every sign-in and Pod grant must be
|
|
17
|
+
made again. Confirm that the owner controls this address;
|
|
18
|
+
- **a fixed preview address**, optional: an address that always shows the
|
|
19
|
+
latest deploy of one branch, for example a branch named `preview`. It gets its
|
|
20
|
+
own identity and should use a test Pod or a test context.
|
|
21
|
+
|
|
22
|
+
Record them with:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm run configure-site --production https://apps.example.org \
|
|
26
|
+
--preview https://preview--apps.netlify.app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The command writes `site` in `apps.json` and regenerates every app's
|
|
30
|
+
configuration. Local development is unchanged: `pnpm run dev <id>`, plain
|
|
31
|
+
builds and tests keep the local identity.
|
|
32
|
+
|
|
33
|
+
## 2. Build the site
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm run build-site # production
|
|
37
|
+
pnpm run build-site --profile preview # the fixed preview address
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It builds every app into `site-dist/<id>/` with the published identity and
|
|
41
|
+
writes the overview, `did.json` per app (`/<id>/did.json`, needed by some
|
|
42
|
+
Pods), the SDK's licence notices, `<id>/callback.html` for each callback and a
|
|
43
|
+
top-level `404.html` for unknown paths. The layout is the same for every host.
|
|
44
|
+
`site-dist/` is build output; do not commit it. `apps/<id>/dist` is not
|
|
45
|
+
touched, so `pnpm run build` and `pnpm run preview` in an app keep the local
|
|
46
|
+
identity.
|
|
47
|
+
|
|
48
|
+
A conforming sempods Pod checks the `did:web` identity against the callback
|
|
49
|
+
address and fetches no DID document. `did.json` serves Pods that also require
|
|
50
|
+
one (step 5 of the deployment check).
|
|
51
|
+
|
|
52
|
+
A build only works on the origin it was made for: opened anywhere else, the SDK
|
|
53
|
+
stops with a configuration error. Previews on other addresses, such as one per
|
|
54
|
+
pull request or per commit, therefore show no working sign-in. Turn them off
|
|
55
|
+
or treat them as unusable.
|
|
56
|
+
|
|
57
|
+
## 3. What the host must provide
|
|
58
|
+
|
|
59
|
+
Every host, whatever it is:
|
|
60
|
+
|
|
61
|
+
- serves `site-dist/` as static files over HTTPS, with Node from
|
|
62
|
+
`.node-version` and pnpm from `packageManager` when it builds;
|
|
63
|
+
- answers `/<id>/` and `/<id>/callback?code=…&state=…` with a first response
|
|
64
|
+
of `200`, without a redirect (`Location` header), with path and query
|
|
65
|
+
unchanged. The SDK recognises the callback only at its exact path;
|
|
66
|
+
- answers `/<id>/did.json` with the JSON document, not with an app page;
|
|
67
|
+
- does not rewrite every path to an `index.html`, which would also replace
|
|
68
|
+
scripts, `sw.js` and `did.json`. Unknown paths show the not-found page;
|
|
69
|
+
- keeps the browser's default referrer policy or a stricter one, loads no
|
|
70
|
+
third-party resources on the callback route, and keeps full callback URLs
|
|
71
|
+
out of host and CDN logs.
|
|
72
|
+
|
|
73
|
+
Build command `pnpm run build-site`, publish directory `site-dist`.
|
|
74
|
+
|
|
75
|
+
Do not add rewrite rules for `/<id>/*`: the files already answer every path,
|
|
76
|
+
and a rule can replace them.
|
|
77
|
+
|
|
78
|
+
### Netlify (tested)
|
|
79
|
+
|
|
80
|
+
Netlify serves `/<id>/callback` from `callback.html` with `200` and the query
|
|
81
|
+
intact, and `404.html` for unknown paths; it needs no `_redirects`. Write this
|
|
82
|
+
`netlify.toml` in the repository root when the owner asks for Netlify (the
|
|
83
|
+
starter ships none):
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
[build]
|
|
87
|
+
command = "pnpm run build-site"
|
|
88
|
+
publish = "site-dist"
|
|
89
|
+
|
|
90
|
+
# The branch whose deploys form the fixed preview address.
|
|
91
|
+
[context.preview]
|
|
92
|
+
command = "pnpm run build-site --profile preview"
|
|
93
|
+
|
|
94
|
+
# Pull request previews have their own addresses and no identity.
|
|
95
|
+
[context.deploy-preview]
|
|
96
|
+
command = "echo 'Deploy previews cannot sign in; push to the preview branch.' && exit 1"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Connect the repository to a Netlify site. For a preview, enable branch deploys
|
|
100
|
+
for the `preview` branch: its address is `https://preview--<site>.netlify.app`.
|
|
101
|
+
|
|
102
|
+
### Cloudflare Pages (not tested by sempods apps)
|
|
103
|
+
|
|
104
|
+
Pages serves `<id>/callback.html` at `/<id>/callback`. Without the top-level
|
|
105
|
+
`404.html` it would serve the overview for every unknown path.
|
|
106
|
+
|
|
107
|
+
In the project's build settings: build command `pnpm run build-site`, build
|
|
108
|
+
output directory `site-dist`. For the fixed preview address, set the variable
|
|
109
|
+
`SEMPODS_SITE_PROFILE=preview` for the Preview environment. The branch alias
|
|
110
|
+
`https://preview.<project>.pages.dev` then shows the `preview` branch. Commit
|
|
111
|
+
addresses (`https://<hash>.<project>.pages.dev`) have no working sign-in.
|
|
112
|
+
|
|
113
|
+
### Cloudflare Workers and other hosts
|
|
114
|
+
|
|
115
|
+
The layout suits hosts that serve `/<path>` from `<path>.html`. Cloudflare
|
|
116
|
+
Workers static assets do this by default, but the tooling has no recipe yet
|
|
117
|
+
for their asset and deploy configuration. For any other host, check the requirements above before
|
|
118
|
+
the first sign-in.
|
|
119
|
+
|
|
120
|
+
### Secrets
|
|
121
|
+
|
|
122
|
+
The apps need no secret: their identity and callback are public. Keep hosting
|
|
123
|
+
and deployment credentials, such as a Netlify or Cloudflare token, in the
|
|
124
|
+
host's or CI's secret store. Everything a build puts into an app is public,
|
|
125
|
+
including `VITE_*` variables and values from `.env.local`; never put a token or
|
|
126
|
+
key there. See "Keep public configuration and credentials separate" in the
|
|
127
|
+
SDK's `deployment.md`.
|
|
128
|
+
|
|
129
|
+
## 4. Check the deployment
|
|
130
|
+
|
|
131
|
+
1. For each app and both addresses:
|
|
132
|
+
- `curl -s -o /dev/null -D - 'https://<host>/<id>/callback?code=x&state=y'`
|
|
133
|
+
(a GET, as the browser sends it) returns `200` and no `Location`;
|
|
134
|
+
- `curl -s 'https://<host>/<id>/did.json'` returns the JSON with
|
|
135
|
+
`"id": "did:web:<host>:<id>"`.
|
|
136
|
+
2. In a fresh browser profile, the owner signs in to each app on the
|
|
137
|
+
published site, grants only the intended context, saves something and
|
|
138
|
+
reloads. Never ask for credentials or tokens.
|
|
139
|
+
3. On the preview address, sign in with its own identity against a test
|
|
140
|
+
context.
|
|
141
|
+
4. In the host's and any CDN's settings, turn off or redact query strings and
|
|
142
|
+
`Referer` headers in request logs, so callback queries are not kept.
|
|
143
|
+
5. If the Pod rejects the app before or at its consent screen, read
|
|
144
|
+
`deployment.md` on Pods that require a DID document or an allow-list.
|
|
145
|
+
Falling back to another identity is not a fix.
|
|
146
|
+
|
|
147
|
+
Record the published addresses, identities, what was checked and what was not
|
|
148
|
+
in each app's `NOTES.md`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Set up an apps repository
|
|
2
|
+
|
|
3
|
+
This is the assistant's setup procedure for a repository created from the
|
|
4
|
+
sempods apps starter. The repository's `INIT.md` holds the bootstrap steps and
|
|
5
|
+
the setup record; record progress only there. Follow the repository's
|
|
6
|
+
`AGENTS.md`.
|
|
7
|
+
|
|
8
|
+
## Check where you are
|
|
9
|
+
|
|
10
|
+
Inspect the worktree and the setup record before changing anything. The
|
|
11
|
+
repository needs `package.json`, `pnpm-workspace.yaml`, `.node-version`,
|
|
12
|
+
`apps.json` and `.sempods-baseline.json`; after `pnpm install`, the tooling
|
|
13
|
+
package `node_modules/@sempods/apps` and the SDK's reference
|
|
14
|
+
`node_modules/@sempods/app-sdk/docs/ai-app-builder.md`. If these are missing,
|
|
15
|
+
report that the repository is incomplete. Do not invent scripts, configuration
|
|
16
|
+
or a replacement SDK reference to work around it. Record the package and
|
|
17
|
+
version from `.sempods-baseline.json` in the setup record; do not edit that
|
|
18
|
+
file or anything under `node_modules`.
|
|
19
|
+
|
|
20
|
+
## Choose the first interaction
|
|
21
|
+
|
|
22
|
+
Use the request and recorded answers. Ask only for missing information needed
|
|
23
|
+
for the first useful interaction. Use the requested UI language, or the owner's
|
|
24
|
+
language when supported (`de` or `en`); otherwise use `en` and tell the owner.
|
|
25
|
+
Choose a short app ID from the idea, such as `konsum`, and tell the owner; the
|
|
26
|
+
generator validates allowed IDs and reserved names. Record choices and
|
|
27
|
+
progress as they become known.
|
|
28
|
+
|
|
29
|
+
A name, GitHub review identity and the version PR choice are not needed to
|
|
30
|
+
try an app. Do not ask for them before the first local interaction. A missing
|
|
31
|
+
Pod also allows UI work; record Pod testing as pending. Before accessing a Pod,
|
|
32
|
+
use the owner's full test Pod URL and a dedicated context, with synthetic data.
|
|
33
|
+
|
|
34
|
+
## Create the first app and open it
|
|
35
|
+
|
|
36
|
+
From the repository root, after the bootstrap in `INIT.md`:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm run new-app konsum --title "Einkaufsliste" --language de
|
|
40
|
+
pnpm run dev konsum
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Replace the example ID, title and language with the recorded choices. PWA is on
|
|
44
|
+
by default; `--no-pwa` is available if the owner chooses it when creating an
|
|
45
|
+
app. The dev server is long-running: keep it in a separate terminal/session.
|
|
46
|
+
Open the exact URL it reports; `apps.json` records the assigned `devPort` and
|
|
47
|
+
`path`. Do not switch hostnames or ports during sign-in.
|
|
48
|
+
|
|
49
|
+
Before calling `new-app`, check both the manifest and `apps/<id>/`. If the
|
|
50
|
+
recorded first app already exists consistently in both, reuse it and its notes;
|
|
51
|
+
skip generation. If only one exists, or the existing app may be unrelated, stop
|
|
52
|
+
that step and explain the mismatch. Never delete the folder, rewrite the
|
|
53
|
+
manifest or choose another ID silently to make setup pass. A failed command
|
|
54
|
+
leaves setup in progress; diagnose it before proceeding.
|
|
55
|
+
|
|
56
|
+
Continue with the [app workflow](app-workflow.md) for the first useful
|
|
57
|
+
interaction. Once the owner can try the screen, ask once whether they have a
|
|
58
|
+
test Pod for trying it with data, unless the setup record answers that. If so,
|
|
59
|
+
let the user sign in in the browser and select their test context; otherwise
|
|
60
|
+
record Pod testing as pending. Run `pnpm run check` in another terminal and
|
|
61
|
+
report its result.
|
|
62
|
+
|
|
63
|
+
## Choose repository preferences (optional)
|
|
64
|
+
|
|
65
|
+
After the owner can try the first interaction, offer to set a GitHub review
|
|
66
|
+
owner and to enable version PRs for the SDK and the tooling. Use choices
|
|
67
|
+
already given. If the owner has not chosen these preferences, leave them
|
|
68
|
+
deferred; do not wait for an answer to finish local setup. If the owner wants
|
|
69
|
+
review rules, ask for the GitHub user/team unless it is known; do not infer a
|
|
70
|
+
handle from a display name. Write `.github/CODEOWNERS` for it, preserving
|
|
71
|
+
entries the owner already has. Record the owner's name if provided; it is not
|
|
72
|
+
a prerequisite for setup.
|
|
73
|
+
|
|
74
|
+
Record each preference in its setup record field and continue app work:
|
|
75
|
+
`deferred` for an open choice, otherwise the choice and what you verified, for
|
|
76
|
+
example `enabled; on the default branch` or `pending; not yet on the default
|
|
77
|
+
branch`. Do not repeat deferred questions on a resumed run unless the owner
|
|
78
|
+
asks to revisit them.
|
|
79
|
+
|
|
80
|
+
## Enable sempods version PRs
|
|
81
|
+
|
|
82
|
+
Dependabot opens version PRs for the repository's dependencies, but not for
|
|
83
|
+
the sempods packages until the owner opts in. In `.github/dependabot.yml`, the
|
|
84
|
+
entry between `# BEGIN OWNER SEMPODS UPDATES` and `# END OWNER SEMPODS
|
|
85
|
+
UPDATES` keeps them out. When the owner chooses version PRs, remove that entry
|
|
86
|
+
and keep the marker lines; tooling updates keep this section as the owner left
|
|
87
|
+
it. Commit the change in the owner's normal commit/PR flow. It takes effect
|
|
88
|
+
once it reaches the default branch; record it as pending until then. To turn
|
|
89
|
+
the PRs off again, restore the entry between the markers.
|
|
90
|
+
|
|
91
|
+
With the opt-in, Dependabot opens one PR that moves both SDK packages in the
|
|
92
|
+
root and every app, and a separate PR for `@sempods/apps`. An SDK PR follows
|
|
93
|
+
[Update the SDK](app-workflow.md#update-the-sdk); a tooling PR follows the
|
|
94
|
+
[update skill](../skills/update/SKILL.md). Security updates are separate: the
|
|
95
|
+
repository's security settings enable them, and they reach the sempods
|
|
96
|
+
packages with or without this opt-in. Neither needs an Actions permission or
|
|
97
|
+
a token; never ask for one.
|
|
98
|
+
|
|
99
|
+
## Finish setup
|
|
100
|
+
|
|
101
|
+
Run `pnpm run check` again. Mark setup **done** only when the chosen app starts
|
|
102
|
+
at its assigned URL, its first useful interaction is available and local checks
|
|
103
|
+
pass. Deferred review identity or version PRs do not block completion;
|
|
104
|
+
keep their status visible in the setup record. Record command results and
|
|
105
|
+
remaining gaps separately: no Pod, incomplete login or untested installed-PWA
|
|
106
|
+
behavior are not successful interoperability evidence. If setup is already
|
|
107
|
+
done, reopen the recorded app and follow its notes; do not recreate it.
|
|
108
|
+
|
|
109
|
+
SDK updates use the [app workflow](app-workflow.md#update-the-sdk). Tooling
|
|
110
|
+
updates follow the [update skill](../skills/update/SKILL.md) when the owner
|
|
111
|
+
asks for one; do not change the `@sempods/apps` version by hand. Publishing
|
|
112
|
+
follows the [publish guide](publish.md) when the owner asks for it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sempods/apps",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0-dev.1",
|
|
4
|
+
"description": "Tooling for a sempods apps repository: create, check, update and publish several sempods apps with one shared SDK version.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sempods",
|
|
7
|
+
"pod",
|
|
8
|
+
"apps",
|
|
9
|
+
"pwa",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT-0",
|
|
13
|
+
"author": "Danilo Stein",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/sempods/sempods-apps-template.git",
|
|
17
|
+
"directory": "packages/apps"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/sempods/sempods-apps-template/tree/main/packages/apps#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/sempods/sempods-apps-template/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24.15.0",
|
|
26
|
+
"pnpm": ">=11.28.2"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"sempods-apps": "bin/sempods-apps.mjs"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
"./create": "./scripts/create.mjs",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"scripts/",
|
|
38
|
+
"skeleton/",
|
|
39
|
+
"instructions/",
|
|
40
|
+
"skills/",
|
|
41
|
+
"shared/",
|
|
42
|
+
"update-policy.json",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"semver": "7.8.5",
|
|
49
|
+
"typescript": "6.0.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"markdown-extensions": "2.0.0",
|
|
53
|
+
"remark": "15.0.1",
|
|
54
|
+
"remark-validate-links": "13.1.0",
|
|
55
|
+
"unified-engine": "11.2.2"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@sempods/app-sdk": "^0.5.0",
|
|
59
|
+
"@sempods/client-sdk": "^0.5.0"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {}
|
|
6
62
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Builds every app with a published profile into its path of one static site
|
|
3
|
+
// folder, site-dist/, and writes the overview and the host's routing files.
|
|
4
|
+
// apps/<id>/dist keeps the app's local build.
|
|
5
|
+
// Usage: pnpm run build-site [--profile production|preview]
|
|
6
|
+
// (or SEMPODS_SITE_PROFILE=<profile> in the host's environment)
|
|
7
|
+
import {
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { join, relative, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { parseArgs } from 'node:util';
|
|
18
|
+
import { readApps, SITE_PROFILES, siteIdentity } from './lib/apps.mjs';
|
|
19
|
+
import { PROFILE_MODES, staleGenerated } from './lib/generate.mjs';
|
|
20
|
+
import { ifPresent, writeJson } from './lib/json.mjs';
|
|
21
|
+
import { renderNotFound, renderOverview } from './lib/overview.mjs';
|
|
22
|
+
import { repositoryRoot } from './lib/paths.mjs';
|
|
23
|
+
import { pnpm, scriptArgs } from './lib/pnpm.mjs';
|
|
24
|
+
import { SDK } from './lib/sdk.mjs';
|
|
25
|
+
|
|
26
|
+
export const SITE_DIR = 'site-dist';
|
|
27
|
+
|
|
28
|
+
/** Runs the app's own build script with a site mode into `outDir`. */
|
|
29
|
+
function appBuild(appDir, mode, outDir) {
|
|
30
|
+
// pnpm appends these to the script's final `vite build`. The output path is
|
|
31
|
+
// relative to the app, so no user directory name passes through the Windows
|
|
32
|
+
// shell (lib/pnpm.mjs).
|
|
33
|
+
const args = [
|
|
34
|
+
...['run', 'build', '--mode', mode],
|
|
35
|
+
...['--outDir', relative(appDir, outDir), '--emptyOutDir'],
|
|
36
|
+
];
|
|
37
|
+
return pnpm(args, { cwd: appDir }).status === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Paths build-site reserves in each app's output, so an app's own copies are
|
|
41
|
+
// left out: did.json (written here), host files that hosts read only at the
|
|
42
|
+
// site root (Cloudflare would serve a nested 404.html), and the callback:
|
|
43
|
+
// `callback.html` is written here, and an extensionless `callback` would
|
|
44
|
+
// answer the callback path before it.
|
|
45
|
+
const RESERVED = [
|
|
46
|
+
'did.json',
|
|
47
|
+
'_redirects',
|
|
48
|
+
'404.html',
|
|
49
|
+
'callback',
|
|
50
|
+
'callback.html',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Removes a file or directory and reports whether there was one. */
|
|
54
|
+
const removeIfPresent = (path) =>
|
|
55
|
+
ifPresent(() => rmSync(path, { recursive: true }) ?? true) ?? false;
|
|
56
|
+
|
|
57
|
+
/** Copies a file and reports whether there was one to copy. */
|
|
58
|
+
const copyIfPresent = (from, to) =>
|
|
59
|
+
ifPresent(() => copyFileSync(from, to) ?? true) ?? false;
|
|
60
|
+
|
|
61
|
+
/** The SDK's Apache-2.0 notices, which travel with the published code. */
|
|
62
|
+
function copyLicences(roots, appOut, appId) {
|
|
63
|
+
for (const pkg of SDK) {
|
|
64
|
+
const to = join(appOut, 'licenses', pkg.split('/')[1]);
|
|
65
|
+
mkdirSync(to, { recursive: true });
|
|
66
|
+
const from = roots
|
|
67
|
+
.map((base) => join(base, 'node_modules', pkg))
|
|
68
|
+
.find((dir) => copyIfPresent(join(dir, 'LICENSE'), join(to, 'LICENSE')));
|
|
69
|
+
if (!from)
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${pkg} with its LICENSE is not installed for apps/${appId}; run pnpm install.`,
|
|
72
|
+
);
|
|
73
|
+
copyIfPresent(join(from, 'NOTICE'), join(to, 'NOTICE'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The overview entry of a built app. */
|
|
78
|
+
function overviewEntry(app, outDir) {
|
|
79
|
+
const entry = {
|
|
80
|
+
id: app.id,
|
|
81
|
+
title: app.title,
|
|
82
|
+
language: app.language,
|
|
83
|
+
href: app.path,
|
|
84
|
+
};
|
|
85
|
+
if (existsSync(join(outDir, 'icon-192.png')))
|
|
86
|
+
entry.icon = `${app.path}icon-192.png`;
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Builds the site for `profile`; returns what it built and any warnings. */
|
|
91
|
+
export function buildSite(root, { profile = 'production', build = appBuild } = {}) {
|
|
92
|
+
if (!SITE_PROFILES.includes(profile))
|
|
93
|
+
throw new Error(`The profile is one of ${SITE_PROFILES.join(', ')}.`);
|
|
94
|
+
const { apps, site } = readApps(root);
|
|
95
|
+
if (!site?.[profile])
|
|
96
|
+
throw new Error(
|
|
97
|
+
`apps.json has no site.${profile}; run pnpm run configure-site first.`,
|
|
98
|
+
);
|
|
99
|
+
if (apps.length === 0) throw new Error('There are no apps to publish yet.');
|
|
100
|
+
const out = join(root, SITE_DIR);
|
|
101
|
+
rmSync(out, { recursive: true, force: true });
|
|
102
|
+
mkdirSync(out, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// A hand edit of apps.json would build one identity and publish another.
|
|
105
|
+
for (const app of apps) {
|
|
106
|
+
const stale = staleGenerated(join(root, 'apps', app.id), app, site);
|
|
107
|
+
if (stale.length > 0)
|
|
108
|
+
throw new Error(
|
|
109
|
+
`apps/${app.id}/${stale[0]} does not match apps.json; run pnpm run configure-site to regenerate it.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entries = [];
|
|
114
|
+
const warnings = [];
|
|
115
|
+
for (const app of apps) {
|
|
116
|
+
const appDir = join(root, 'apps', app.id);
|
|
117
|
+
const appOut = join(out, app.id);
|
|
118
|
+
if (!build(appDir, PROFILE_MODES[profile], appOut))
|
|
119
|
+
throw new Error(`apps/${app.id}: the build failed.`);
|
|
120
|
+
const index = join(appOut, 'index.html');
|
|
121
|
+
const html = ifPresent(() => readFileSync(index, 'utf8'));
|
|
122
|
+
if (html === undefined)
|
|
123
|
+
throw new Error(
|
|
124
|
+
`apps/${app.id}: the build wrote no index.html to ${SITE_DIR}/${app.id}; the app's build script must end with vite build.`,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
for (const file of RESERVED)
|
|
128
|
+
if (removeIfPresent(join(appOut, file)))
|
|
129
|
+
warnings.push(
|
|
130
|
+
`apps/${app.id}/public/${file} is not published: build-site reserves this path.`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Some Pods resolve the did:web document before they accept the app.
|
|
134
|
+
const { clientId, redirectUri } = siteIdentity(site[profile], app);
|
|
135
|
+
writeJson(join(appOut, 'did.json'), {
|
|
136
|
+
'@context': 'https://www.w3.org/ns/did/v1',
|
|
137
|
+
id: clientId,
|
|
138
|
+
});
|
|
139
|
+
copyLicences([appDir, root], appOut, app.id);
|
|
140
|
+
|
|
141
|
+
// The callback must answer 200 at its exact path with its query intact.
|
|
142
|
+
// Static hosts serve <path>.html for an extensionless path; Netlify does
|
|
143
|
+
// so without a redirect.
|
|
144
|
+
const callback = new URL(redirectUri).pathname;
|
|
145
|
+
writeFileSync(join(out, `${callback.slice(1)}.html`), html);
|
|
146
|
+
|
|
147
|
+
entries.push(overviewEntry(app, appOut));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
writeFileSync(join(out, 'index.html'), renderOverview(entries));
|
|
151
|
+
// Hosts serve it for unknown paths; without it, Cloudflare Pages would serve
|
|
152
|
+
// the overview for every unknown path.
|
|
153
|
+
writeFileSync(join(out, '404.html'), renderNotFound(entries));
|
|
154
|
+
return { apps, site, warnings };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function main() {
|
|
158
|
+
const { values } = parseArgs({
|
|
159
|
+
args: scriptArgs(),
|
|
160
|
+
options: { profile: { type: 'string' } },
|
|
161
|
+
});
|
|
162
|
+
const root = repositoryRoot();
|
|
163
|
+
const profile =
|
|
164
|
+
values.profile ?? process.env.SEMPODS_SITE_PROFILE ?? 'production';
|
|
165
|
+
const { apps, site, warnings } = buildSite(root, { profile });
|
|
166
|
+
for (const warning of warnings) console.warn(`build-site: ${warning}`);
|
|
167
|
+
console.log(`\nBuilt ${SITE_DIR}/ for ${site[profile]} (${profile}):`);
|
|
168
|
+
for (const app of apps) console.log(` ${site[profile]}${app.path}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
process.argv[1] &&
|
|
173
|
+
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
174
|
+
) {
|
|
175
|
+
try {
|
|
176
|
+
main();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(`build-site: ${error.message}`);
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
}
|
|
181
|
+
}
|