@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.
- package/README.md +165 -2
- package/lib/cli.d.ts +6 -0
- package/lib/cli.js +91 -0
- package/lib/client/site.css +1286 -0
- package/lib/client/site.js +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +9 -0
- package/lib/render/color.d.ts +12 -0
- package/lib/render/color.js +45 -0
- package/lib/render/html.d.ts +8 -0
- package/lib/render/html.js +20 -0
- package/lib/render/i18n.d.ts +38 -0
- package/lib/render/i18n.js +106 -0
- package/lib/render/icons.d.ts +14 -0
- package/lib/render/icons.js +42 -0
- package/lib/render/image-backend.d.ts +26 -0
- package/lib/render/image-backend.js +130 -0
- package/lib/render/images.d.ts +51 -0
- package/lib/render/images.js +97 -0
- package/lib/render/pages.d.ts +32 -0
- package/lib/render/pages.js +628 -0
- package/lib/render/qr.d.ts +8 -0
- package/lib/render/qr.js +25 -0
- package/lib/site-data/config.d.ts +23 -0
- package/lib/site-data/config.js +142 -0
- package/lib/site-data/downloads.d.ts +29 -0
- package/lib/site-data/downloads.js +193 -0
- package/lib/site-data/flathub.d.ts +24 -0
- package/lib/site-data/flathub.js +35 -0
- package/lib/site-data/github.d.ts +9 -0
- package/lib/site-data/github.js +45 -0
- package/lib/site-data/index.d.ts +12 -0
- package/lib/site-data/index.js +12 -0
- package/lib/site-data/load.d.ts +18 -0
- package/lib/site-data/load.js +164 -0
- package/lib/site-data/metainfo.d.ts +9 -0
- package/lib/site-data/metainfo.js +142 -0
- package/lib/site-data/network.d.ts +32 -0
- package/lib/site-data/network.js +67 -0
- package/lib/site-data/oars.d.ts +15 -0
- package/lib/site-data/oars.js +62 -0
- package/lib/site-data/po.d.ts +40 -0
- package/lib/site-data/po.js +148 -0
- package/lib/site-data/screenshots.d.ts +33 -0
- package/lib/site-data/screenshots.js +90 -0
- package/lib/site-data/sepa.d.ts +20 -0
- package/lib/site-data/sepa.js +45 -0
- package/lib/site-data/types.d.ts +224 -0
- package/lib/site-data/types.js +1 -0
- package/lib/site-data/xml.d.ts +11 -0
- package/lib/site-data/xml.js +51 -0
- package/lib/site.d.ts +49 -0
- package/lib/site.js +161 -0
- package/package.json +77 -5
- package/po/de.po +265 -0
- package/po/site.pot +264 -0
package/README.md
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gjsify/site
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|