@celestia-island/hikari 0.55.248 → 0.55.249

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.248",
3
+ "version": "0.55.249",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -41,6 +41,10 @@
41
41
  "./plugins/boot-chrome": {
42
42
  "import": "./src/plugins/boot-chrome.mjs",
43
43
  "types": "./src/plugins/boot-chrome.d.mts"
44
+ },
45
+ "./plugins/boot-recovery": {
46
+ "import": "./src/plugins/boot-recovery.mjs",
47
+ "types": "./src/plugins/boot-recovery.d.mts"
44
48
  }
45
49
  },
46
50
  "files": [
@@ -0,0 +1,132 @@
1
+ // AI-generated: developed with the DeepSeek Harness agent (model
2
+ // DeepSeek-Flash) and the ZCode agent (model GLM-5.3), directed and reviewed
3
+ // by langyo — SySL-1.0 §2.3 disclosure.
4
+
5
+ import type { Plugin } from "vite";
6
+
7
+ /**
8
+ * Types for `boot-recovery.mjs` — the self-contained, published Vite plugin
9
+ * entry (`@celestia-island/hikari/plugins/boot-recovery`).
10
+ *
11
+ * The implementation is plain ESM loaded from `node_modules` while a consumer's
12
+ * `vite.config.*` is resolved, where TypeScript cannot be stripped and Vite's
13
+ * `configLoader: "runner"` tears the module runner down before plugin hooks
14
+ * run. Keep the runtime file limited to static imports (`node:path` only).
15
+ */
16
+
17
+ /** The canonical Tauri app origin on Windows/Android. */
18
+ export declare const BOOT_RECOVERY_CANONICAL_ORIGIN: "http://tauri.localhost";
19
+
20
+ /** Default watchdog delay in ms. */
21
+ export declare const BOOT_RECOVERY_DEADLINE_MS: 15000;
22
+
23
+ /** Default selector of the host's static loading element. */
24
+ export declare const BOOT_RECOVERY_LOADING_SELECTOR: "#loading-screen";
25
+
26
+ /** Default class that marks a loading element as already dismissed. */
27
+ export declare const BOOT_RECOVERY_DISMISSED_CLASS: "hidden";
28
+
29
+ /** Default failure-panel message (English; override through `copy.message`). */
30
+ export declare const BOOT_RECOVERY_MESSAGE: string;
31
+
32
+ /** Default failure-panel retry label (English; override through `copy.retry`). */
33
+ export declare const BOOT_RECOVERY_RETRY: string;
34
+
35
+ /** Marker attribute on the injected inline `<script>` tag. */
36
+ export declare const BOOT_RECOVERY_ATTR: "data-hk-boot-recovery";
37
+
38
+ /** The default options, frozen (see {@link BootRecoveryPluginOptions}). */
39
+ export interface BootRecoveryDefaults {
40
+ readonly canonicalOrigin: string;
41
+ readonly deadlineMs: number;
42
+ readonly loadingScreen: Readonly<{
43
+ selector: string;
44
+ dismissedClass: string;
45
+ }>;
46
+ readonly copy: Readonly<{ message: string; retry: string }>;
47
+ readonly injectTo: "head" | "head-prepend";
48
+ }
49
+
50
+ /** Frozen snapshot of every default. */
51
+ export declare const BOOT_RECOVERY_DEFAULTS: Readonly<BootRecoveryDefaults>;
52
+
53
+ /** The loading-screen half of the recovery (the watchdog). */
54
+ export interface BootRecoveryLoadingScreenOptions {
55
+ /** Element whose contents are replaced when nothing mounted (default `#loading-screen`). */
56
+ selector?: string;
57
+ /** Class marking the element as dismissed/fading (default `hidden`; empty disables the check). */
58
+ dismissedClass?: string;
59
+ }
60
+
61
+ /** Overridable failure-panel copy (English defaults). */
62
+ export interface BootRecoveryCopyOptions {
63
+ /** Main message of the panel. */
64
+ message?: string;
65
+ /** Retry button label. */
66
+ retry?: string;
67
+ }
68
+
69
+ /** Options for {@link bootRecoveryPlugin} / {@link bootRecoveryScript}. */
70
+ export interface BootRecoveryPluginOptions {
71
+ /**
72
+ * Which HTML entries get the inline script. Default: only entries whose
73
+ * basename is `index.html`. Multi-window shells MUST configure this — a
74
+ * transparent overlay window must never be reloaded or healed by boot
75
+ * recovery.
76
+ *
77
+ * A `RegExp` is matched against the entry's file path; a `string[]` is
78
+ * matched against the basename or the path suffix (`"pages/index.html"`).
79
+ */
80
+ htmlFiles?: string[] | RegExp;
81
+ /**
82
+ * The watchdog half: an object customises the loading element; `false`
83
+ * disables it entirely (heal-only mode, for hosts with their own fatal
84
+ * landing page). Default: `{ selector: "#loading-screen", dismissedClass: "hidden" }`.
85
+ */
86
+ loadingScreen?: BootRecoveryLoadingScreenOptions | false;
87
+ /** Watchdog delay in ms (default 15000). */
88
+ deadlineMs?: number;
89
+ /** Absolute http(s) app origin (default `http://tauri.localhost`). */
90
+ canonicalOrigin?: string;
91
+ /**
92
+ * Custom mangled-origin predicate. It runs in the PAGE, so it is embedded
93
+ * by source text: pass a self-contained ES2017-safe function expression or
94
+ * arrow function (no closures, no method shorthand, ASCII-only). Default:
95
+ * `isMangledOrigin` (http/https only, hostname starts with the canonical
96
+ * host and differs from it; `tauri://` untouched).
97
+ */
98
+ isMangledOrigin?: (protocol: string, hostname: string) => boolean;
99
+ /** Failure-panel copy overrides. */
100
+ copy?: BootRecoveryCopyOptions;
101
+ /**
102
+ * Where the tag is injected. Default `"head"` (end of `<head>`, after
103
+ * `<meta charset>` — charset-prescan safe). `"head-prepend"` runs even
104
+ * earlier but is charset-unsafe (the encoding prescan only reads the first
105
+ * 1024 bytes); use it only when the host sends an explicit charset header.
106
+ */
107
+ injectTo?: "head" | "head-prepend";
108
+ }
109
+
110
+ /**
111
+ * The default mangled-origin predicate: http(s) only, a hostname that starts
112
+ * with the canonical origin's hostname but is not equal to it (`tauri://`
113
+ * origins are never redirected).
114
+ */
115
+ export declare function isMangledOrigin(
116
+ protocol: string,
117
+ hostname: string,
118
+ canonicalOrigin?: string,
119
+ ): boolean;
120
+
121
+ /**
122
+ * The generated inline IIFE — deterministic, ASCII-only, classic (non-module)
123
+ * and safe to hash into a host CSP.
124
+ */
125
+ export declare function bootRecoveryScript(
126
+ options?: BootRecoveryPluginOptions,
127
+ ): string;
128
+
129
+ /** Vite plugin: inject the boot recovery script into the configured entries. */
130
+ export declare function bootRecoveryPlugin(
131
+ options?: BootRecoveryPluginOptions,
132
+ ): Plugin;
@@ -0,0 +1,280 @@
1
+ // @vitest-environment node
2
+ // AI-generated: developed with the DeepSeek Harness agent (model
3
+ // DeepSeek-Flash) and the ZCode agent (model GLM-5.3), directed and reviewed
4
+ // by langyo — SySL-1.0 §2.3 disclosure.
5
+
6
+ import { execFileSync } from "node:child_process";
7
+ import {
8
+ cpSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ readFileSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { createRequire } from "node:module";
17
+ import { tmpdir } from "node:os";
18
+ import { dirname, join, resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ import { afterAll, describe, expect, it } from "vitest";
22
+
23
+ /**
24
+ * The npm install-face regression test for `boot-recovery.mjs`.
25
+ *
26
+ * `bootRecoveryPlugin` runs inside a consumer's `vite.config.*`, loaded from
27
+ * `node_modules`. Two failure modes have bitten hikari before (pinned by
28
+ * `boot-chrome.install.test.ts`, reproduced here for this entry):
29
+ *
30
+ * 1. shipping the plugin as TypeScript → Node refuses type stripping under
31
+ * `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`);
32
+ * 2. a dynamic `import(...)` from a plugin hook → Vite's
33
+ * `configLoader: "runner"` has already torn the module runner down
34
+ * ("Vite module runner has been closed").
35
+ *
36
+ * Vitest transforms TS and keeps a module runner alive, so it can never catch
37
+ * either one. This test copies the published layout into a fake
38
+ * `node_modules` — `src/plugins` ONLY: boot recovery needs no `src/res`
39
+ * asset, which is part of its install face — and imports it from a CHILD
40
+ * `node` process, then builds a consumer through Vite with BOTH config loaders.
41
+ */
42
+
43
+ const HERE = dirname(fileURLToPath(import.meta.url));
44
+ const VUE_DIR = resolve(HERE, "../..");
45
+ const PACKAGE_JSON = JSON.parse(
46
+ readFileSync(join(VUE_DIR, "package.json"), "utf8"),
47
+ ) as {
48
+ name: string;
49
+ version: string;
50
+ type: string;
51
+ exports: Record<string, { import: string; types?: string }>;
52
+ };
53
+
54
+ const INSTALL_SUBPATH = "./plugins/boot-recovery";
55
+
56
+ const temps: string[] = [];
57
+
58
+ afterAll(() => {
59
+ for (const dir of temps.splice(0)) {
60
+ try {
61
+ rmSync(dir, { recursive: true, force: true });
62
+ } catch {
63
+ /* a leftover temp dir must not fail the suite */
64
+ }
65
+ }
66
+ });
67
+
68
+ /** Lay out `node_modules/@celestia-island/hikari` exactly like `npm pack` does. */
69
+ function fakeInstall(): string {
70
+ const root = mkdtempSync(join(tmpdir(), "hikari-boot-recovery-install-"));
71
+ temps.push(root);
72
+
73
+ const pkgDir = join(root, "node_modules", PACKAGE_JSON.name);
74
+ mkdirSync(join(pkgDir, "src"), { recursive: true });
75
+ // `files: ["src/"]` — the published tarball carries the sources it needs.
76
+ // NOTE: no `src/res` copy on purpose — this plugin has no runtime asset.
77
+ cpSync(join(VUE_DIR, "src/plugins"), join(pkgDir, "src/plugins"), {
78
+ recursive: true,
79
+ });
80
+ writeFileSync(
81
+ join(pkgDir, "package.json"),
82
+ JSON.stringify(
83
+ {
84
+ name: PACKAGE_JSON.name,
85
+ version: PACKAGE_JSON.version,
86
+ private: false,
87
+ type: PACKAGE_JSON.type,
88
+ exports: PACKAGE_JSON.exports,
89
+ },
90
+ null,
91
+ 2,
92
+ ),
93
+ );
94
+ writeFileSync(
95
+ join(root, "package.json"),
96
+ JSON.stringify(
97
+ { name: "boot-recovery-install-probe", private: true, type: "module" },
98
+ null,
99
+ 2,
100
+ ),
101
+ );
102
+ return root;
103
+ }
104
+
105
+ /** Run any command in a child process, capturing everything. */
106
+ function runCommand(
107
+ command: string,
108
+ args: string[],
109
+ cwd: string,
110
+ ): { code: number; stdout: string; stderr: string } {
111
+ try {
112
+ const stdout = execFileSync(command, args, {
113
+ cwd,
114
+ encoding: "utf8",
115
+ stdio: "pipe",
116
+ timeout: 120_000,
117
+ env: { ...process.env, NO_COLOR: "1" },
118
+ });
119
+ return { code: 0, stdout, stderr: "" };
120
+ } catch (err) {
121
+ const failure = err as { status?: number; stdout?: string; stderr?: string };
122
+ return {
123
+ code: failure.status ?? 1,
124
+ stdout: failure.stdout ?? "",
125
+ stderr: failure.stderr ?? String(err),
126
+ };
127
+ }
128
+ }
129
+
130
+ /** Run one ESM probe in a child `node` (no bundler, no transforms). */
131
+ function runProbe(
132
+ root: string,
133
+ source: string,
134
+ ): { code: number; stdout: string; stderr: string } {
135
+ try {
136
+ const stdout = execFileSync(
137
+ process.execPath,
138
+ ["--input-type=module", "-e", source],
139
+ { cwd: root, encoding: "utf8", stdio: "pipe", timeout: 30_000 },
140
+ );
141
+ return { code: 0, stdout, stderr: "" };
142
+ } catch (err) {
143
+ const failure = err as { status?: number; stdout?: string; stderr?: string };
144
+ return {
145
+ code: failure.status ?? 1,
146
+ stdout: failure.stdout ?? "",
147
+ stderr: failure.stderr ?? String(err),
148
+ };
149
+ }
150
+ }
151
+
152
+ describe("published install face (boot recovery)", () => {
153
+ it("declares a resolvable ESM subpath with types", () => {
154
+ const entry = PACKAGE_JSON.exports[INSTALL_SUBPATH];
155
+ expect(entry, INSTALL_SUBPATH + " must be exported").toBeTruthy();
156
+ expect(entry.import).toBe("./src/plugins/boot-recovery.mjs");
157
+ expect(entry.types).toBe("./src/plugins/boot-recovery.d.mts");
158
+ expect(existsSync(join(VUE_DIR, entry.import))).toBe(true);
159
+ expect(existsSync(join(VUE_DIR, entry.types!))).toBe(true);
160
+ });
161
+
162
+ it("imports and runs from node_modules in a plain child node process", { timeout: 60_000 }, () => {
163
+ const root = fakeInstall();
164
+ const probe = [
165
+ "// Plain Node: no type stripping of node_modules, no Vite module runner.",
166
+ 'const mod = await import("' + PACKAGE_JSON.name + '/plugins/boot-recovery");',
167
+ "const script = mod.bootRecoveryScript();",
168
+ 'if (!script.includes("http://tauri.localhost")) {',
169
+ ' console.error("NO_SCRIPT");',
170
+ " process.exit(3);",
171
+ "}",
172
+ 'if (!script.startsWith("(function () {")) {',
173
+ ' console.error("NOT_AN_IIFE");',
174
+ " process.exit(4);",
175
+ "}",
176
+ "const plugin = mod.bootRecoveryPlugin();",
177
+ 'if (!plugin || plugin.name !== "hikari-boot-recovery") {',
178
+ ' console.error("NO_FACTORY");',
179
+ " process.exit(5);",
180
+ "}",
181
+ 'const tags = plugin.transformIndexHtml("", { filename: "/app/index.html" });',
182
+ "if (",
183
+ " !Array.isArray(tags) ||",
184
+ " tags.length !== 1 ||",
185
+ ' tags[0].tag !== "script" ||',
186
+ ' tags[0].injectTo !== "head" ||',
187
+ ' tags[0].attrs["data-hk-boot-recovery"] !== "" ||',
188
+ " tags[0].children !== script",
189
+ ") {",
190
+ ' console.error("NO_TAG");',
191
+ " process.exit(6);",
192
+ "}",
193
+ "const healOnly = mod.bootRecoveryScript({ loadingScreen: false });",
194
+ 'if (healOnly.includes("setTimeout(")) {',
195
+ ' console.error("HEAL_ONLY_BROKEN");',
196
+ " process.exit(7);",
197
+ "}",
198
+ 'console.log("INSTALL_OK " + script.length);',
199
+ ].join("\n");
200
+ const result = runProbe(root, probe);
201
+
202
+ expect(
203
+ result.stderr,
204
+ "child node stderr (type-strip / runner failures show up here):\n" +
205
+ result.stderr,
206
+ ).not.toMatch(
207
+ /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING|module runner has been closed|Cannot find module/,
208
+ );
209
+ expect(result.code, "child exited " + result.code + ": " + result.stderr).toBe(0);
210
+ expect(result.stdout).toContain("INSTALL_OK");
211
+ });
212
+
213
+ it("builds a consumer through vite with BOTH config loaders", { timeout: 120_000 }, () => {
214
+ // The R2 scenario: a consumer whose vite.config imports the plugin from its
215
+ // own node_modules. The DEFAULT loader fails on a TypeScript entry; the
216
+ // `runner` loader additionally fails on a dynamic import from a hook. Both
217
+ // loader modes are exercised so a future Vite bump gets caught.
218
+ const root = fakeInstall();
219
+ writeFileSync(
220
+ join(root, "index.html"),
221
+ '<!doctype html><html><head><meta charset="UTF-8"><title>App</title></head>' +
222
+ '<body><div id="loading-screen"><span>Loading...</span></div>' +
223
+ '<div id="app"></div><script type="module" src="/main.js"></script></body></html>',
224
+ );
225
+ writeFileSync(join(root, "main.js"), 'document.getElementById("app");');
226
+ writeFileSync(
227
+ join(root, "vite.config.mjs"),
228
+ [
229
+ 'import { bootRecoveryPlugin } from "' +
230
+ PACKAGE_JSON.name +
231
+ '/plugins/boot-recovery";',
232
+ "export default {",
233
+ " plugins: [bootRecoveryPlugin()],",
234
+ ' build: { outDir: "dist", emptyOutDir: true },',
235
+ "};",
236
+ ].join("\n"),
237
+ );
238
+
239
+ // Resolve the repo's own vite binary: the fake install has no toolchain,
240
+ // and the point is what a consumer's build does with the published files.
241
+ const viteEntry = createRequire(import.meta.url).resolve("vite");
242
+ const viteBin = resolve(dirname(viteEntry), "../../bin/vite.js");
243
+ for (const loader of ["default", "runner"]) {
244
+ const args = [viteBin, "build"];
245
+ if (loader === "runner") args.push("--configLoader", "runner");
246
+ const result = runCommand(process.execPath, args, root);
247
+ expect(
248
+ result.code,
249
+ "vite build (" + loader + " config loader) failed:\n" +
250
+ (result.stderr || result.stdout),
251
+ ).toBe(0);
252
+ const html = readFileSync(join(root, "dist", "index.html"), "utf8");
253
+ expect(html).toContain("data-hk-boot-recovery");
254
+ expect(html).toContain("(function () {");
255
+ // Inline, not an external asset the install would have had to ship.
256
+ expect(html).not.toMatch(/<script[^>]*data-hk-boot-recovery[^>]*\ssrc=/);
257
+ }
258
+ });
259
+
260
+ it("keeps the runtime module free of dynamic imports and TypeScript", () => {
261
+ // Enforce the properties the failure modes above come from: a refactor back
262
+ // to a dynamic import or a .ts implementation is caught without a full
263
+ // build. Comments mention the banned forms (they explain why), so only
264
+ // real code is checked.
265
+ const source = readFileSync(
266
+ join(VUE_DIR, "src/plugins/boot-recovery.mjs"),
267
+ "utf8",
268
+ );
269
+ const code = source
270
+ .replace(/\/\*[\s\S]*?\*\//g, "")
271
+ .replace(/^\s*\/\/.*$/gm, "");
272
+ expect(code).not.toMatch(/\bimport\s*\(/);
273
+ expect(code).not.toMatch(/\bfrom\s+["'][^"']*\.ts["']/);
274
+ // Static imports only — and only the one Node builtin it needs.
275
+ expect(code).toMatch(/from "node:path"/);
276
+ // No runtime asset: the plugin is pure string generation.
277
+ expect(code).not.toMatch(/res\//);
278
+ expect(code).not.toContain("readFileSync");
279
+ });
280
+ });