waybetter 0.6.0 → 0.7.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 +1 -2
- package/bin/waybetter.mjs +2 -5
- package/cli/buildenv.mjs +1 -1
- package/cli/release.mjs +11 -18
- package/cli/releasecheck.mjs +15 -0
- package/cli/remote.mjs +4 -4
- package/cli/store.mjs +80 -28
- package/cli/strings.mjs +2 -0
- package/dist/runtime/stringPackHook.js +2 -2
- package/dist/runtime/stringPackHook.js.map +1 -1
- package/dist/runtime/stringPackTranslate.d.ts +3 -1
- package/dist/runtime/stringPackTranslate.js +6 -3
- package/dist/runtime/stringPackTranslate.js.map +1 -1
- package/dist/runtime/stringPacks.d.ts +53 -5
- package/dist/runtime/stringPacks.js +161 -25
- package/dist/runtime/stringPacks.js.map +1 -1
- package/dist/runtime/themeBar.d.ts +28 -0
- package/dist/runtime/themeBar.js +82 -0
- package/dist/runtime/themeBar.js.map +1 -0
- package/dist/runtime/themeComponents.js +16 -3
- package/dist/runtime/themeComponents.js.map +1 -1
- package/dist/runtime/themeDecor.js +3 -1
- package/dist/runtime/themeDecor.js.map +1 -1
- package/dist/runtime/themeDerive.d.ts +69 -0
- package/dist/runtime/themeDerive.js +117 -0
- package/dist/runtime/themeDerive.js.map +1 -0
- package/dist/runtime/themeDoctor.d.ts +44 -3
- package/dist/runtime/themeDoctor.js +86 -25
- package/dist/runtime/themeDoctor.js.map +1 -1
- package/dist/runtime/themeGuards.d.ts +60 -0
- package/dist/runtime/themeGuards.js +235 -0
- package/dist/runtime/themeGuards.js.map +1 -0
- package/dist/runtime/themeIcons.d.ts +1 -1
- package/dist/runtime/themeIcons.js +13 -5
- package/dist/runtime/themeIcons.js.map +1 -1
- package/dist/runtime/themeInk.d.ts +37 -0
- package/dist/runtime/themeInk.js +111 -0
- package/dist/runtime/themeInk.js.map +1 -0
- package/dist/runtime/themeInvariants.d.ts +39 -0
- package/dist/runtime/themeInvariants.js +273 -0
- package/dist/runtime/themeInvariants.js.map +1 -0
- package/dist/runtime/themeRuntime.d.ts +15 -1
- package/dist/runtime/themeRuntime.js +78 -5
- package/dist/runtime/themeRuntime.js.map +1 -1
- package/dist/runtime/themeSpec.d.ts +5 -1
- package/dist/runtime/themeSpec.js +9 -13
- package/dist/runtime/themeSpec.js.map +1 -1
- package/dist/shell/InstallDialog.js +14 -8
- package/dist/shell/InstallDialog.js.map +1 -1
- package/dist/shell/StoreView.d.ts +12 -0
- package/dist/shell/StoreView.js +9 -4
- package/dist/shell/StoreView.js.map +1 -1
- package/dist/shell/frame/AppMenu.d.ts +26 -3
- package/dist/shell/frame/AppMenu.js +106 -27
- package/dist/shell/frame/AppMenu.js.map +1 -1
- package/dist/shell/frame/FindBar.js +1 -1
- package/dist/shell/frame/FindBar.js.map +1 -1
- package/dist/shell/frame/Shell.d.ts +1 -0
- package/dist/shell/frame/Shell.js +10 -2
- package/dist/shell/frame/Shell.js.map +1 -1
- package/dist/shell/frame/Titlebar.d.ts +2 -1
- package/dist/shell/frame/Titlebar.js +5 -3
- package/dist/shell/frame/Titlebar.js.map +1 -1
- package/dist/shell/frame/WebPanel.js +4 -1
- package/dist/shell/frame/WebPanel.js.map +1 -1
- package/dist/shell/frame/accountSwitcher.css +10 -10
- package/dist/shell/frame/appMenu.css +19 -13
- package/dist/shell/frame/boot.js +2 -0
- package/dist/shell/frame/boot.js.map +1 -1
- package/dist/shell/frame/chromeTheme.d.ts +19 -1
- package/dist/shell/frame/chromeTheme.js +51 -14
- package/dist/shell/frame/chromeTheme.js.map +1 -1
- package/dist/shell/frame/deck.css +30 -30
- package/dist/shell/frame/edges.css +159 -0
- package/dist/shell/frame/index.d.ts +2 -2
- package/dist/shell/frame/index.js +1 -1
- package/dist/shell/frame/index.js.map +1 -1
- package/dist/shell/frame/mediaModel.d.ts +13 -0
- package/dist/shell/frame/mediaModel.js +14 -0
- package/dist/shell/frame/mediaModel.js.map +1 -1
- package/dist/shell/frame/menuMessages.en.json +43 -1
- package/dist/shell/frame/menuModel.d.ts +42 -0
- package/dist/shell/frame/menuModel.js +8 -0
- package/dist/shell/frame/menuModel.js.map +1 -1
- package/dist/shell/frame/nativeMenu.d.ts +5 -0
- package/dist/shell/frame/nativeMenu.js +178 -0
- package/dist/shell/frame/nativeMenu.js.map +1 -0
- package/dist/shell/frame/nativeMenuModel.d.ts +102 -0
- package/dist/shell/frame/nativeMenuModel.js +186 -0
- package/dist/shell/frame/nativeMenuModel.js.map +1 -0
- package/dist/shell/frame/paletteModel.js +3 -1
- package/dist/shell/frame/paletteModel.js.map +1 -1
- package/dist/shell/frame/paneTheme.css +1 -1
- package/dist/shell/frame/reportModel.d.ts +1 -1
- package/dist/shell/frame/reportModel.js +1 -1
- package/dist/shell/frame/reportModel.js.map +1 -1
- package/dist/shell/frame/shell.css +56 -51
- package/dist/shell/frame/state.d.ts +2 -0
- package/dist/shell/frame/state.js +3 -1
- package/dist/shell/frame/state.js.map +1 -1
- package/dist/shell/frame/themeStore.js +21 -6
- package/dist/shell/frame/themeStore.js.map +1 -1
- package/dist/shell/frame/workspaces.css +12 -12
- package/dist/shell/installController.d.ts +16 -1
- package/dist/shell/installController.js +31 -5
- package/dist/shell/installController.js.map +1 -1
- package/dist/shell/panel/Appearance.js +12 -3
- package/dist/shell/panel/Appearance.js.map +1 -1
- package/dist/shell/panel/Settings.d.ts +1 -1
- package/dist/shell/panel/Settings.js +4 -3
- package/dist/shell/panel/Settings.js.map +1 -1
- package/dist/shell/panel/StoreSection.d.ts +2 -1
- package/dist/shell/panel/StoreSection.js +31 -1
- package/dist/shell/panel/StoreSection.js.map +1 -1
- package/dist/shell/panel/StringPacks.d.ts +12 -3
- package/dist/shell/panel/StringPacks.js +33 -8
- package/dist/shell/panel/StringPacks.js.map +1 -1
- package/dist/shell/panel/backup.css +1 -1
- package/dist/shell/panel/builder.css +48 -40
- package/dist/shell/panel/make.css +8 -7
- package/dist/shell/panel/panel.css +401 -401
- package/dist/shell/panel/panelMessages.d.ts +41 -1
- package/dist/shell/panel/panelMessages.js +13 -1
- package/dist/shell/panel/panelMessages.js.map +1 -1
- package/dist/shell/panel/plugins.css +5 -5
- package/dist/shell/panel/strings.css +1 -1
- package/dist/shell/store.css +34 -14
- package/dist/shell/storeMessages.d.ts +24 -0
- package/dist/shell/storeMessages.js +24 -0
- package/dist/shell/storeMessages.js.map +1 -1
- package/dist/shell/storeModel.d.ts +31 -7
- package/dist/shell/storeModel.js +15 -2
- package/dist/shell/storeModel.js.map +1 -1
- package/dist/shell/stringPacksModel.js +7 -2
- package/dist/shell/stringPacksModel.js.map +1 -1
- package/icons.schema.json +9 -0
- package/locales/ar.json +109 -2
- package/locales/bg.json +109 -2
- package/locales/bn.json +109 -2
- package/locales/ca.json +109 -2
- package/locales/cs.json +109 -2
- package/locales/da.json +109 -2
- package/locales/de.json +109 -2
- package/locales/el.json +109 -2
- package/locales/en.json +108 -2
- package/locales/es.json +109 -2
- package/locales/eu.json +109 -2
- package/locales/fa.json +109 -2
- package/locales/fi.json +109 -2
- package/locales/fil.json +109 -2
- package/locales/fr.json +109 -2
- package/locales/ga.json +109 -2
- package/locales/gl.json +109 -2
- package/locales/gu.json +109 -2
- package/locales/ha.json +109 -2
- package/locales/he.json +109 -2
- package/locales/hi.json +109 -2
- package/locales/hr.json +109 -2
- package/locales/hu.json +110 -2
- package/locales/id.json +109 -2
- package/locales/ig.json +109 -2
- package/locales/it.json +109 -2
- package/locales/ja.json +109 -2
- package/locales/kn.json +109 -2
- package/locales/ko.json +109 -2
- package/locales/mr.json +109 -2
- package/locales/ms.json +109 -2
- package/locales/nb.json +109 -2
- package/locales/nl.json +109 -2
- package/locales/pl.json +109 -2
- package/locales/pt.json +109 -2
- package/locales/ro.json +109 -2
- package/locales/ru.json +109 -2
- package/locales/sk.json +109 -2
- package/locales/sr.json +109 -2
- package/locales/sv.json +109 -2
- package/locales/ta.json +109 -2
- package/locales/th.json +109 -2
- package/locales/tr.json +109 -2
- package/locales/uk.json +109 -2
- package/locales/ur.json +109 -2
- package/locales/vi.json +109 -2
- package/locales/yo.json +109 -2
- package/locales/zh-Hans.json +109 -2
- package/locales/zh-Hant.json +109 -2
- package/package.json +2 -3
- package/store-catalog.schema.json +18 -5
- package/theme.schema.json +9 -0
- package/windows/installer.nsi +22 -2
- package/cli/privacy.mjs +0 -768
package/README.md
CHANGED
|
@@ -120,9 +120,8 @@ const Shell = createShell({ ...site, panel: { id: 'app', title: 'wayBetter', com
|
|
|
120
120
|
- `waybetter store sign [--init] --key <private key>` / `waybetter store verify --pub <key file>`: Ed25519 signature over the exact catalog bytes; any listed public key verifies (rotation). The private key is never printed or copied.
|
|
121
121
|
- `waybetter strings check <pack> [--inventory <file>] [--verbose]`: validates a string pack (data that replaces the wrapped site's own UI strings: `waybetter/runtime/stringPacks`, `waybetter/runtime/stringPackHook`, `waybetter/runtime/textNet`) against the site's string inventory and prints its coverage; the inventories otherwise come from `waybetter.strings.json` in the app's root.
|
|
122
122
|
- `waybetter i18n check [--write] [--verbose] [locale…]`: checks an app's catalogs (and the framework's in the same languages) with the `waybetter/i18n/check` rules, bundled plugins' inline English and the generated files; `--write` regenerates `languages.json` and the native localization files (macOS, Linux). `waybetter i18n extract [--missing]`: keys the code looks up that English lacks, and English keys without a literal lookup. Both read `waybetter.i18n.json` in the app's root (`locales`, `sources`, `nativeSources`, `bundledPlugins`, `runtimeKeys`, `supported`, `native`; schema: `i18n-config.schema.json`). `supported` lists the languages in the supported tier; every other catalog gets `tier: "community"` in the generated `languages.json` (`languages.schema.json`), and an app resolves its automatic language choices only to supported ones (`createLanguage({ supported })`).
|
|
123
|
-
- `waybetter release windows|macos`: signed native releases and the update manifest (`waybetter.release.json`). The Windows build runs path-free (below)
|
|
123
|
+
- `waybetter release windows|macos`: signed native releases and the update manifest (`waybetter.release.json`). The Windows build runs path-free (below); `WAYBETTER_RELEASE_CHECK` can name a command run on every artifact before anything is copied or signed.
|
|
124
124
|
- `waybetter build-env -- <command>`: runs a build (`tauri build`, `cargo build`) so the binary holds none of the machine's paths: the home folder, the workspace, `CARGO_HOME`, `RUSTUP_HOME` and `CARGO_TARGET_DIR` become `/home`, `/src`, `/cargo`, `/rustup` and `/target` (`--remap-path-prefix` appended to the flags already in `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS`, `/PDBALTPATH` on MSVC; for C and C++ a build script compiles, `-ffile-prefix-map` on gcc and clang and, on MSVC, `/d1trimfile:<root>` in `CL` and `/PDBALTPATH` in `LINK`, which reach the cc crate, CMake and MSBuild alike). Without a command it prints that environment (`--json`).
|
|
125
|
-
- `waybetter privacy-scan <paths…> [--terms <file>] [--tracked] [--history]`: searches files, binaries (UTF-8 and UTF-16), archives (zip, tar, gzip, zstd, .deb, DMG; NSIS installers, AppImages and xz with 7-Zip or the matching tool), git-tracked files and a repository's whole history for forbidden terms: the lines of a file kept outside every repository (`--terms`, `WAYBETTER_FORBIDDEN_TERMS`, or `"forbiddenTerms"` in `waybetter.release.json`) plus this machine's account name. A name is a hit as a whole word in any case; a name of three characters or fewer only in a path or as the capitalized word. Output shows file and line or offset with the term masked (`m***`); exit 1 on a hit, 2 when a container could not be opened. Reviewed false positives: `"privacyAllowlist"`, lines of `<file glob>: <regex over the context, the term written <term>>`.
|
|
126
125
|
- `windows/languages/<Language>.nsh`: the Windows installer's messages for the languages Tauri does not ship, next to the installer template (`windows/installer.nsi`).
|
|
127
126
|
|
|
128
127
|
## Plugin UI kit (`runtime/ui`)
|
package/bin/waybetter.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The `waybetter` command: `waybetter store build|sign|verify` (cli/store.mjs),
|
|
3
3
|
// `waybetter release windows|macos|linux` (cli/release.mjs), `waybetter manifests` (cli/manifests.mjs), `waybetter i18n check|extract`
|
|
4
|
-
// (cli/i18n.mjs), `waybetter
|
|
4
|
+
// (cli/i18n.mjs), `waybetter build-env`
|
|
5
5
|
// (cli/buildenv.mjs), `waybetter dev` (cli/dev.mjs: watchers + tauri dev), `waybetter strings check`
|
|
6
6
|
// (cli/strings.mjs).
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
@@ -9,7 +9,6 @@ import { BUILD_ENV_USAGE, buildEnvCommand } from '../cli/buildenv.mjs';
|
|
|
9
9
|
import { DEV_USAGE, devCommand } from '../cli/dev.mjs';
|
|
10
10
|
import { MANIFESTS_USAGE, manifestsCommand } from '../cli/manifests.mjs';
|
|
11
11
|
import { I18N_USAGE, i18nCommand } from '../cli/i18n.mjs';
|
|
12
|
-
import { PRIVACY_USAGE, privacyCommand } from '../cli/privacy.mjs';
|
|
13
12
|
import { STORE_USAGE, storeCommand } from '../cli/store.mjs';
|
|
14
13
|
import { STRINGS_USAGE, stringsCommand } from '../cli/strings.mjs';
|
|
15
14
|
import { RELEASE_USAGE, releaseCommand } from '../cli/release.mjs';
|
|
@@ -26,8 +25,6 @@ try {
|
|
|
26
25
|
process.exitCode = await i18nCommand(rest);
|
|
27
26
|
} else if (area === 'strings') {
|
|
28
27
|
process.exitCode = await stringsCommand(rest);
|
|
29
|
-
} else if (area === 'privacy-scan') {
|
|
30
|
-
process.exitCode = privacyCommand(rest);
|
|
31
28
|
} else if (area === 'build-env') {
|
|
32
29
|
process.exitCode = buildEnvCommand(rest);
|
|
33
30
|
} else if (area === 'dev') {
|
|
@@ -35,7 +32,7 @@ try {
|
|
|
35
32
|
} else if (area === '--version' || area === '-v') {
|
|
36
33
|
console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
|
|
37
34
|
} else {
|
|
38
|
-
console.log(`${STORE_USAGE}\n\n${RELEASE_USAGE}\n\n${MANIFESTS_USAGE}\n\n${I18N_USAGE}\n\n${STRINGS_USAGE}\n\n${
|
|
35
|
+
console.log(`${STORE_USAGE}\n\n${RELEASE_USAGE}\n\n${MANIFESTS_USAGE}\n\n${I18N_USAGE}\n\n${STRINGS_USAGE}\n\n${BUILD_ENV_USAGE}
|
|
39
36
|
|
|
40
37
|
${DEV_USAGE}`);
|
|
41
38
|
if (area && area !== '--help' && area !== '-h') process.exitCode = 1;
|
package/cli/buildenv.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// waybetter build-env -- tauri build run a command with that environment
|
|
19
19
|
// waybetter build-env [--json] show it
|
|
20
20
|
//
|
|
21
|
-
// `waybetter release windows` builds with it.
|
|
21
|
+
// `waybetter release windows` builds with it.
|
|
22
22
|
import { spawnSync } from 'node:child_process';
|
|
23
23
|
import { existsSync } from 'node:fs';
|
|
24
24
|
import { homedir } from 'node:os';
|
package/cli/release.mjs
CHANGED
|
@@ -17,17 +17,13 @@
|
|
|
17
17
|
// "prepareCommand": "bun install --frozen-lockfile", // remote/WSL builds: runs first in the unpacked sources
|
|
18
18
|
// "distribution": { "publisher": "CRT", "id": "CRT.Name", "homepage": "https://...", "license": "...", "shortDescription": "...", "downloadBase": "https://..." },
|
|
19
19
|
// // manifests written after each release step (see cli/manifests.mjs)
|
|
20
|
-
// "exportExclude": ["verification"]
|
|
21
|
-
// "forbiddenTerms": "~/.better-twttr-keys/forbidden-names.txt", // optional, outside the repo
|
|
22
|
-
// "privacyAllowlist": "privacy-allowlist.txt" // optional, reviewed false positives
|
|
20
|
+
// "exportExclude": ["verification"] // remote/WSL builds: app-repo paths left out of the upload
|
|
23
21
|
// }
|
|
24
22
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// copied, signed or written
|
|
29
|
-
// installer needs 7-Zip), stops the release. The terms are the file "forbiddenTerms" names (or
|
|
30
|
-
// WAYBETTER_FORBIDDEN_TERMS) plus this machine's account name and home folder name.
|
|
23
|
+
// The Windows build runs with the path-free build environment (cli/buildenv.mjs). Every artifact
|
|
24
|
+
// (the installer, the main binary, the front end, the dmg and the updater bundle) goes through the
|
|
25
|
+
// optional release check (cli/releasecheck.mjs, WAYBETTER_RELEASE_CHECK) before anything is
|
|
26
|
+
// copied, signed or written.
|
|
31
27
|
//
|
|
32
28
|
// The private key is read only into the build's environment (Windows) or handed to the Tauri
|
|
33
29
|
// signer by path (macOS). Nothing here prints, copies or moves it.
|
|
@@ -37,7 +33,7 @@ import { homedir } from 'node:os';
|
|
|
37
33
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
38
34
|
import { pathToFileURL } from 'node:url';
|
|
39
35
|
import { pathFreeBuildEnv } from './buildenv.mjs';
|
|
40
|
-
import {
|
|
36
|
+
import { runReleaseCheck } from './releasecheck.mjs';
|
|
41
37
|
import { CHANNELS, resolveSpec, writeManifests } from './manifests.mjs';
|
|
42
38
|
import { buildLinuxWsl, buildMacRemote, scanBuilt } from './remote.mjs';
|
|
43
39
|
|
|
@@ -65,10 +61,9 @@ export const RELEASE_USAGE = `Usage:
|
|
|
65
61
|
linux-x86_64 to latest.json (run the Windows step first).
|
|
66
62
|
|
|
67
63
|
Config: ${CONFIG_FILE} in the app's root (product, artifact, desktopDir, keysDir, keysDirEnv,
|
|
68
|
-
cdnBase, apiBase, outDir, optional manifestValidator
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
waybetter privacy-scan. Nothing is ever uploaded.`;
|
|
64
|
+
cdnBase, apiBase, outDir, optional manifestValidator). Env: CARGO_TARGET_DIR, CDN_BASE,
|
|
65
|
+
DEPLOY_TARGET, CDN_DIR, RELEASES_DIR, WAYBETTER_RELEASE_CHECK (an optional command run on every
|
|
66
|
+
artifact first). The build is path-free (waybetter build-env). Nothing is ever uploaded.`;
|
|
72
67
|
|
|
73
68
|
class ReleaseError extends Error {}
|
|
74
69
|
const fail = message => { throw new ReleaseError(message); };
|
|
@@ -127,8 +122,6 @@ export function normalizeReleaseConfig(raw, root) {
|
|
|
127
122
|
distribution: raw.distribution,
|
|
128
123
|
exportExclude: Array.isArray(raw.exportExclude) ? raw.exportExclude.map(String) : [],
|
|
129
124
|
manifestValidator: validator && { module: at(validator.module), export: validator.export, hint: validator.hint },
|
|
130
|
-
forbiddenTerms: raw.forbiddenTerms === undefined ? undefined : at(home(text('forbiddenTerms'))),
|
|
131
|
-
privacyAllowlist: raw.privacyAllowlist === undefined ? undefined : at(text('privacyAllowlist')),
|
|
132
125
|
};
|
|
133
126
|
}
|
|
134
127
|
|
|
@@ -179,9 +172,9 @@ function readVersion(config) {
|
|
|
179
172
|
return { version, productName: conf.productName, binary, dist };
|
|
180
173
|
}
|
|
181
174
|
|
|
182
|
-
/** The release
|
|
175
|
+
/** The optional release check (cli/releasecheck.mjs) over every artifact that exists. */
|
|
183
176
|
function gate(config, paths, env, log) {
|
|
184
|
-
|
|
177
|
+
runReleaseCheck(paths.filter(path => path && existsSync(path)), { env, log, label: 'release: check' });
|
|
185
178
|
}
|
|
186
179
|
|
|
187
180
|
function option(args, name) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// An optional, owner-supplied check run on every artifact before a release copies, signs or writes
|
|
2
|
+
// anything: WAYBETTER_RELEASE_CHECK names a command (run through the shell) that gets the artifact
|
|
3
|
+
// paths as arguments. A non-zero exit stops the release. Unset, nothing runs.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export const RELEASE_CHECK_ENV = 'WAYBETTER_RELEASE_CHECK';
|
|
7
|
+
|
|
8
|
+
export function runReleaseCheck(paths, { env = process.env, log = console.log, label = 'release check' } = {}) {
|
|
9
|
+
const command = env[RELEASE_CHECK_ENV];
|
|
10
|
+
if (!command || !paths.length) return;
|
|
11
|
+
const quoted = paths.map(path => `'${String(path).replaceAll("'", "'\\''")}'`).join(' ');
|
|
12
|
+
log(`${label}: ${paths.length} file(s)`);
|
|
13
|
+
const result = spawnSync(`${command} ${quoted}`, { shell: true, stdio: 'inherit', env });
|
|
14
|
+
if (result.status !== 0) throw new Error(`${label} failed (exit ${result.status ?? result.signal})`);
|
|
15
|
+
}
|
package/cli/remote.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Remote and WSL builds for `waybetter release macos|linux`: the committed state of an app (its
|
|
2
2
|
// git HEAD plus every submodule at the commit the app records) is exported, sent to the machine
|
|
3
3
|
// that builds it, built there through `waybetter build-env` (no build path in the binary) and the
|
|
4
|
-
// artifacts come back to be
|
|
4
|
+
// artifacts come back to be checked (the optional release check) and, later, signed on this machine.
|
|
5
5
|
// Nothing here touches the working tree the developer has open, and no key ever leaves this
|
|
6
6
|
// machine: the remote build is unsigned (updater artifacts off), signing is `waybetter release`.
|
|
7
7
|
//
|
|
@@ -18,7 +18,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
18
18
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { tmpdir } from 'node:os';
|
|
20
20
|
import { basename, join, relative, resolve } from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import { runReleaseCheck } from './releasecheck.mjs';
|
|
22
22
|
|
|
23
23
|
class RemoteError extends Error {}
|
|
24
24
|
const fail = message => { throw new RemoteError(message); };
|
|
@@ -250,11 +250,11 @@ export function buildLinuxWsl({ config, distro = 'Ubuntu-22.04', remoteDir, ref
|
|
|
250
250
|
|
|
251
251
|
const relTauri = (config, name) => `${relative(config.root, config.tauriDir).replaceAll('\\', '/')}/${name}`;
|
|
252
252
|
|
|
253
|
-
/**
|
|
253
|
+
/** Prints the built artifacts' sizes and runs the optional release check on them here. */
|
|
254
254
|
export function scanBuilt(config, files, { env = process.env, log = console.log } = {}) {
|
|
255
255
|
for (const file of files) {
|
|
256
256
|
if (!existsSync(file)) fail(`expected build output missing: ${file}`);
|
|
257
257
|
log(`built: ${basename(file)} ${(statSync(file).size / 1048576).toFixed(1)} MB`);
|
|
258
258
|
}
|
|
259
|
-
|
|
259
|
+
runReleaseCheck(files, { env, log, label: 'release: check' });
|
|
260
260
|
}
|
package/cli/store.mjs
CHANGED
|
@@ -15,7 +15,19 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
|
|
|
15
15
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Two catalog files, one source. `catalog.json` is format 2 and lists only the kinds every app
|
|
20
|
+
* since the first release knows (theme, plugin, strings): an app from before a kind existed refuses
|
|
21
|
+
* a whole catalog that lists it. `catalog.v3.json` is format 3, lists everything (icons, builtin, ...)
|
|
22
|
+
* and is read first by apps that know it; they fall back to `catalog.json` when it is not published.
|
|
23
|
+
* `waybetter store build` writes both from the same items; `sign` and `verify` handle both.
|
|
24
|
+
*/
|
|
18
25
|
export const CATALOG_FORMAT = 2;
|
|
26
|
+
export const CATALOG_FORMAT_NEXT = 3;
|
|
27
|
+
export const CATALOG_FILE = 'catalog.json';
|
|
28
|
+
export const CATALOG_FILE_NEXT = 'catalog.v3.json';
|
|
29
|
+
/** The kinds format 2 may list; a newer kind is format 3 only. */
|
|
30
|
+
export const LEGACY_KINDS = ['theme', 'plugin', 'strings'];
|
|
19
31
|
export const DEFAULT_BASE = 'https://waybetter.fyi/store/';
|
|
20
32
|
/** The framework's download hosts (waybetter-host `DEFAULT_HOSTS`). */
|
|
21
33
|
export const DEFAULT_HOSTS = [
|
|
@@ -25,6 +37,9 @@ export const DEFAULT_HOSTS = [
|
|
|
25
37
|
];
|
|
26
38
|
export const PERMISSIONS = ['storage', 'ui', 'clipboard', 'account', 'users', 'commands', 'network', 'network:none'];
|
|
27
39
|
const MAX_BYTES = 2 * 1024 * 1024;
|
|
40
|
+
/** An icon pack file (the runtime's `ICON_LIMITS.packBytes`). */
|
|
41
|
+
const MAX_ICONS_BYTES = 120_000;
|
|
42
|
+
const KINDS = ['theme', 'plugin', 'strings', 'icons', 'builtin'];
|
|
28
43
|
const MAX_ITEMS = 500;
|
|
29
44
|
const MAX_CATALOG_BYTES = 1024 * 1024;
|
|
30
45
|
// SPKI DER prefix of an Ed25519 public key; the raw 32 bytes follow it.
|
|
@@ -84,7 +99,7 @@ export function itemProblems(item, hosts) {
|
|
|
84
99
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return ['not an object'];
|
|
85
100
|
for (const key of Object.keys(item)) if (!ITEM_FIELDS.has(key)) bad.push(`unknown field ${key}`);
|
|
86
101
|
if (!Array.isArray(item.apps) || !item.apps.length || item.apps.length > 16 || !item.apps.every(validAppId) || new Set(item.apps).size !== item.apps.length) bad.push('apps');
|
|
87
|
-
if (
|
|
102
|
+
if (!KINDS.includes(item.kind)) bad.push('kind');
|
|
88
103
|
// Any item may name its licence and credit its authors; only a string pack has a language.
|
|
89
104
|
if ((item.license !== undefined && (!text(item.license, 60) || !String(item.license).trim())) || (item.credits !== undefined && !text(item.credits, 600))) bad.push('license or credits');
|
|
90
105
|
if (item.kind !== 'strings' && item.language !== undefined) bad.push('language');
|
|
@@ -92,10 +107,18 @@ export function itemProblems(item, hosts) {
|
|
|
92
107
|
if (!text(item.name, 60) || !String(item.name).trim() || !text(item.author ?? '', 60) || !text(item.description ?? '', 600)) bad.push('name, author or description');
|
|
93
108
|
if (!text(item.version, 32) || !item.version || !text(item.category ?? '', 32)) bad.push('version or category');
|
|
94
109
|
if (item.tags !== undefined && (!Array.isArray(item.tags) || item.tags.length > 12 || !item.tags.every(tag => text(tag, 32)))) bad.push('tags');
|
|
95
|
-
if (
|
|
110
|
+
if (item.kind === 'builtin') {
|
|
111
|
+
// A feature that ships inside the app (an area plugin): nothing to download, so nothing to point at or hash.
|
|
112
|
+
for (const key of ['url', 'sha256', 'size', 'file', 'permissions', 'colors', 'language']) if (item[key] !== undefined) bad.push('built-in item fields');
|
|
113
|
+
} else {
|
|
114
|
+
if (typeof item.url !== 'string' || !item.url.startsWith('https://') || (hosts && !allowedUrl(item.url, hosts))) bad.push('url (host not allowed)');
|
|
115
|
+
}
|
|
96
116
|
if (item.preview !== undefined && (typeof item.preview !== 'string' || !item.preview.startsWith('https://') || (hosts && !allowedUrl(item.preview, hosts)))) bad.push('preview');
|
|
97
|
-
if (
|
|
98
|
-
|
|
117
|
+
if (item.kind !== 'builtin') {
|
|
118
|
+
if (typeof item.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(item.sha256)) bad.push('sha256');
|
|
119
|
+
if (!Number.isInteger(item.size) || item.size <= 0 || item.size > (item.kind === 'icons' ? MAX_ICONS_BYTES : MAX_BYTES)) bad.push('size');
|
|
120
|
+
}
|
|
121
|
+
if (item.kind === 'icons' && (item.file !== undefined || item.permissions !== undefined || item.colors !== undefined)) bad.push('icon pack fields');
|
|
99
122
|
if (item.kind === 'plugin') {
|
|
100
123
|
if (!validFileName(item.file) || item.colors != null) bad.push('plugin file');
|
|
101
124
|
const p = item.permissions;
|
|
@@ -118,7 +141,7 @@ export function catalogProblems(catalog, { app, hosts = DEFAULT_HOSTS } = {}) {
|
|
|
118
141
|
const out = [];
|
|
119
142
|
if (!catalog || typeof catalog !== 'object') return ['catalog: not an object'];
|
|
120
143
|
for (const key of Object.keys(catalog)) if (!['format', 'name', 'generatedAt', 'items'].includes(key)) out.push(`catalog: unknown field ${key}`);
|
|
121
|
-
if (catalog.format !== CATALOG_FORMAT) out.push(`catalog: format must be ${CATALOG_FORMAT}`);
|
|
144
|
+
if (catalog.format !== CATALOG_FORMAT && catalog.format !== CATALOG_FORMAT_NEXT) out.push(`catalog: format must be ${CATALOG_FORMAT} or ${CATALOG_FORMAT_NEXT}`);
|
|
122
145
|
if (typeof catalog.name !== 'string' || typeof catalog.generatedAt !== 'string') out.push('catalog: name and generatedAt are strings');
|
|
123
146
|
if (!Array.isArray(catalog.items)) return [...out, 'catalog: items must be a list'];
|
|
124
147
|
if (catalog.items.length > MAX_ITEMS) out.push(`catalog: more than ${MAX_ITEMS} items`);
|
|
@@ -127,6 +150,7 @@ export function catalogProblems(catalog, { app, hosts = DEFAULT_HOSTS } = {}) {
|
|
|
127
150
|
for (const item of catalog.items) {
|
|
128
151
|
const where = `item ${item?.kind}:${item?.id}`;
|
|
129
152
|
for (const problem of itemProblems(item, app && item?.apps?.includes(app) ? hosts : undefined)) out.push(`${where}: invalid ${problem}`);
|
|
153
|
+
if (catalog.format === CATALOG_FORMAT && !LEGACY_KINDS.includes(item?.kind)) out.push(`${where}: a format ${CATALOG_FORMAT} catalog lists only ${LEGACY_KINDS.join(', ')} (older apps refuse the whole file otherwise); this kind belongs in ${CATALOG_FILE_NEXT}`);
|
|
130
154
|
for (const a of Array.isArray(item?.apps) ? item.apps : []) {
|
|
131
155
|
const id = `${a}/${item.kind}/${item.id}`;
|
|
132
156
|
if (ids.has(id)) out.push(`${where}: duplicate for app ${a}`);
|
|
@@ -146,12 +170,15 @@ export function catalogProblems(catalog, { app, hosts = DEFAULT_HOSTS } = {}) {
|
|
|
146
170
|
/**
|
|
147
171
|
* Builds `config.app`'s part of the shared catalog in `out`: writes the files the config returns,
|
|
148
172
|
* replaces the app's items (other apps' items stay as they are), removes files only the app's old
|
|
149
|
-
* items used, validates the whole catalog and writes `catalog.json` (
|
|
173
|
+
* items used, validates the whole catalog and writes `catalog.v3.json` (every item) and `catalog.json`
|
|
174
|
+
* (the kinds older apps know), sign both afterwards.
|
|
150
175
|
*
|
|
151
176
|
* config: { app, name?, base?, out?, hosts?, items(context) → [{ kind, id, name, author, version,
|
|
152
177
|
* description, category, tags, file (plugins), permissions (plugins), colors (themes), language
|
|
153
178
|
* (string packs), license, credits,
|
|
154
179
|
* path ('plugins/x.plugin.js'), bytes (Buffer|string), preview?: { path, bytes } }] }
|
|
180
|
+
* Kinds: theme, plugin, strings, icons (an icon pack, `icons/<id>.icons.json`) and builtin (a
|
|
181
|
+
* feature that ships inside the app, shown in the Store as built in: no path or bytes).
|
|
155
182
|
*/
|
|
156
183
|
export async function buildStore(config, options = {}) {
|
|
157
184
|
const app = config.app;
|
|
@@ -160,11 +187,14 @@ export async function buildStore(config, options = {}) {
|
|
|
160
187
|
if (!/^https:\/\/[^/]+\/(.*\/)?$/.test(base)) throw new Error('base must be an https URL ending in /');
|
|
161
188
|
const out = resolve(options.out ?? config.out ?? '.');
|
|
162
189
|
const hosts = config.hosts ?? DEFAULT_HOSTS;
|
|
163
|
-
const catalogPath = join(out,
|
|
164
|
-
const
|
|
190
|
+
const catalogPath = join(out, CATALOG_FILE);
|
|
191
|
+
const nextPath = join(out, CATALOG_FILE_NEXT);
|
|
192
|
+
// The full list (format 3) is what to start from: it has every app's items of every kind.
|
|
193
|
+
const source = existsSync(nextPath) ? nextPath : catalogPath;
|
|
194
|
+
const previous = existsSync(source) ? JSON.parse(readFileSync(source, 'utf8')) : null;
|
|
165
195
|
const kept = [];
|
|
166
196
|
const oldFiles = new Set();
|
|
167
|
-
for (const item of previous?.format === CATALOG_FORMAT ? previous.items : []) {
|
|
197
|
+
for (const item of previous?.format === CATALOG_FORMAT || previous?.format === CATALOG_FORMAT_NEXT ? previous.items : []) {
|
|
168
198
|
if (!item.apps?.includes(app)) { kept.push(item); continue; }
|
|
169
199
|
for (const url of [item.url, item.preview]) if (url?.startsWith(base)) oldFiles.add(url.slice(base.length));
|
|
170
200
|
const others = item.apps.filter(a => a !== app);
|
|
@@ -181,12 +211,16 @@ export async function buildStore(config, options = {}) {
|
|
|
181
211
|
const files = new Map();
|
|
182
212
|
const toBytes = value => (Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8'));
|
|
183
213
|
for (const entry of entries) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
214
|
+
const item = { apps: [app], kind: entry.kind, id: entry.id, name: entry.name, author: entry.author ?? '', version: entry.version, description: entry.description ?? '', category: entry.category ?? '', tags: entry.tags ?? [] };
|
|
215
|
+
// A built-in item has no file: the app already has it.
|
|
216
|
+
if (entry.kind !== 'builtin') {
|
|
217
|
+
const bytes = toBytes(entry.bytes);
|
|
218
|
+
files.set(entry.path, { target: inside(entry.path), bytes });
|
|
219
|
+
item.url = `${base}${entry.path}`;
|
|
220
|
+
if (entry.kind === 'plugin') item.file = entry.file;
|
|
221
|
+
item.sha256 = sha256(bytes);
|
|
222
|
+
item.size = bytes.length;
|
|
223
|
+
}
|
|
190
224
|
if (entry.preview) {
|
|
191
225
|
files.set(entry.preview.path, { target: inside(entry.preview.path), bytes: toBytes(entry.preview.bytes) });
|
|
192
226
|
item.preview = `${base}${entry.preview.path}`;
|
|
@@ -199,23 +233,27 @@ export async function buildStore(config, options = {}) {
|
|
|
199
233
|
items.push(item);
|
|
200
234
|
}
|
|
201
235
|
const catalog = {
|
|
202
|
-
format:
|
|
236
|
+
format: CATALOG_FORMAT_NEXT,
|
|
203
237
|
name: config.name ?? previous?.name ?? 'wayBetter Store',
|
|
204
238
|
generatedAt: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
205
239
|
items: [...kept, ...items],
|
|
206
240
|
};
|
|
207
241
|
// Nothing is written unless the whole catalog is valid.
|
|
208
|
-
|
|
242
|
+
// The same items twice: everything for apps that know the newer kinds, and what every app knows.
|
|
243
|
+
const legacy = { ...catalog, format: CATALOG_FORMAT, items: catalog.items.filter(item => LEGACY_KINDS.includes(item.kind)) };
|
|
244
|
+
const problems = [...catalogProblems(catalog, { app, hosts }), ...catalogProblems(legacy, { app, hosts })];
|
|
209
245
|
if (problems.length) throw new Error(`the catalog would be invalid:\n ${problems.join('\n ')}`);
|
|
210
246
|
const json = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
247
|
+
const legacyJson = `${JSON.stringify(legacy, null, 2)}\n`;
|
|
211
248
|
if (Buffer.byteLength(json) > MAX_CATALOG_BYTES) throw new Error('the catalog is larger than apps accept (1 MiB)');
|
|
212
249
|
for (const { target, bytes } of files.values()) { mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, bytes); }
|
|
213
250
|
// Files this app's previous build wrote and no item uses any more (never another app's files).
|
|
214
251
|
const stillUsed = new Set(kept.flatMap(item => [item.url, item.preview]).filter(url => url?.startsWith(base)).map(url => url.slice(base.length)));
|
|
215
252
|
for (const path of oldFiles) if (!files.has(path) && !stillUsed.has(path)) rmSync(inside(path), { force: true });
|
|
216
253
|
mkdirSync(out, { recursive: true });
|
|
217
|
-
writeFileSync(
|
|
218
|
-
|
|
254
|
+
writeFileSync(nextPath, json);
|
|
255
|
+
writeFileSync(catalogPath, legacyJson);
|
|
256
|
+
return { catalog, legacy, path: catalogPath, nextPath, items };
|
|
219
257
|
}
|
|
220
258
|
|
|
221
259
|
// ---- Keys and signatures ----------------------------------------------------------------------
|
|
@@ -286,6 +324,12 @@ function parse(argv) {
|
|
|
286
324
|
return out;
|
|
287
325
|
}
|
|
288
326
|
|
|
327
|
+
/** `catalog.json` and, next to it, `catalog.v3.json` when it exists (one source, two files); any other file alone. */
|
|
328
|
+
export function catalogFiles(file) {
|
|
329
|
+
const next = join(dirname(file), CATALOG_FILE_NEXT);
|
|
330
|
+
return file.endsWith(CATALOG_FILE) && !file.endsWith(CATALOG_FILE_NEXT) && existsSync(next) ? [file, next] : [file];
|
|
331
|
+
}
|
|
332
|
+
|
|
289
333
|
/** Runs `waybetter store …`; returns the lines to print. Throws on errors. */
|
|
290
334
|
export async function storeCommand(argv) {
|
|
291
335
|
const [command, ...rest] = argv;
|
|
@@ -297,7 +341,7 @@ export async function storeCommand(argv) {
|
|
|
297
341
|
const config = (await import(pathToFileURL(resolve(options.config)).href)).default;
|
|
298
342
|
const { path, items } = await buildStore(config, { out: options.out, base: options.base });
|
|
299
343
|
const count = kind => items.filter(item => item.kind === kind).length;
|
|
300
|
-
return [`store: ${config.app}: ${count('plugin')} plugins, ${count('theme')} themes → ${path}`, 'Now sign
|
|
344
|
+
return [`store: ${config.app}: ${count('plugin')} plugins, ${count('theme')} themes, ${count('icons')} icon packs, ${count('builtin')} built-in → ${path} and ${CATALOG_FILE_NEXT}`, 'Now sign both: waybetter store sign <catalog.json> --key <private key>'];
|
|
301
345
|
}
|
|
302
346
|
if (command === 'sign') {
|
|
303
347
|
if (!key) throw new Error('sign needs --key (or WAYBETTER_STORE_KEY)');
|
|
@@ -307,18 +351,26 @@ export async function storeCommand(argv) {
|
|
|
307
351
|
return [`${created ? 'Created the store key' : 'Store key already exists'}; public key ${publicKey} → ${options.pub[0]}`];
|
|
308
352
|
}
|
|
309
353
|
if (!existsSync(key)) throw new Error(`no store key at ${key} (create it once with --init)`);
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
354
|
+
const lines = [];
|
|
355
|
+
for (const file of catalogFiles(catalog)) {
|
|
356
|
+
const publicKey = signFile(file, key);
|
|
357
|
+
const keys = options.pub.length ? options.pub.map(pub => readFileSync(pub, 'utf8').trim()) : [publicKey];
|
|
358
|
+
if (!verifyFile(file, keys)) throw new Error(`signature of ${file} does NOT verify with ${options.pub.join(', ') || 'the key'}`);
|
|
359
|
+
lines.push(`Signed and verified ${file}.sig`);
|
|
360
|
+
}
|
|
361
|
+
return lines;
|
|
314
362
|
}
|
|
315
363
|
if (command === 'verify') {
|
|
316
364
|
if (!options.pub.length) throw new Error('verify needs --pub');
|
|
317
365
|
const keys = options.pub.map(file => readFileSync(file, 'utf8').trim());
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
366
|
+
const lines = [];
|
|
367
|
+
for (const file of catalogFiles(catalog)) {
|
|
368
|
+
if (!existsSync(`${file}.sig`) || !verifyFile(file, keys)) throw new Error(`signature of ${file} does NOT verify with ${options.pub.join(', ')}`);
|
|
369
|
+
const problems = catalogProblems(JSON.parse(readFileSync(file, 'utf8')), { app: options.app });
|
|
370
|
+
if (problems.length) throw new Error(`${file} is signed but invalid:\n ${problems.join('\n ')}`);
|
|
371
|
+
lines.push(`Verified ${file}.sig`);
|
|
372
|
+
}
|
|
373
|
+
return lines;
|
|
322
374
|
}
|
|
323
375
|
throw new Error(USAGE);
|
|
324
376
|
}
|
package/cli/strings.mjs
CHANGED
|
@@ -77,6 +77,8 @@ export async function checkPack(packFile, inventories, { verbose = false } = {})
|
|
|
77
77
|
const coverage = rules.coverage(match.inventory, compiled);
|
|
78
78
|
lines.push(`${pack.name} (${pack.language}) for ${pack.site}/${pack.catalog}, inventory build ${match.inventory.build ?? 'unknown'}`);
|
|
79
79
|
lines.push(`${report.total} strings: ${report.valid} valid, ${report.rejected.length} refused, ${report.unknown.length} unknown`);
|
|
80
|
+
if (pack.when?.languages) lines.push(`Applies only while the site shows: ${pack.when.languages.join(', ')}`);
|
|
81
|
+
if (pack.rewrite) lines.push(`Rewords ${Object.keys(pack.rewrite.words).length} words inside the site's own text: ${Object.keys(pack.rewrite.words).slice(0, 6).join(', ')}${Object.keys(pack.rewrite.words).length > 6 ? ', …' : ''}`);
|
|
80
82
|
const list = (items, render) => {
|
|
81
83
|
for (const item of verbose ? items : items.slice(0, LISTED)) lines.push(` ${render(item)}`);
|
|
82
84
|
if (!verbose && items.length > LISTED) lines.push(` … ${items.length - LISTED} more (--verbose lists all)`);
|
|
@@ -27,7 +27,7 @@ export function installStringPacks(win, setups, options = {}) {
|
|
|
27
27
|
refused.push(error instanceof Error ? error.message : String(error));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
const usable = packs.filter(pack => pack.values.size > 0);
|
|
30
|
+
const usable = packs.filter(pack => pack.values.size > 0 || pack.rewrite);
|
|
31
31
|
return {
|
|
32
32
|
catalog: setup.adapter.catalog, adapter: setup.adapter, packs, refused, usable,
|
|
33
33
|
engine: usable.length || options.mark ? createStringEngine(setup.inventory, usable) : null,
|
|
@@ -66,7 +66,7 @@ export function installStringPacks(win, setups, options = {}) {
|
|
|
66
66
|
continue;
|
|
67
67
|
const sink = {
|
|
68
68
|
seen(group = 'main') { state.seen[group] = (state.seen[group] ?? 0) + 1; },
|
|
69
|
-
matched(language) { state.languages.add(String(language)); },
|
|
69
|
+
matched(language) { state.languages.add(String(language)); engine.setLanguage(String(language)); },
|
|
70
70
|
resolve(id, original) {
|
|
71
71
|
try {
|
|
72
72
|
state.keys += 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stringPackHook.js","sourceRoot":"","sources":["../../../runtime/src/stringPackHook.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,0FAA0F;AAC1F,8FAA8F;AAC9F,6CAA6C;AAC7C,EAAE;AACF,iGAAiG;AACjG,8FAA8F;AAC9F,+EAA+E;AAC/E,EAAE;AACF,kGAAkG;AAClG,4DAA4D;AAC5D,OAAO,EACL,WAAW,EAAE,kBAAkB,GAEhC,MAAM,kBAAkB,CAAC;AAuE1B;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,MAA+B,EAAE,UAA8B,EAAE;IAC/G,MAAM,MAAM,GAAG,GAA6B,CAAC;IAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;QAChC,MAAM,KAAK,GAAmB,EAAE,CAAC;QACjC,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QAC1D,OAAO;YACL,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM;YAC9E,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI;YAC1F,IAAI,EAAE,EAA4B,EAAE,SAAS,EAAE,IAAI,GAAG,EAAU,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK;YACtG,MAAM,EAAE,IAA2B;SACpC,CAAC;IACJ,CAAC,CAAC,CAAC;IAGH,MAAM,MAAM,GAAG,CAAC,KAAY,EAAc,EAAE;QAC1C,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QACrC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC;IACvE,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,CAAC,KAAY,EAAiB,EAAE,CAAC,CAAC;QACjD,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC;QACrB,SAAS,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC;QAC/B,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE;QACvB,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACnG,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC;QAC3B,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;KACvF,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,KAAY,EAAE,EAAE;QAC5B,IAAI,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,SAAS;YAAE,OAAO;QACtD,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC;YAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,wCAAwC,CAAC,CAAC;IACjG,CAAC,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,MAAM,IAAI,GAAgB;YACxB,IAAI,CAAC,KAAK,GAAG,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1E,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5D,OAAO,CAAC,EAAE,EAAE,QAAQ;gBAClB,IAAI,CAAC;oBACH,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;oBAChB,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;oBAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;oBACnE,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;wBAAE,IAAI,CAAC,KAAK,CAAC,CAAC;oBAClC,OAAO,KAAK,CAAC;gBACf,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,QAAQ,CAAC;gBAClB,CAAC;YACH,CAAC;SACF,CAAC;QACF,IAAI,CAAC;YACH,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM;QACzB,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;YAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC;QAC5H,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,EAAE;YAChB,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;YAC/F,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3C,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,MAAM,IAAI,SAAS;QACrF,OAAO;YACL,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,CAAC,YAAY,CAAC,KAAc,CAAC,CAAC;YACxD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,iCAAiC,CAAC,CAAC;gBACrE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;YACtB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAYD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,IAAY,EAAE,OAA+C;IACxG,MAAM,MAAM,GAAG,GAA2F,CAAC;IAC3G,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAc,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACnD,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IACnC,IAAI,OAAO,GAAoC,KAAK,CAAC,IAAI,CAAC;IAC1D,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,IAAI,GAAG,CAAC,KAAyB,EAAE,EAAE;QACzC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YAC3B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,SAAS;YAC7E,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAChB,IAAI,CAAC;gBACH,OAAO,CAAC,KAAc,EAAE,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;YACrE,CAAC;YAAC,MAAM,CAAC;gBACP,+DAA+D;YACjE,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE;YACnC,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,KAAK;YACjB,GAAG;gBACD,MAAM,MAAM,GAAG,OAAO,CAAC;gBACvB,OAAO,SAAS,IAAI,CAAgB,GAAG,KAAgB;oBACrD,IAAI,CAAC,KAAK,CAAC,CAAC;oBACZ,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACnC,CAAC,CAAC;YACJ,CAAC;YACD,GAAG,CAAC,IAAqC,IAAI,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;SAC/D,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,GAAG,EAAE;QACV,MAAM,GAAG,KAAK,CAAC;QACf,IAAI,CAAC;YACH,IAAI,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC,IAAI;gBAAE,OAAQ,KAA4B,CAAC,IAAI,CAAC;;gBAC3E,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACvH,CAAC;QAAC,MAAM,CAAC;YACP,oCAAoC;QACtC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAY,EAAE,GAAkC;IAC/E,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO;IAC5D,MAAM,KAAK,GAAG,OAAkC,CAAC;IACjD,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1B,IAAI,OAAO,OAAO,KAAK,UAAU;YAAE,SAAS;QAC5C,KAAK,CAAC,EAAE,CAAC,GAAG,UAAyB,MAAe,EAAE,OAAgB,EAAE,OAAgB,EAAE,GAAG,IAAe;YAC1G,IAAI,OAAO,GAAG,OAAO,CAAC;YACtB,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;gBAClC,IAAI,CAAC;oBACH,OAAO,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE;wBAC3B,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI;4BACtB,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAsC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;4BACjF,IAAI,CAAC;gCACH,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;gCAC7B,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;4BACpD,CAAC;4BAAC,MAAM,CAAC;gCACP,OAAO,MAAM,CAAC;4BAChB,CAAC;wBACH,CAAC;qBACF,CAAC,CAAC;gBACL,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,GAAG,OAAO,CAAC;gBACpB,CAAC;YACH,CAAC;YACD,OAAQ,OAAwC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;QACjG,CAAC,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["// String packs in the page (published as `waybetter/runtime/stringPackHook`): the per-site\n// \"catalog hook adapter\" interface, the installer that connects adapters to the engine of\n// stringPacks.ts, a self-check that says when a hook did not match, and the generic half of a\n// common hook point (a webpack chunk array).\n//\n// How a site hands its strings to its own code is that site's implementation detail, so the hook\n// is an adapter the app supplies. One site can have several catalogs (different builds of the\n// site, different key spaces); each gets its own adapter, inventory and packs.\n//\n// Everything here fails open: an exception inside a wrapper leaves the site's own behaviour as it\n// was, and with no pack installed nothing is hooked at all.\nimport {\n compilePack, createStringEngine, type CompiledPack, type EngineStats, type PackReport, type PageInventory, type StringEngine,\n type StringInventory,\n} from './stringPacks.ts';\n\n// -------------------------------------------------------------------------------------------\n// The adapter interface\n\n/** What an adapter reports to and asks from the framework. None of these throw. */\nexport type CatalogSink = {\n /** A catalog file of this adapter was seen loading (whether or not the hook then matches). */\n seen(group?: string): void;\n /** The hook matched: the site is about to register values in `language`. */\n matched(language: string, group?: string): void;\n /** The value to register for `id`: a pack's, or exactly `original`. */\n resolve(id: string, original: unknown): unknown;\n};\n\nexport type CatalogHookAdapter = {\n /** The catalog (key space) this adapter hooks; packs and the inventory name the same one. */\n catalog: string;\n /** Installs the hook at document start; returns its removal. Must not change the site's objects. */\n install(win: Window, sink: CatalogSink): () => void;\n};\n\nexport type CatalogSetup = {\n adapter: CatalogHookAdapter;\n inventory: StringInventory | PageInventory;\n /** Pack documents in application order (a later one wins). Untrusted data: validated here. */\n packs: readonly unknown[];\n};\n\n/**\n * `ok`: the hook matched and keys went through it.\n * `not-matched`: a catalog file was seen but the hook did not match it (the site changed).\n * `no-catalog`: no catalog file was seen at all (another build of the site, or not loaded yet).\n */\nexport type HookStatus = 'pending' | 'ok' | 'not-matched' | 'no-catalog';\n\nexport type CatalogReport = {\n catalog: string;\n status: HookStatus;\n /** Languages the site registered through the hook. */\n languages: string[];\n /** Catalog files seen, per group. */\n seen: Record<string, number>;\n packs: { name: string; language: string; report: PackReport }[];\n /** Pack documents that were refused as a whole. */\n refused: string[];\n engine: EngineStats;\n};\n\nexport type StringPacksHandle = {\n status(catalog?: string): HookStatus;\n report(): CatalogReport[];\n /** The engines, per catalog (the originals they keep are what a restore needs). */\n engine(catalog: string): StringEngine | undefined;\n dispose(): void;\n};\n\nexport type StringPacksOptions = {\n /** When the self-check decides (default 15 s after install). */\n settleMs?: number;\n /** Called once per catalog when its status is decided (`ok` as soon as it is reached). */\n onStatus?: (report: CatalogReport) => void;\n /**\n * Translate mode: every value passes through here after the packs (the marker of\n * stringPackTranslate.ts), and the catalog is hooked even without a usable pack.\n */\n mark?: (id: string, value: unknown) => unknown;\n};\n\ntype TimerWindow = { setTimeout(handler: () => void, ms: number): unknown; clearTimeout(id: never): void };\n\n/**\n * Installs the adapters that have at least one usable pack. A catalog without packs is not hooked\n * (the site runs untouched), so its status stays `pending`.\n */\nexport function installStringPacks(win: Window, setups: readonly CatalogSetup[], options: StringPacksOptions = {}): StringPacksHandle {\n const timers = win as unknown as TimerWindow;\n const states = setups.map(setup => {\n const packs: CompiledPack[] = [];\n const refused: string[] = [];\n for (const document of setup.packs) {\n try {\n packs.push(compilePack(document, setup.inventory));\n } catch (error) {\n refused.push(error instanceof Error ? error.message : String(error));\n }\n }\n const usable = packs.filter(pack => pack.values.size > 0);\n return {\n catalog: setup.adapter.catalog, adapter: setup.adapter, packs, refused, usable,\n engine: usable.length || options.mark ? createStringEngine(setup.inventory, usable) : null,\n seen: {} as Record<string, number>, languages: new Set<string>(), keys: 0, settled: false, told: false,\n remove: null as null | (() => void),\n };\n });\n type State = (typeof states)[number];\n\n const status = (state: State): HookStatus => {\n if (state.languages.size && state.keys > 0) return 'ok';\n if (!state.settled) return 'pending';\n return Object.keys(state.seen).length ? 'not-matched' : 'no-catalog';\n };\n const reportOf = (state: State): CatalogReport => ({\n catalog: state.catalog,\n status: status(state),\n languages: [...state.languages],\n seen: { ...state.seen },\n packs: state.packs.map(pack => ({ name: pack.name, language: pack.language, report: pack.report })),\n refused: [...state.refused],\n engine: state.engine?.stats() ?? { seen: 0, applied: 0, unlisted: [], mismatched: [] },\n });\n const tell = (state: State) => {\n if (state.told || status(state) === 'pending') return;\n state.told = true;\n try { options.onStatus?.(reportOf(state)); } catch { /* a reporter must not break the page */ }\n };\n\n for (const state of states) {\n const engine = state.engine;\n if (!engine) continue;\n const sink: CatalogSink = {\n seen(group = 'main') { state.seen[group] = (state.seen[group] ?? 0) + 1; },\n matched(language) { state.languages.add(String(language)); },\n resolve(id, original) {\n try {\n state.keys += 1;\n const resolved = engine.resolve(id, original);\n const value = options.mark ? options.mark(id, resolved) : resolved;\n if (state.keys === 1) tell(state);\n return value;\n } catch {\n return original;\n }\n },\n };\n try {\n state.remove = state.adapter.install(win, sink);\n } catch {\n state.remove = null;\n }\n }\n\n const hooked = states.filter(state => state.engine);\n const timer = hooked.length\n ? timers.setTimeout(() => { for (const state of hooked) { state.settled = true; tell(state); } }, options.settleMs ?? 15000)\n : null;\n\n return {\n status: catalog => {\n const state = catalog ? states.find(item => item.catalog === catalog) : hooked[0] ?? states[0];\n return state ? status(state) : 'pending';\n },\n report: () => states.map(reportOf),\n engine: catalog => states.find(item => item.catalog === catalog)?.engine ?? undefined,\n dispose() {\n if (timer !== null) timers.clearTimeout(timer as never);\n for (const state of states) {\n try { state.remove?.(); } catch { /* the hook is gone either way */ }\n state.remove = null;\n }\n },\n };\n}\n\n// -------------------------------------------------------------------------------------------\n// A webpack chunk array as the hook point\n//\n// A webpack site loads its code as chunks pushed onto a global array\n// (`(globalThis.webpackChunk_x ||= []).push([ids, modules, runtime?])`); webpack's runtime later\n// replaces the array's `push` with its own and replays what is already there. Watching the array\n// therefore sees every chunk before its modules are installed, whichever loads first.\n\ntype Chunk = [unknown, Record<string, unknown> | unknown[], ...unknown[]];\n\n/**\n * Calls `onChunk(chunk, script)` for every chunk pushed onto `win[name]`, before the site's own\n * `push` gets it; `script` is the URL of the script doing the push (`''` when unknown). Install at\n * document start. The array and webpack's own `push` replacement keep working unchanged: the\n * wrapper calls exactly the `push` that was current when it was read, with the same arguments. An\n * exception in `onChunk` is swallowed. Returns the removal.\n */\nexport function watchChunkArray(win: object, name: string, onChunk: (chunk: Chunk, script: string) => void): () => void {\n const holder = win as Record<string, unknown> & { document?: { currentScript?: { src?: string } | null } };\n const existing = holder[name];\n const array: unknown[] = Array.isArray(existing) ? existing : [];\n if (!Array.isArray(existing)) holder[name] = array;\n const done = new WeakSet<object>();\n let current: (...items: unknown[]) => number = array.push;\n let active = true;\n const look = (items: ArrayLike<unknown>) => {\n if (!active) return;\n for (let index = 0; index < items.length; index += 1) {\n const chunk = items[index];\n if (typeof chunk !== 'object' || chunk === null || done.has(chunk)) continue;\n done.add(chunk);\n try {\n onChunk(chunk as Chunk, holder.document?.currentScript?.src ?? '');\n } catch {\n // Fail open: the chunk goes on to the site exactly as it came.\n }\n }\n };\n try {\n Object.defineProperty(array, 'push', {\n configurable: true,\n enumerable: false,\n get() {\n const target = current;\n return function push(this: unknown, ...items: unknown[]) {\n look(items);\n return target.apply(this, items);\n };\n },\n set(next: (...items: unknown[]) => number) { current = next; },\n });\n } catch {\n return () => {};\n }\n return () => {\n active = false;\n try {\n if (current === Array.prototype.push) delete (array as { push?: unknown }).push;\n else Object.defineProperty(array, 'push', { configurable: true, enumerable: false, writable: true, value: current });\n } catch {\n // Left as the pass-through wrapper.\n }\n };\n}\n\n/**\n * Wraps the module factories of one chunk so that what they `require` passes through `map`:\n * `map(exports)` returns a replacement for that one call, or `undefined` to hand out the module's\n * own exports. The module itself is never changed (return a shadow, e.g. `Object.create(exports)`).\n * Only call this for a chunk you have identified; other chunks must stay untouched.\n */\nexport function mapChunkRequires(chunk: Chunk, map: (exports: unknown) => unknown): void {\n const modules = chunk[1];\n if (typeof modules !== 'object' || modules === null) return;\n const table = modules as Record<string, unknown>;\n for (const id of Object.keys(table)) {\n const factory = table[id];\n if (typeof factory !== 'function') continue;\n table[id] = function (this: unknown, module: unknown, exports: unknown, require: unknown, ...rest: unknown[]) {\n let through = require;\n if (typeof require === 'function') {\n try {\n through = new Proxy(require, {\n apply(target, self, args) {\n const result = Reflect.apply(target as (...a: unknown[]) => unknown, self, args);\n try {\n const replaced = map(result);\n return replaced === undefined ? result : replaced;\n } catch {\n return result;\n }\n },\n });\n } catch {\n through = require;\n }\n }\n return (factory as (...a: unknown[]) => unknown).call(this, module, exports, through, ...rest);\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stringPackHook.js","sourceRoot":"","sources":["../../../runtime/src/stringPackHook.ts"],"names":[],"mappings":"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","sourcesContent":["// String packs in the page (published as `waybetter/runtime/stringPackHook`): the per-site\n// \"catalog hook adapter\" interface, the installer that connects adapters to the engine of\n// stringPacks.ts, a self-check that says when a hook did not match, and the generic half of a\n// common hook point (a webpack chunk array).\n//\n// How a site hands its strings to its own code is that site's implementation detail, so the hook\n// is an adapter the app supplies. One site can have several catalogs (different builds of the\n// site, different key spaces); each gets its own adapter, inventory and packs.\n//\n// Everything here fails open: an exception inside a wrapper leaves the site's own behaviour as it\n// was, and with no pack installed nothing is hooked at all.\nimport {\n compilePack, createStringEngine, type CompiledPack, type EngineStats, type PackReport, type PageInventory, type StringEngine,\n type StringInventory,\n} from './stringPacks.ts';\n\n// -------------------------------------------------------------------------------------------\n// The adapter interface\n\n/** What an adapter reports to and asks from the framework. None of these throw. */\nexport type CatalogSink = {\n /** A catalog file of this adapter was seen loading (whether or not the hook then matches). */\n seen(group?: string): void;\n /** The hook matched: the site is about to register values in `language`. */\n matched(language: string, group?: string): void;\n /** The value to register for `id`: a pack's, or exactly `original`. */\n resolve(id: string, original: unknown): unknown;\n};\n\nexport type CatalogHookAdapter = {\n /** The catalog (key space) this adapter hooks; packs and the inventory name the same one. */\n catalog: string;\n /** Installs the hook at document start; returns its removal. Must not change the site's objects. */\n install(win: Window, sink: CatalogSink): () => void;\n};\n\nexport type CatalogSetup = {\n adapter: CatalogHookAdapter;\n inventory: StringInventory | PageInventory;\n /** Pack documents in application order (a later one wins). Untrusted data: validated here. */\n packs: readonly unknown[];\n};\n\n/**\n * `ok`: the hook matched and keys went through it.\n * `not-matched`: a catalog file was seen but the hook did not match it (the site changed).\n * `no-catalog`: no catalog file was seen at all (another build of the site, or not loaded yet).\n */\nexport type HookStatus = 'pending' | 'ok' | 'not-matched' | 'no-catalog';\n\nexport type CatalogReport = {\n catalog: string;\n status: HookStatus;\n /** Languages the site registered through the hook. */\n languages: string[];\n /** Catalog files seen, per group. */\n seen: Record<string, number>;\n packs: { name: string; language: string; report: PackReport }[];\n /** Pack documents that were refused as a whole. */\n refused: string[];\n engine: EngineStats;\n};\n\nexport type StringPacksHandle = {\n status(catalog?: string): HookStatus;\n report(): CatalogReport[];\n /** The engines, per catalog (the originals they keep are what a restore needs). */\n engine(catalog: string): StringEngine | undefined;\n dispose(): void;\n};\n\nexport type StringPacksOptions = {\n /** When the self-check decides (default 15 s after install). */\n settleMs?: number;\n /** Called once per catalog when its status is decided (`ok` as soon as it is reached). */\n onStatus?: (report: CatalogReport) => void;\n /**\n * Translate mode: every value passes through here after the packs (the marker of\n * stringPackTranslate.ts), and the catalog is hooked even without a usable pack.\n */\n mark?: (id: string, value: unknown) => unknown;\n};\n\ntype TimerWindow = { setTimeout(handler: () => void, ms: number): unknown; clearTimeout(id: never): void };\n\n/**\n * Installs the adapters that have at least one usable pack. A catalog without packs is not hooked\n * (the site runs untouched), so its status stays `pending`.\n */\nexport function installStringPacks(win: Window, setups: readonly CatalogSetup[], options: StringPacksOptions = {}): StringPacksHandle {\n const timers = win as unknown as TimerWindow;\n const states = setups.map(setup => {\n const packs: CompiledPack[] = [];\n const refused: string[] = [];\n for (const document of setup.packs) {\n try {\n packs.push(compilePack(document, setup.inventory));\n } catch (error) {\n refused.push(error instanceof Error ? error.message : String(error));\n }\n }\n const usable = packs.filter(pack => pack.values.size > 0 || pack.rewrite);\n return {\n catalog: setup.adapter.catalog, adapter: setup.adapter, packs, refused, usable,\n engine: usable.length || options.mark ? createStringEngine(setup.inventory, usable) : null,\n seen: {} as Record<string, number>, languages: new Set<string>(), keys: 0, settled: false, told: false,\n remove: null as null | (() => void),\n };\n });\n type State = (typeof states)[number];\n\n const status = (state: State): HookStatus => {\n if (state.languages.size && state.keys > 0) return 'ok';\n if (!state.settled) return 'pending';\n return Object.keys(state.seen).length ? 'not-matched' : 'no-catalog';\n };\n const reportOf = (state: State): CatalogReport => ({\n catalog: state.catalog,\n status: status(state),\n languages: [...state.languages],\n seen: { ...state.seen },\n packs: state.packs.map(pack => ({ name: pack.name, language: pack.language, report: pack.report })),\n refused: [...state.refused],\n engine: state.engine?.stats() ?? { seen: 0, applied: 0, unlisted: [], mismatched: [] },\n });\n const tell = (state: State) => {\n if (state.told || status(state) === 'pending') return;\n state.told = true;\n try { options.onStatus?.(reportOf(state)); } catch { /* a reporter must not break the page */ }\n };\n\n for (const state of states) {\n const engine = state.engine;\n if (!engine) continue;\n const sink: CatalogSink = {\n seen(group = 'main') { state.seen[group] = (state.seen[group] ?? 0) + 1; },\n matched(language) { state.languages.add(String(language)); engine.setLanguage(String(language)); },\n resolve(id, original) {\n try {\n state.keys += 1;\n const resolved = engine.resolve(id, original);\n const value = options.mark ? options.mark(id, resolved) : resolved;\n if (state.keys === 1) tell(state);\n return value;\n } catch {\n return original;\n }\n },\n };\n try {\n state.remove = state.adapter.install(win, sink);\n } catch {\n state.remove = null;\n }\n }\n\n const hooked = states.filter(state => state.engine);\n const timer = hooked.length\n ? timers.setTimeout(() => { for (const state of hooked) { state.settled = true; tell(state); } }, options.settleMs ?? 15000)\n : null;\n\n return {\n status: catalog => {\n const state = catalog ? states.find(item => item.catalog === catalog) : hooked[0] ?? states[0];\n return state ? status(state) : 'pending';\n },\n report: () => states.map(reportOf),\n engine: catalog => states.find(item => item.catalog === catalog)?.engine ?? undefined,\n dispose() {\n if (timer !== null) timers.clearTimeout(timer as never);\n for (const state of states) {\n try { state.remove?.(); } catch { /* the hook is gone either way */ }\n state.remove = null;\n }\n },\n };\n}\n\n// -------------------------------------------------------------------------------------------\n// A webpack chunk array as the hook point\n//\n// A webpack site loads its code as chunks pushed onto a global array\n// (`(globalThis.webpackChunk_x ||= []).push([ids, modules, runtime?])`); webpack's runtime later\n// replaces the array's `push` with its own and replays what is already there. Watching the array\n// therefore sees every chunk before its modules are installed, whichever loads first.\n\ntype Chunk = [unknown, Record<string, unknown> | unknown[], ...unknown[]];\n\n/**\n * Calls `onChunk(chunk, script)` for every chunk pushed onto `win[name]`, before the site's own\n * `push` gets it; `script` is the URL of the script doing the push (`''` when unknown). Install at\n * document start. The array and webpack's own `push` replacement keep working unchanged: the\n * wrapper calls exactly the `push` that was current when it was read, with the same arguments. An\n * exception in `onChunk` is swallowed. Returns the removal.\n */\nexport function watchChunkArray(win: object, name: string, onChunk: (chunk: Chunk, script: string) => void): () => void {\n const holder = win as Record<string, unknown> & { document?: { currentScript?: { src?: string } | null } };\n const existing = holder[name];\n const array: unknown[] = Array.isArray(existing) ? existing : [];\n if (!Array.isArray(existing)) holder[name] = array;\n const done = new WeakSet<object>();\n let current: (...items: unknown[]) => number = array.push;\n let active = true;\n const look = (items: ArrayLike<unknown>) => {\n if (!active) return;\n for (let index = 0; index < items.length; index += 1) {\n const chunk = items[index];\n if (typeof chunk !== 'object' || chunk === null || done.has(chunk)) continue;\n done.add(chunk);\n try {\n onChunk(chunk as Chunk, holder.document?.currentScript?.src ?? '');\n } catch {\n // Fail open: the chunk goes on to the site exactly as it came.\n }\n }\n };\n try {\n Object.defineProperty(array, 'push', {\n configurable: true,\n enumerable: false,\n get() {\n const target = current;\n return function push(this: unknown, ...items: unknown[]) {\n look(items);\n return target.apply(this, items);\n };\n },\n set(next: (...items: unknown[]) => number) { current = next; },\n });\n } catch {\n return () => {};\n }\n return () => {\n active = false;\n try {\n if (current === Array.prototype.push) delete (array as { push?: unknown }).push;\n else Object.defineProperty(array, 'push', { configurable: true, enumerable: false, writable: true, value: current });\n } catch {\n // Left as the pass-through wrapper.\n }\n };\n}\n\n/**\n * Wraps the module factories of one chunk so that what they `require` passes through `map`:\n * `map(exports)` returns a replacement for that one call, or `undefined` to hand out the module's\n * own exports. The module itself is never changed (return a shadow, e.g. `Object.create(exports)`).\n * Only call this for a chunk you have identified; other chunks must stay untouched.\n */\nexport function mapChunkRequires(chunk: Chunk, map: (exports: unknown) => unknown): void {\n const modules = chunk[1];\n if (typeof modules !== 'object' || modules === null) return;\n const table = modules as Record<string, unknown>;\n for (const id of Object.keys(table)) {\n const factory = table[id];\n if (typeof factory !== 'function') continue;\n table[id] = function (this: unknown, module: unknown, exports: unknown, require: unknown, ...rest: unknown[]) {\n let through = require;\n if (typeof require === 'function') {\n try {\n through = new Proxy(require, {\n apply(target, self, args) {\n const result = Reflect.apply(target as (...a: unknown[]) => unknown, self, args);\n try {\n const replaced = map(result);\n return replaced === undefined ? result : replaced;\n } catch {\n return result;\n }\n },\n });\n } catch {\n through = require;\n }\n }\n return (factory as (...a: unknown[]) => unknown).call(this, module, exports, through, ...rest);\n };\n }\n}\n"]}
|
|
@@ -31,6 +31,7 @@ export type TranslateWords = {
|
|
|
31
31
|
key: string;
|
|
32
32
|
original: string;
|
|
33
33
|
parameters: string;
|
|
34
|
+
plural: string;
|
|
34
35
|
save: string;
|
|
35
36
|
cancel: string;
|
|
36
37
|
remove: string;
|
|
@@ -44,7 +45,8 @@ export type TranslateUiOptions = {
|
|
|
44
45
|
marker: Marker;
|
|
45
46
|
site: string;
|
|
46
47
|
catalog: string;
|
|
47
|
-
words
|
|
48
|
+
/** The bar's and the editor's words; a function is asked again each time they are drawn (the app's language may change). */
|
|
49
|
+
words?: Partial<TranslateWords> | (() => Partial<TranslateWords>);
|
|
48
50
|
/** Leaves translate mode (the app drops the flag and reloads). */
|
|
49
51
|
exit(): void;
|
|
50
52
|
};
|
|
@@ -85,7 +85,7 @@ export function checkEdit(lookup, id, value) {
|
|
|
85
85
|
export const TRANSLATE_WORDS = {
|
|
86
86
|
bar: 'Translate mode', hint: 'Click a word to change it.', edits: count => `${count} ${count === 1 ? 'edit' : 'edits'}`,
|
|
87
87
|
exportPack: 'Copy pack file', copied: 'Pack file copied', exit: 'Leave', reload: 'Reload to see it',
|
|
88
|
-
key: 'Key', original: 'Now', parameters: 'Parameters', save: 'Save', cancel: 'Cancel', remove: 'Remove edit',
|
|
88
|
+
key: 'Key', original: 'Now', parameters: 'Parameters', plural: 'plural', save: 'Save', cancel: 'Cancel', remove: 'Remove edit',
|
|
89
89
|
notTranslatable: 'This string cannot be changed by a pack yet.', choose: 'Which one?',
|
|
90
90
|
};
|
|
91
91
|
const CSS = `
|
|
@@ -108,7 +108,7 @@ button.primary { background: #2563eb; }
|
|
|
108
108
|
*/
|
|
109
109
|
export function installTranslateUi(win, options) {
|
|
110
110
|
const doc = win.document;
|
|
111
|
-
const
|
|
111
|
+
const currentWords = () => ({ ...TRANSLATE_WORDS, ...(typeof options.words === 'function' ? options.words() : options.words) });
|
|
112
112
|
const lookup = inventoryLookup(options.inventory);
|
|
113
113
|
const storage = win.sessionStorage;
|
|
114
114
|
const draft = () => readDraft(storage) ?? { language: '', name: '', strings: {} };
|
|
@@ -135,6 +135,7 @@ export function installTranslateUi(win, options) {
|
|
|
135
135
|
return el;
|
|
136
136
|
};
|
|
137
137
|
const renderBar = () => {
|
|
138
|
+
const words = currentWords();
|
|
138
139
|
bar.replaceChildren();
|
|
139
140
|
const title = doc.createElement('b');
|
|
140
141
|
title.textContent = words.bar;
|
|
@@ -151,6 +152,7 @@ export function installTranslateUi(win, options) {
|
|
|
151
152
|
};
|
|
152
153
|
const closePop = () => { pop?.remove(); pop = null; };
|
|
153
154
|
const edit = (id, x, y, now) => {
|
|
155
|
+
const words = currentWords();
|
|
154
156
|
closePop();
|
|
155
157
|
pop = doc.createElement('div');
|
|
156
158
|
pop.className = 'pop';
|
|
@@ -174,7 +176,7 @@ export function installTranslateUi(win, options) {
|
|
|
174
176
|
}
|
|
175
177
|
if (shape.params?.length) {
|
|
176
178
|
const params = doc.createElement('small');
|
|
177
|
-
params.textContent = `${words.parameters}: ${shape.params.map(name => `{${name}}${shape.plural?.includes(name) ?
|
|
179
|
+
params.textContent = `${words.parameters}: ${shape.params.map(name => `{${name}}${shape.plural?.includes(name) ? ` (${words.plural})` : ''}`).join(', ')}`;
|
|
178
180
|
pop.append(params);
|
|
179
181
|
}
|
|
180
182
|
const input = doc.createElement('textarea');
|
|
@@ -215,6 +217,7 @@ export function installTranslateUi(win, options) {
|
|
|
215
217
|
return [];
|
|
216
218
|
};
|
|
217
219
|
const onClick = (event) => {
|
|
220
|
+
const words = currentWords();
|
|
218
221
|
if (event.composedPath().includes(host))
|
|
219
222
|
return;
|
|
220
223
|
const found = keysAt(event.target);
|