myphp-runtime 1.28.0 → 1.30.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 +4 -4
- package/package.json +7 -2
- package/src/i18n.test.ts +0 -148
- package/src/index.test.ts +0 -161
package/README.md
CHANGED
|
@@ -14,10 +14,10 @@ const greeting = await Greeter.greet("Ada");
|
|
|
14
14
|
events.tick.on((time) => console.log(time));
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
`"myphp-runtime": "file:<
|
|
19
|
-
-template web` writes it. Its modules are TypeScript, which
|
|
20
|
-
take as they are.
|
|
17
|
+
Install it from npm, `npm install myphp-runtime`, or depend on it from the
|
|
18
|
+
MyPHP the app runs on, `"myphp-runtime": "file:<MyPHP>/packages/runtime"`,
|
|
19
|
+
as `myphp init -template web` writes it. Its modules are TypeScript, which
|
|
20
|
+
Vite and Bun take as they are.
|
|
21
21
|
|
|
22
22
|
Use the package directly for the window and the platform:
|
|
23
23
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myphp-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Typed access to the MyPHP runtime in the pages of MyPHP desktop apps",
|
|
5
5
|
"keywords": ["myphp", "desktop", "webview", "php"],
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/owner888/myphp.git",
|
|
9
|
+
"directory": "packages/runtime"
|
|
10
|
+
},
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"type": "module",
|
|
8
13
|
"sideEffects": false,
|
|
@@ -11,7 +16,7 @@
|
|
|
11
16
|
"./i18n": "./src/i18n.ts"
|
|
12
17
|
},
|
|
13
18
|
"types": "./src/index.ts",
|
|
14
|
-
"files": ["src", "README.md"],
|
|
19
|
+
"files": ["src", "!src/**/*.test.ts", "README.md"],
|
|
15
20
|
"scripts": {
|
|
16
21
|
"test": "bun test"
|
|
17
22
|
}
|
package/src/i18n.test.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// The formatter of messages in pages (i18n.ts, mygo-runtime's i18n.test.ts):
|
|
2
|
-
// the fixtures MyPHP\Message checks too (tests/i18n/messages.json), so that
|
|
3
|
-
// PHP and pages say the same, catalogs, and the module `myphp generate`
|
|
4
|
-
// writes (tests/i18n/app/locales.ts, which tests/i18ngen_test.php checks
|
|
5
|
-
// against the generator). bun test, here.
|
|
6
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
createCatalog,
|
|
11
|
-
currentLocale,
|
|
12
|
-
formatMessage,
|
|
13
|
-
isPseudoLocale,
|
|
14
|
-
MessageSyntaxError,
|
|
15
|
-
parseMessage,
|
|
16
|
-
skeletonOptions,
|
|
17
|
-
type CatalogData,
|
|
18
|
-
} from "./i18n";
|
|
19
|
-
|
|
20
|
-
const fixtures = JSON.parse(readFileSync(join(import.meta.dir, "../../../tests/i18n/messages.json"), "utf8")) as {
|
|
21
|
-
format: { message: string; locale?: string; args?: Record<string, unknown>; want: string }[];
|
|
22
|
-
errors: string[];
|
|
23
|
-
pseudo: { message: string; locale: string; args?: Record<string, unknown>; want: string }[];
|
|
24
|
-
};
|
|
25
|
-
const g = globalThis as { myphp?: unknown };
|
|
26
|
-
|
|
27
|
-
describe("formatMessage", () => {
|
|
28
|
-
test("has the fixtures", () => {
|
|
29
|
-
expect(fixtures.format.length).toBeGreaterThan(50);
|
|
30
|
-
expect(fixtures.errors.length).toBeGreaterThan(20);
|
|
31
|
-
});
|
|
32
|
-
for (const c of [...fixtures.format, ...fixtures.pseudo]) {
|
|
33
|
-
test(`${c.message} in ${c.locale ?? "en"} with ${JSON.stringify(c.args ?? {})}`, () => {
|
|
34
|
-
expect(formatMessage(c.locale ?? "en", c.message, c.args)).toBe(c.want);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
for (const s of fixtures.errors) {
|
|
38
|
-
test(`${JSON.stringify(s)} does not parse`, () => {
|
|
39
|
-
expect(() => parseMessage(s)).toThrow(MessageSyntaxError);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
test("formats numbers and dates with Intl", () => {
|
|
43
|
-
expect(formatMessage("de", "{n, plural, one {# Datei} other {# Dateien}}", { n: 1000 })).toBe("1.000 Dateien");
|
|
44
|
-
expect(formatMessage("en", "{n, number, ::currency/EUR}", { n: 3 })).toBe("€3.00");
|
|
45
|
-
const day = new Date(Date.UTC(2026, 0, 2, 15, 4));
|
|
46
|
-
expect(formatMessage("en", "{d, date, short}", { d: day })).toBe(new Intl.DateTimeFormat("en", { dateStyle: "short" }).format(day));
|
|
47
|
-
expect(formatMessage("en", "{d}", { d: day })).toBe(new Intl.DateTimeFormat("en", { dateStyle: "short", timeStyle: "short" }).format(day));
|
|
48
|
-
expect(formatMessage("en", "{d, date}", { d: "tomorrow" })).toBe("tomorrow");
|
|
49
|
-
});
|
|
50
|
-
test("writes other values", () => {
|
|
51
|
-
expect(formatMessage("en", "{e}", { e: new Error("boom") })).toBe("boom");
|
|
52
|
-
expect(formatMessage("en", "{v}", { v: null })).toBe("");
|
|
53
|
-
expect(formatMessage("en", "{n, plural, one {#} other {# x}}", { n: 2n })).toBe("2 x");
|
|
54
|
-
});
|
|
55
|
-
test("reports where a message does not parse", () => {
|
|
56
|
-
expect(() => parseMessage("Hello {name")).toThrow(MessageSyntaxError);
|
|
57
|
-
try {
|
|
58
|
-
parseMessage("Hello {name");
|
|
59
|
-
} catch (err) {
|
|
60
|
-
expect((err as MessageSyntaxError).offset).toBe(11);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const data: CatalogData = {
|
|
66
|
-
source: "en",
|
|
67
|
-
languages: ["en", "de", "en-001", "pt", "pt-PT", "zh-Hans", "zh-Hant"],
|
|
68
|
-
fallbacks: { en: ["en"], de: ["de", "en"], "en-001": ["en-001", "en"], pt: ["pt", "en"], "pt-PT": ["pt-PT", "pt", "en"], "zh-Hans": ["zh-Hans", "en"], "zh-Hant": ["zh-Hant", "en"] },
|
|
69
|
-
params: { hello: ["name"], color: [], files: ["n"], broken: [] },
|
|
70
|
-
messages: {
|
|
71
|
-
en: { hello: "Hello, {name}!", color: "Color", files: "{n, plural, one {# file} other {# files}}", broken: "Fine in English" },
|
|
72
|
-
"en-001": { color: "Colour" },
|
|
73
|
-
de: { hello: "Hallo, {name}!", files: "{n, plural, one {# Datei} other {# Dateien}}", broken: "{kaputt" },
|
|
74
|
-
"zh-Hans": { hello: "你好,{name}!" },
|
|
75
|
-
"zh-Hant": { hello: "你好,{name}!", color: "顏色" },
|
|
76
|
-
pt: { color: "Cor" },
|
|
77
|
-
"pt-PT": { hello: "Olá, {name}!" },
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
interface Messages {
|
|
82
|
-
hello(name: string): string;
|
|
83
|
-
color(): string;
|
|
84
|
-
files(n: number): string;
|
|
85
|
-
broken(): string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
describe("createCatalog", () => {
|
|
89
|
-
const catalog = createCatalog<Messages>(data);
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
delete g.myphp;
|
|
92
|
-
});
|
|
93
|
-
test("falls back along the languages", () => {
|
|
94
|
-
const cases: [string, string, Record<string, unknown>, string][] = [
|
|
95
|
-
["en", "hello", { name: "Ada" }, "Hello, Ada!"], ["de", "hello", { name: "Ada" }, "Hallo, Ada!"], ["de-CH", "hello", { name: "Ada" }, "Hallo, Ada!"],
|
|
96
|
-
["de", "color", {}, "Color"], ["de", "broken", {}, "broken"], ["en-001", "color", {}, "Colour"], ["zh-TW", "color", {}, "顏色"], ["zh-Hant-HK", "color", {}, "顏色"],
|
|
97
|
-
["zh-CN", "color", {}, "Color"], ["pt-PT", "color", {}, "Cor"], ["fr", "hello", { name: "Ada" }, "Hello, Ada!"], ["de", "missing", {}, "missing"],
|
|
98
|
-
["de", "toString", {}, "toString"], ["de", "constructor", {}, "constructor"], ["de", "files", { n: 1 }, "1 Datei"], ["fr", "files", { n: 0 }, "0 files"],
|
|
99
|
-
["en-XA", "color", {}, "[Çöļöŕ one]"], ["ar-XB", "color", {}, "Color"], ["", "color", {}, "Color"],
|
|
100
|
-
];
|
|
101
|
-
for (const [locale, key, args, want] of cases) expect(catalog.format(locale, key, args), `${locale} ${key}`).toBe(want);
|
|
102
|
-
expect([catalog.resolve("zh-TW"), catalog.resolve("en-AU"), catalog.resolve("xx")]).toEqual(["zh-Hant", "en", "en"]);
|
|
103
|
-
});
|
|
104
|
-
test("binds the messages to the language the page shows", () => {
|
|
105
|
-
const messages = catalog.messages();
|
|
106
|
-
g.myphp = { locale: "de" };
|
|
107
|
-
expect(currentLocale()).toBe("de");
|
|
108
|
-
expect(messages.files(3)).toBe("3 Dateien");
|
|
109
|
-
g.myphp = { locale: "zh-Hant" };
|
|
110
|
-
expect(messages.hello("Ada")).toBe("你好,Ada!");
|
|
111
|
-
expect(catalog.messagesIn("en").files(1)).toBe("1 file");
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("isPseudoLocale", () => {
|
|
116
|
-
expect([isPseudoLocale("en-XA"), isPseudoLocale("ar-xb"), isPseudoLocale("en"), isPseudoLocale("not a tag")]).toEqual([true, true, false, false]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("skeletons as Intl's options", () => {
|
|
120
|
-
expect(skeletonOptions("yMMMd")).toEqual({ year: "numeric", month: "short", day: "numeric" });
|
|
121
|
-
expect(skeletonOptions("Hmm")).toEqual({ hour: "numeric", hourCycle: "h23", minute: "2-digit" });
|
|
122
|
-
expect(skeletonOptions("Kms")).toEqual({ hour: "numeric", hourCycle: "h11", minute: "numeric", second: "numeric" });
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("the module myphp generate writes", () => {
|
|
126
|
-
const file = join(import.meta.dir, "../../../tests/i18n/app/locales.ts");
|
|
127
|
-
test("type-checks", () => {
|
|
128
|
-
// The repository's tsconfig.json, which resolves myphp-runtime/i18n to its source, includes it.
|
|
129
|
-
const tsc = Bun.spawnSync(["tsc", "-p", join(import.meta.dir, "../../../tsconfig.json")]);
|
|
130
|
-
expect(tsc.stdout.toString() + tsc.stderr.toString()).toBe("");
|
|
131
|
-
expect(tsc.exitCode).toBe(0);
|
|
132
|
-
});
|
|
133
|
-
test("says the messages", async () => {
|
|
134
|
-
const locales = await import(file);
|
|
135
|
-
expect(locales.languages).toEqual(["en", "de", "en-GB", "zh-Hant"]);
|
|
136
|
-
const de = locales.messagesIn("de");
|
|
137
|
-
expect([de.title(), de.filesSelected(0), de.filesSelected(1), de.progress(0.5)]).toEqual(["Notizen", "Keine Dateien ausgewählt", "1 Datei ausgewählt", "50 % done"]);
|
|
138
|
-
expect(locales.messagesIn("en-GB").progress(0.5)).toBe("50% complete");
|
|
139
|
-
expect(locales.messagesIn("zh-TW").filesSelected(2)).toBe("已選取 2 個檔案");
|
|
140
|
-
expect([locales.messagesIn("en").guests("Ada", 3), locales.messagesIn("en").place(22), locales.messagesIn("en").positional("a", "b"), locales.messagesIn("en").keyword("x", "y", "z", "w"), locales.messagesIn("en-XA").title()]).toEqual(["Ada and 2 others", "You came 22nd", "a of b", "x y z w", "[Ñöţéš one]"]);
|
|
141
|
-
g.myphp = { locale: "de" };
|
|
142
|
-
try {
|
|
143
|
-
expect(locales.messages.greeting("Ada")).toBe("Hallo, Ada!");
|
|
144
|
-
} finally {
|
|
145
|
-
delete g.myphp;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
package/src/index.test.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { afterEach, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
Channel,
|
|
4
|
-
call,
|
|
5
|
-
currentWindow,
|
|
6
|
-
direction,
|
|
7
|
-
event,
|
|
8
|
-
isCallError,
|
|
9
|
-
isMyPHP,
|
|
10
|
-
locale,
|
|
11
|
-
on,
|
|
12
|
-
onDocumentReload,
|
|
13
|
-
onDocumentState,
|
|
14
|
-
onFileDrop,
|
|
15
|
-
onLocaleChange,
|
|
16
|
-
onPreferences,
|
|
17
|
-
preferences,
|
|
18
|
-
runtime,
|
|
19
|
-
type DocumentState,
|
|
20
|
-
type FileDrop,
|
|
21
|
-
type Runtime,
|
|
22
|
-
type ThemePreferences,
|
|
23
|
-
} from "./index";
|
|
24
|
-
|
|
25
|
-
const calls: [string, unknown[]][] = [];
|
|
26
|
-
const listeners = new Map<string, (payload: unknown) => void>();
|
|
27
|
-
|
|
28
|
-
const fake: Runtime = {
|
|
29
|
-
call: async <T>(method: string, ...args: unknown[]) => {
|
|
30
|
-
calls.push([method, args]);
|
|
31
|
-
if (method === "Svc.fail") {
|
|
32
|
-
const err = new Error("boom") as Error & { method: string };
|
|
33
|
-
err.name = "CallError";
|
|
34
|
-
err.method = method;
|
|
35
|
-
throw err;
|
|
36
|
-
}
|
|
37
|
-
return method as T;
|
|
38
|
-
},
|
|
39
|
-
channel: <T>(onmessage?: (value: T) => void) =>
|
|
40
|
-
({ onmessage, onclose: null, closed: false, close() {}, [Symbol.asyncIterator]: () => ({}) }) as unknown as Channel<T>,
|
|
41
|
-
on: (name, listener) => {
|
|
42
|
-
listeners.set(name, listener as (payload: unknown) => void);
|
|
43
|
-
return () => listeners.delete(name);
|
|
44
|
-
},
|
|
45
|
-
once: (name, listener) => {
|
|
46
|
-
listeners.set(name, listener as (payload: unknown) => void);
|
|
47
|
-
return () => listeners.delete(name);
|
|
48
|
-
},
|
|
49
|
-
platform: "darwin",
|
|
50
|
-
windowId: 1,
|
|
51
|
-
version: "0.1.0",
|
|
52
|
-
preferences: { accent: null, reduceMotion: false, reduceTransparency: true, highContrast: false, textScale: 1 },
|
|
53
|
-
locale: "en",
|
|
54
|
-
dir: "ltr",
|
|
55
|
-
window: {
|
|
56
|
-
minimize: () => fake.call("myphp:window.Minimize"),
|
|
57
|
-
maximize: () => fake.call("myphp:window.Maximize"),
|
|
58
|
-
unmaximize: () => fake.call("myphp:window.Unmaximize"),
|
|
59
|
-
toggleMaximize: () => fake.call("myphp:window.ToggleMaximize"),
|
|
60
|
-
isMaximized: () => fake.call("myphp:window.IsMaximized"),
|
|
61
|
-
toggleFullScreen: () => fake.call("myphp:window.ToggleFullScreen"),
|
|
62
|
-
close: () => fake.call("myphp:window.Close"),
|
|
63
|
-
setTitle: (title) => fake.call("myphp:window.SetTitle", title),
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
afterEach(() => {
|
|
68
|
-
delete (globalThis as { myphp?: Runtime }).myphp;
|
|
69
|
-
calls.length = 0;
|
|
70
|
-
listeners.clear();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("outside MyPHP", async () => {
|
|
74
|
-
expect(isMyPHP()).toBe(false);
|
|
75
|
-
expect(() => runtime()).toThrow("runtime not found");
|
|
76
|
-
await expect(call("Svc.method")).rejects.toThrow("runtime not found");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("delegates to window.myphp", async () => {
|
|
80
|
-
(globalThis as { myphp?: Runtime }).myphp = fake;
|
|
81
|
-
expect(isMyPHP()).toBe(true);
|
|
82
|
-
expect(await call<string>("Svc.method", 1, "a")).toBe("Svc.method");
|
|
83
|
-
expect(calls).toEqual([["Svc.method", [1, "a"]]]);
|
|
84
|
-
|
|
85
|
-
const got: unknown[] = [];
|
|
86
|
-
const off = event<{ n: number }>("tick").on((p) => got.push(p.n));
|
|
87
|
-
listeners.get("tick")?.({ n: 7 });
|
|
88
|
-
off();
|
|
89
|
-
expect(got).toEqual([7]);
|
|
90
|
-
expect(listeners.has("tick")).toBe(false);
|
|
91
|
-
|
|
92
|
-
on("other", () => {});
|
|
93
|
-
expect(listeners.has("other")).toBe(true);
|
|
94
|
-
|
|
95
|
-
await currentWindow.setTitle("Hi");
|
|
96
|
-
expect(calls.at(-1)).toEqual(["myphp:window.SetTitle", ["Hi"]]);
|
|
97
|
-
|
|
98
|
-
const err = await call("Svc.fail").catch((e: unknown) => e);
|
|
99
|
-
expect(isCallError(err)).toBe(true);
|
|
100
|
-
expect(isCallError(new Error("other"))).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("Channel", () => {
|
|
104
|
-
expect(() => new Channel()).toThrow("runtime not found");
|
|
105
|
-
(globalThis as { myphp?: Runtime }).myphp = fake;
|
|
106
|
-
const fn = (n: number) => n;
|
|
107
|
-
const ch = new Channel<number>(fn);
|
|
108
|
-
expect(ch.onmessage).toBe(fn);
|
|
109
|
-
expect(ch.closed).toBe(false);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("onFileDrop", () => {
|
|
113
|
-
(globalThis as { myphp?: Runtime }).myphp = fake;
|
|
114
|
-
const got: FileDrop[] = [];
|
|
115
|
-
const off = onFileDrop((d) => got.push(d));
|
|
116
|
-
listeners.get("myphp:file-drop")?.({ paths: ["/tmp/a.txt"], x: 10, y: 20 });
|
|
117
|
-
expect(got).toEqual([{ paths: ["/tmp/a.txt"], x: 10, y: 20 }]);
|
|
118
|
-
off();
|
|
119
|
-
expect(listeners.has("myphp:file-drop")).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("onDocumentState and onDocumentReload", () => {
|
|
123
|
-
(globalThis as { myphp?: Runtime }).myphp = fake;
|
|
124
|
-
const states: DocumentState[] = [];
|
|
125
|
-
let reloads = 0;
|
|
126
|
-
const off = onDocumentState((s) => states.push(s));
|
|
127
|
-
const offReload = onDocumentReload(() => reloads++);
|
|
128
|
-
const state = { name: "a.md", path: "/tmp/a.md", edited: true, readOnly: false, conflict: false, deleted: false };
|
|
129
|
-
listeners.get("myphp:document-state")?.(state);
|
|
130
|
-
listeners.get("myphp:document-reload")?.({});
|
|
131
|
-
expect(states).toEqual([state]);
|
|
132
|
-
expect(reloads).toBe(1);
|
|
133
|
-
off();
|
|
134
|
-
offReload();
|
|
135
|
-
expect(listeners.has("myphp:document-state")).toBe(false);
|
|
136
|
-
expect(listeners.has("myphp:document-reload")).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("preferences and onPreferences", () => {
|
|
140
|
-
(globalThis as { myphp?: Runtime }).myphp = fake;
|
|
141
|
-
expect(preferences()).toEqual(fake.preferences);
|
|
142
|
-
const got: ThemePreferences[] = [];
|
|
143
|
-
const off = onPreferences((p) => got.push(p));
|
|
144
|
-
const changed = { accent: "#ff8000", reduceMotion: true, reduceTransparency: false, highContrast: false, textScale: 1.25 };
|
|
145
|
-
listeners.get("myphp:preferences")?.(changed);
|
|
146
|
-
expect(got).toEqual([changed]);
|
|
147
|
-
off();
|
|
148
|
-
expect(listeners.has("myphp:preferences")).toBe(false);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("locale, direction and onLocaleChange", () => {
|
|
152
|
-
(globalThis as { myphp?: Runtime }).myphp = fake;
|
|
153
|
-
expect(locale()).toBe("en");
|
|
154
|
-
expect(direction()).toBe("ltr");
|
|
155
|
-
const got: [string, string][] = [];
|
|
156
|
-
const off = onLocaleChange((tag, dir) => got.push([tag, dir]));
|
|
157
|
-
listeners.get("myphp:locale")?.({ locale: "ar", dir: "rtl" });
|
|
158
|
-
expect(got).toEqual([["ar", "rtl"]]);
|
|
159
|
-
off();
|
|
160
|
-
expect(listeners.has("myphp:locale")).toBe(false);
|
|
161
|
-
});
|