@gjsify/site 0.0.0-stage → 0.62.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.
Files changed (56) hide show
  1. package/README.md +165 -2
  2. package/lib/cli.d.ts +6 -0
  3. package/lib/cli.js +91 -0
  4. package/lib/client/site.css +1286 -0
  5. package/lib/client/site.js +1 -0
  6. package/lib/index.d.ts +9 -0
  7. package/lib/index.js +9 -0
  8. package/lib/render/color.d.ts +12 -0
  9. package/lib/render/color.js +45 -0
  10. package/lib/render/html.d.ts +8 -0
  11. package/lib/render/html.js +20 -0
  12. package/lib/render/i18n.d.ts +38 -0
  13. package/lib/render/i18n.js +106 -0
  14. package/lib/render/icons.d.ts +14 -0
  15. package/lib/render/icons.js +42 -0
  16. package/lib/render/image-backend.d.ts +26 -0
  17. package/lib/render/image-backend.js +130 -0
  18. package/lib/render/images.d.ts +51 -0
  19. package/lib/render/images.js +97 -0
  20. package/lib/render/pages.d.ts +32 -0
  21. package/lib/render/pages.js +628 -0
  22. package/lib/render/qr.d.ts +8 -0
  23. package/lib/render/qr.js +25 -0
  24. package/lib/site-data/config.d.ts +23 -0
  25. package/lib/site-data/config.js +142 -0
  26. package/lib/site-data/downloads.d.ts +29 -0
  27. package/lib/site-data/downloads.js +193 -0
  28. package/lib/site-data/flathub.d.ts +24 -0
  29. package/lib/site-data/flathub.js +35 -0
  30. package/lib/site-data/github.d.ts +9 -0
  31. package/lib/site-data/github.js +45 -0
  32. package/lib/site-data/index.d.ts +12 -0
  33. package/lib/site-data/index.js +12 -0
  34. package/lib/site-data/load.d.ts +18 -0
  35. package/lib/site-data/load.js +164 -0
  36. package/lib/site-data/metainfo.d.ts +9 -0
  37. package/lib/site-data/metainfo.js +142 -0
  38. package/lib/site-data/network.d.ts +32 -0
  39. package/lib/site-data/network.js +67 -0
  40. package/lib/site-data/oars.d.ts +15 -0
  41. package/lib/site-data/oars.js +62 -0
  42. package/lib/site-data/po.d.ts +40 -0
  43. package/lib/site-data/po.js +148 -0
  44. package/lib/site-data/screenshots.d.ts +33 -0
  45. package/lib/site-data/screenshots.js +90 -0
  46. package/lib/site-data/sepa.d.ts +20 -0
  47. package/lib/site-data/sepa.js +45 -0
  48. package/lib/site-data/types.d.ts +224 -0
  49. package/lib/site-data/types.js +1 -0
  50. package/lib/site-data/xml.d.ts +11 -0
  51. package/lib/site-data/xml.js +51 -0
  52. package/lib/site.d.ts +49 -0
  53. package/lib/site.js +161 -0
  54. package/package.json +77 -5
  55. package/po/de.po +265 -0
  56. package/po/site.pot +264 -0
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ # @gjsify/site
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Static landing page generator for a GNOME-style app. Everything on the page comes from files the
4
+ project already has — the AppStream metainfo, the root `package.json`, the app's gettext
5
+ catalogues, a screenshot directory — plus, at build time, the GitHub release and Flathub APIs.
6
+ Nothing about any particular app is compiled in: all app-specific values come from a
7
+ `gjsify.site` block.
8
+
9
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS
10
+ (GNOME JavaScript).
11
+
12
+ The output works without JavaScript. Two small web components (`<app-download>`,
13
+ `<screenshot-carousel>`) only enhance what is already in the HTML.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ gjsify install -D @gjsify/site
19
+
20
+ # npm or yarn also work:
21
+ npm install -D @gjsify/site
22
+ ```
23
+
24
+ Runs on whichever runtime your `gjsify` runs on — GJS, Node, Bun or Deno. The screenshot
25
+ renditions go through GdkPixbuf, reached natively under GJS and through
26
+ [`@gjsify/node-gi`](../../node-gi/node-gi) otherwise, so add that package if you build the page on
27
+ Node, Bun or Deno. A site build also needs the gdk-pixbuf AVIF and WebP loaders, which current
28
+ GNOME ships; if one is missing the build says so and names the format.
29
+
30
+ ## Use
31
+
32
+ ```sh
33
+ gjsify site build # render ./dist from ./package.json's gjsify.site
34
+ gjsify site build --root <repo> --out <dir>
35
+ gjsify site i18n # update the page catalogues of <root>
36
+ gjsify site i18n --check # fail instead of writing
37
+ gjsify site build --runtime node # build and run on a specific runtime
38
+ ```
39
+
40
+ | Option | Meaning |
41
+ | --- | --- |
42
+ | `--root <path>` | Repository to work on — a checkout whose root `package.json` has `gjsify.site`. Default: the working directory. |
43
+ | `--out <dir>` | Output directory, emptied first. Default: `<root>/dist`. |
44
+ | `--offline` | Skip every network call. The page then links to the releases page and omits the counts. |
45
+ | `--no-cache` | Ignore the one-hour HTTP response cache. |
46
+ | `--cache-dir <dir>` | Where the response cache and the image renditions live. Default: `<root>/node_modules/.cache/gjsify-site`. |
47
+ | `--check` | `i18n`: report what is out of date instead of writing it. |
48
+ | `--runtime <r>` | `gjs` \| `node` \| `bun` \| `deno`. Default: the runtime the CLI itself is on. |
49
+
50
+ `GITHUB_TOKEN` is optional and only raises the API rate limit. When a lookup fails the build
51
+ warns rather than failing: a release API outage is not a reason to have no page.
52
+
53
+ The same thing from a script:
54
+
55
+ ```ts
56
+ import { buildSite, syncSiteI18n } from '@gjsify/site';
57
+
58
+ const { languages, screenshots } = await buildSite({ root, out, warn: console.warn });
59
+ ```
60
+
61
+ ## `gjsify.site`
62
+
63
+ Lives in the root `package.json`, next to `ship` and `flatpak`.
64
+
65
+ | Key | Meaning |
66
+ | --- | --- |
67
+ | `metainfo` | AppStream file, relative to the repo root. Source of name, summary, description, releases, URLs, branding, keywords, age rating. |
68
+ | `base` / `url` | URL path the site is served under, and its absolute origin + path (canonical, `hreflang`, `og:image`). |
69
+ | `languages` / `defaultLanguage` / `sourceLanguage` | Languages to render (`/en/`, `/de/`). `defaultLanguage` is where `/` lands when the visitor's languages match none (default: the first). `sourceLanguage` is the language of the metainfo's untranslated text (default `en`); the others are looked up in `translations`. |
70
+ | `repository` | `owner/name`; release assets and counts come from its latest GitHub release. |
71
+ | `flathub` | Flathub app id: adds the Flathub download and install count. Optional. |
72
+ | `webApp` | Path of a web app below `base`; adds "Try in browser". Optional. |
73
+ | `translations` | Directory of the app's `<lang>.po` catalogues, used when the metainfo lacks a translation. Optional. |
74
+ | `pageTranslations` | Directory of `<lang>.po` catalogues for the *page's own* strings, overlaying this package's `po/`. Optional, see below. |
75
+ | `icon` | Icon (SVG or PNG) for the header, hero and favicon; overrides the Flatpak/ship icon. Optional. |
76
+ | `downloads` | `[{platform, format, url?, label?, available?}]`: targets the release assets do not cover, e.g. browser stores or a source install. `platform` is an OS or `firefox` / `chromium`; `format` adds `store`, `xpi`, `source`. `url` is required unless `available` is `false`, which lists the target as "Not yet available" and never recommends it. |
77
+ | `releaseAssets` | `false` ignores the GitHub release assets, so `downloads` is the whole list. Default `true`. |
78
+ | `support`, `legal` | `support`: `{kind, url}` links, or `{kind: "sepa", name, iban, bic?, reference?}`, a bank transfer shown as IBAN plus an EPC QR code. `legal`: `{label, url}` links. Both empty by default: nothing is invented. |
79
+
80
+ A `sepa` entry carries no amount, and its IBAN is checked when the config loads — length per
81
+ country, alphabet, and the mod-97 checksum — so a typo fails the build instead of printing a QR
82
+ code that moves money nowhere.
83
+
84
+ ### Screenshots
85
+
86
+ Nothing to configure: the page shows the metainfo `<screenshots>`, in document order, with their
87
+ captions — the same artwork Flathub shows, so the page doubles as a preview of the store listing.
88
+
89
+ Each `<image>` is located before it is processed. A `raw.githubusercontent.com` URL into
90
+ `repository` is read from the checkout being built, so a metainfo pinned to a tag still shows the
91
+ artwork of the working tree. Anything else is downloaded once into the build cache, which is what
92
+ makes a later `--offline` build find it again. A screenshot whose file cannot be found or fetched
93
+ is skipped with a warning naming it — one image missing beats no page.
94
+
95
+ Two things happen to the artwork:
96
+
97
+ - **Fully transparent rows and columns are cropped away.** A window shot is saved on a canvas
98
+ large enough for the window's own shadow, and how much empty space is left around it is up to
99
+ whoever took the shot; the page would otherwise frame that emptiness. The threshold is "not one
100
+ pixel painted at all", so the shadow itself survives.
101
+ - **Renditions go up to 2560px and the display width is capped at 1280.** That settles what a "2×"
102
+ source is without asking the metainfo: a 2560px screenshot fills a 1280 CSS px box at
103
+ device-pixel-ratio 2 and stays crisp, a narrower one is shown at its natural width. Nothing is
104
+ ever upscaled.
105
+
106
+ The `<picture>` carries AVIF, WebP and PNG, each with a `srcset` of 640px, 1280px and 2560px
107
+ renditions up to the source's own width.
108
+
109
+ ### Colour
110
+
111
+ Also nothing to configure: the metainfo's `<branding>` colour for the page's scheme paints the
112
+ masthead — the bar, the icon and title, the install button, the facts row and the screenshots, all
113
+ one surface. The text on it is not assumed to be white; the build measures the brand colour's
114
+ relative luminance and writes near-black or white into `--band-fg`, whichever the eye can read.
115
+ Everything below the masthead keeps the site's own neutral palette, so a pale brand colour cannot
116
+ make the description unreadable. An app without `<branding>` gets the default blue.
117
+
118
+ ## Translations
119
+
120
+ Two catalogue sets meet on the page, and they have different owners.
121
+
122
+ - **Page strings** — "Download", "Release notes", the download labels — are gettext msgids in
123
+ English and ship with this package, in `po/`. Nothing to do to get them.
124
+ - **App text** — name, summary, description, release notes — is looked up per language: the
125
+ metainfo's own `xml:lang` element, then `translations/<lang>.po` (msgid = the source text), then
126
+ the source text.
127
+ - **Consumer page strings** — any page string a consumer wants to word differently — live in
128
+ `pageTranslations`. That directory
129
+ overlays `po/`: an entry there wins, everything else falls back. `gjsify site i18n` regenerates
130
+ it from the strings the build can emit and warns about each one still untranslated; `--check`
131
+ turns that into a failure for CI.
132
+
133
+ `gjsify site i18n --package` is the other mode: it syncs *this package's* `po/site.pot` and
134
+ `po/<lang>.po` from `src/render`, and is what `gjsify workspace @gjsify/site run i18n` calls. No
135
+ `xgettext` needed in either mode — the msgids are read from the sources directly.
136
+
137
+ ## Development
138
+
139
+ ```sh
140
+ gjsify workspace @gjsify/site build
141
+ gjsify workspace @gjsify/site check # typecheck + catalogues in sync
142
+ gjsify workspace @gjsify/site test # the node leg, then the gjs leg
143
+ gjsify workspace @gjsify/site test:cross-runtime # the GI-free suites on node + bun + deno
144
+ gjsify workspace @gjsify/site test:gjs-on-node # the image suite via @gjsify/node-gi
145
+ ```
146
+
147
+ The client bundle and the stylesheet are built once, here, into `lib/client/`. A site build only
148
+ content-hashes them and copies them to `<out>/assets/site-<hash>.{js,css}`, so a consumer needs no
149
+ bundler of its own.
150
+
151
+ `src/test.mts` holds the suites that need no GI and runs anywhere — `test:cross-runtime` runs that
152
+ one bundle on node, bun and deno. `src/test.gjs.mts` adds the image suites, which need GdkPixbuf,
153
+ and is the gjs leg. `src/test.node-gi.mts` registers the image suite alone and `test:gjs-on-node`
154
+ builds it `--app node`, so the same `src/render/image-backend.ts` encodes under Node through
155
+ `@gjsify/node-gi` — which is what proves the one image backend really is one.
156
+
157
+ ## Design
158
+
159
+ [ADR 0109](../../../docs/adr/0109-a-static-site-generator-reads-an-app-from-its-metainfo.md) —
160
+ why the page is derived from the metainfo, why the config is a union rather than a template, why
161
+ the entry point is a `gjsify` subcommand rather than a `bin` with a shebang, and why GdkPixbuf is
162
+ the only image backend.
163
+
164
+ ## License
165
+
166
+ MIT
package/lib/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runs one `gjsify site` invocation and answers the process exit code. Never
3
+ * exits the process itself: under GJS `process.exit` does not halt
4
+ * synchronously, so the decision belongs to the generated entry.
5
+ */
6
+ export declare function runSiteCli(argv: readonly string[]): Promise<number>;
package/lib/cli.js ADDED
@@ -0,0 +1,91 @@
1
+ // Argv parsing over `site.ts`; every decision worth testing lives there.
2
+ //
3
+ // No shebang and no top-level run: this is a LIBRARY entry point. `gjsify site`
4
+ // generates a tiny module that calls `runSiteCli`, bundles it for the runtime
5
+ // the CLI itself is on, and runs it — so there is no `gjsify-site` bin bound to
6
+ // one runtime. See `packages/infra/cli/src/commands/site.ts`.
7
+ import { join, resolve } from 'node:path';
8
+ import { buildSite, syncSiteI18n } from './site.js';
9
+ const USAGE = `Usage: gjsify site <command> [options]
10
+
11
+ build [options] Render the static pages of a repository
12
+ i18n [options] Update the page catalogues from the strings the build can emit
13
+
14
+ Options:
15
+ --root <path> Repository to work on (default: the working directory)
16
+ --out <dir> Output directory, emptied first (default: <root>/dist)
17
+ --offline Skip every network call
18
+ --no-cache Ignore the one-hour HTTP response cache
19
+ --cache-dir <dir> Where to keep the response cache and image renditions
20
+ (default: <root>/node_modules/.cache/gjsify-site)
21
+ --check i18n: report what is out of date instead of writing it
22
+ --package i18n: sync this package's own catalogues instead of a consumer's
23
+ --package-dir <d> Where @gjsify/site is installed; the client bundle and the
24
+ shipped catalogues are read from there (default: derived
25
+ from this module's own location)
26
+ -h, --help Show this text
27
+
28
+ Environment: GITHUB_TOKEN raises the GitHub API rate limit.`;
29
+ /** The value after `--name`, or `undefined` when the flag is absent. */
30
+ function flagValue(flags, name) {
31
+ const index = flags.indexOf(name);
32
+ if (index === -1)
33
+ return undefined;
34
+ const value = flags[index + 1];
35
+ if (value === undefined || value.startsWith('--'))
36
+ throw new Error(`${name} needs a value`);
37
+ return value;
38
+ }
39
+ function cacheOption(flags) {
40
+ if (flags.includes('--no-cache'))
41
+ return false;
42
+ return flagValue(flags, '--cache-dir');
43
+ }
44
+ async function build(flags) {
45
+ const root = resolve(flagValue(flags, '--root') ?? process.cwd());
46
+ const result = await buildSite({
47
+ root,
48
+ out: resolve(flagValue(flags, '--out') ?? join(root, 'dist')),
49
+ offline: flags.includes('--offline'),
50
+ cacheDir: cacheOption(flags),
51
+ packageDir: flagValue(flags, '--package-dir'),
52
+ githubToken: process.env.GITHUB_TOKEN || undefined,
53
+ warn: (message) => console.warn(`warning: ${message}`),
54
+ });
55
+ console.log(`Built ${result.languages.length} pages and ${result.screenshots} screenshots`);
56
+ return 0;
57
+ }
58
+ async function i18n(flags) {
59
+ const check = flags.includes('--check');
60
+ const root = flags.includes('--package') ? undefined : resolve(flagValue(flags, '--root') ?? process.cwd());
61
+ const { stale, untranslated } = await syncSiteI18n({
62
+ root,
63
+ check,
64
+ packageDir: flagValue(flags, '--package-dir'),
65
+ });
66
+ for (const file of stale)
67
+ console.log(check ? `${file} is out of date; run \`gjsify site i18n\`` : `updated ${file}`);
68
+ for (const message of untranslated)
69
+ console.error(message);
70
+ return untranslated.length > 0 || (check && stale.length > 0) ? 1 : 0;
71
+ }
72
+ /**
73
+ * Runs one `gjsify site` invocation and answers the process exit code. Never
74
+ * exits the process itself: under GJS `process.exit` does not halt
75
+ * synchronously, so the decision belongs to the generated entry.
76
+ */
77
+ export async function runSiteCli(argv) {
78
+ const [command, ...flags] = argv;
79
+ try {
80
+ if (command === 'build')
81
+ return await build(flags);
82
+ if (command === 'i18n')
83
+ return await i18n(flags);
84
+ }
85
+ catch (error) {
86
+ console.error(error instanceof Error ? error.message : error);
87
+ return 1;
88
+ }
89
+ console.error(USAGE);
90
+ return command === '--help' || command === '-h' ? 0 : 2;
91
+ }