@sempods/apps 0.0.0-stage → 0.7.0-dev

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/LICENSE +19 -0
  3. package/README.md +50 -2
  4. package/bin/sempods-apps.mjs +36 -0
  5. package/instructions/app-workflow.md +210 -0
  6. package/instructions/publish.md +148 -0
  7. package/instructions/setup.md +112 -0
  8. package/package.json +59 -3
  9. package/scripts/build-site.mjs +181 -0
  10. package/scripts/check.mjs +260 -0
  11. package/scripts/configure-site.mjs +118 -0
  12. package/scripts/create.mjs +83 -0
  13. package/scripts/dev.mjs +19 -0
  14. package/scripts/lib/apps.mjs +164 -0
  15. package/scripts/lib/files.mjs +57 -0
  16. package/scripts/lib/generate.mjs +175 -0
  17. package/scripts/lib/html.mjs +3 -0
  18. package/scripts/lib/json.mjs +42 -0
  19. package/scripts/lib/merge.mjs +159 -0
  20. package/scripts/lib/overview.mjs +105 -0
  21. package/scripts/lib/paths.mjs +33 -0
  22. package/scripts/lib/pnpm.mjs +30 -0
  23. package/scripts/lib/sdk.mjs +45 -0
  24. package/scripts/lib/shared.mjs +221 -0
  25. package/scripts/migrate.mjs +550 -0
  26. package/scripts/new-app.mjs +166 -0
  27. package/scripts/regenerate.mjs +41 -0
  28. package/scripts/sdk-update.mjs +220 -0
  29. package/scripts/update.mjs +152 -0
  30. package/shared/files/0221b70340152ba3cc615342254603b29b89c4d7d944d7b3b3e647873bdcefa0 +11 -0
  31. package/shared/files/1c29eed0a0297c742ca5814de9081ff8efce3b2da5d9e1bdf90a403873da4aa9 +61 -0
  32. package/shared/files/31ede0d1a035d8531100ebfd751510c772ab0871309a0b9752a77fb96187eb53 +25 -0
  33. package/shared/files/393b09ecd18889d9e1fdf2a6df12d1f6c193fc7fcd89ba808ad94df521bad8db +41 -0
  34. package/shared/files/4660ca1887261ae658c7f587190e2b71ac91d15b1b6c4cf04dec823fdd0da654 +22 -0
  35. package/shared/files/528d7544990228d6d66fa62423775338fe1716326fc68371fb2665aee86e39b0 +6 -0
  36. package/shared/files/676e6bf33b44044fb4c62c9fe5441729bd47f2d5077ff904e57a4a1b658859ac +38 -0
  37. package/shared/files/8b7567319e18694aeca8722107281a760e3100239c6d0f548acb71e1806fa172 +28 -0
  38. package/shared/files/9945f9bd573e5af8aad2e65cc8b4284fbb1d7e8b96c4efdd42eeb49132fb4327 +4 -0
  39. package/shared/files/b911698dc3cf3227d34d0ad6727f1c4408791203c9665de99ab959331a1fb07d +1 -0
  40. package/shared/files/c2a4b026a87f6c8e7473e7fc456c6020b63c7e675087f6b58013ed5163c2f6ee +23 -0
  41. package/shared/files/d3ca3a13c6524a2ee9382977ddc0539790813225f366306024a58c957a4716ee +11 -0
  42. package/shared/files/e13474647ebaf26d042d76f4917203d209a2b14c0d2af8d12139ea328530823e +78 -0
  43. package/shared/files/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +0 -0
  44. package/shared/files/f04e547b89bb933c39de2f781978b93ea8b586ac9dee50312612e2f5c40391b1 +5 -0
  45. package/shared/files/f2fb601d5ebc4ca72274ac9d3fcf7b7a96e27958bb92cd065a9165b501ee94d7 +57 -0
  46. package/shared/files/ff9f774d9732fdc7d704ef37ddc40ef507a1bab7ee3c1db5b1a6a8f26e6e4b37 +35 -0
  47. package/shared/snapshot.json +25 -0
  48. package/skeleton/app/.oxlintrc.json +14 -0
  49. package/skeleton/app/NOTES.md +19 -0
  50. package/skeleton/app/index.html +16 -0
  51. package/skeleton/app/package.json +28 -0
  52. package/skeleton/app/public/favicon.svg +1 -0
  53. package/skeleton/app/public/icon-192.png +0 -0
  54. package/skeleton/app/public/icon-512.png +0 -0
  55. package/skeleton/app/src/App.tsx +73 -0
  56. package/skeleton/app/src/NewVersionNotice.tsx +63 -0
  57. package/skeleton/app/src/index.css +37 -0
  58. package/skeleton/app/src/main.tsx +18 -0
  59. package/skeleton/app/src/pwa.ts +18 -0
  60. package/skeleton/app/src/start.test.ts +15 -0
  61. package/skeleton/app/src/start.ts +9 -0
  62. package/skeleton/app/tsconfig.app.json +27 -0
  63. package/skeleton/app/tsconfig.json +8 -0
  64. package/skeleton/app/tsconfig.node.json +23 -0
  65. package/skeleton/app/tsconfig.test.json +10 -0
  66. package/skeleton/app/vite.config.ts +13 -0
  67. package/skills/app-workflow/SKILL.md +22 -0
  68. package/skills/update/SKILL.md +69 -0
  69. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.7.0-dev",
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
+ }