arcadible 1.0.9 → 1.0.10

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
@@ -65,5 +65,5 @@
65
65
  "scripts": {
66
66
  "test": "node --test \"test/**/*.test.js\""
67
67
  },
68
- "version": "1.0.9"
68
+ "version": "1.0.10"
69
69
  }
@@ -22,6 +22,7 @@ const commands = require("../../util/command");
22
22
  const error = require("../../util/error");
23
23
  const files = require("../../serve/files");
24
24
  const given = require("../../parser/given");
25
+ const media = require("../../parser/media");
25
26
  const os = require("../../util/os");
26
27
  const phrase = require("../../phrase");
27
28
  const reload = require("../../serve/reload");
@@ -36,6 +37,7 @@ const configDir = os.configDir;
36
37
  const dirFiles = files.dirFiles;
37
38
  const join = path.join;
38
39
  const pathExists = fs.pathExists;
40
+ const readFile = fs.promises.readFile;
39
41
  const relative = path.relative;
40
42
  const remoteFiles = remote.remoteFiles;
41
43
  const resolvePath = path.resolve;
@@ -92,6 +94,17 @@ const probe = async function (url) {
92
94
 
93
95
  // [function]
94
96
 
97
+ // the project's store icon, .arcadible/icon.png, or null: found by its exact
98
+ // name, as `arc deploy` finds it, each time it's asked for.
99
+ const storeIcon = function (root) {
100
+ return async () => {
101
+ const { game } = await media(root);
102
+ return game.icon ? readFile(join(root, game.icon)) : null;
103
+ };
104
+ };
105
+
106
+ // [function]
107
+
95
108
  // listens, turning a port in use into its code.
96
109
  const listen = async function (options) {
97
110
  try {
@@ -125,6 +138,7 @@ const main = async function (conf) {
125
138
  const project = await given(conf);
126
139
  const game = project.game;
127
140
  const options = {
141
+ icon: storeIcon(root),
128
142
  info: {
129
143
  channel: CHANNEL,
130
144
  id: game.id ?? UNNAMED,
Binary file
package/src/serve/page.js CHANGED
@@ -3,7 +3,13 @@
3
3
  // (site/src/javascripts/arcadible/play/app/page.coffee), with the same
4
4
  // sandbox and permissions, and runs the play page's own host for the SDK,
5
5
  // so saves are kept by the page, across builds. It reloads the
6
- // game, and only the game, when told to.
6
+ // game, and only the game, when told to. Its tab shows the game's store
7
+ // icon, as the play page's does, or Arcadible's.
8
+
9
+ // [import]
10
+
11
+ const fs = require("node:fs");
12
+ const path = require("node:path");
7
13
 
8
14
  // [import]
9
15
 
@@ -12,6 +18,8 @@ const serveGame = require("./game");
12
18
  // [use]
13
19
 
14
20
  const endWith = serveGame.endWith;
21
+ const join = path.join;
22
+ const readFile = fs.promises.readFile;
15
23
 
16
24
  // [declare]
17
25
 
@@ -23,6 +31,10 @@ const SANDBOX =
23
31
  // the page's own paths. Its origin isn't the game's, so they can't collide.
24
32
  const HOST = "/_arcadible/host.js";
25
33
  const RELOAD = "/_arcadible/reload";
34
+ const ICON = "/_arcadible/icon";
35
+ // the play page's own favicon.ico (app/src/web/arcadible-play/content), for a
36
+ // game without a store icon, and for /favicon.ico, which browsers ask for.
37
+ const FAVICON = join(__dirname, "favicon.ico");
26
38
 
27
39
  // [declare]
28
40
 
@@ -44,6 +56,24 @@ const scriptJson = function (value) {
44
56
 
45
57
  // [function]
46
58
 
59
+ // the play page's favicon.ico, read once.
60
+ let favicon = null;
61
+ const readFavicon = function () {
62
+ favicon ??= readFile(FAVICON);
63
+ return favicon;
64
+ };
65
+
66
+ // [function]
67
+
68
+ // `png`, the game's store icon, with the corners the play page's icons have
69
+ // (h.border_radius in app/lib/python/offst/util.py: 22% of the size).
70
+ const roundedIcon = function (png) {
71
+ const href = `data:image/png;base64,${png.toString("base64")}`;
72
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><clipPath id="c"><rect width="100" height="100" rx="22"/></clipPath><image href="${href}" width="100" height="100" preserveAspectRatio="xMidYMid slice" clip-path="url(#c)"/></svg>`;
73
+ };
74
+
75
+ // [function]
76
+
47
77
  const escapeHtml = function (text) {
48
78
  return text.replace(
49
79
  /[&<>"]/gu,
@@ -70,6 +100,7 @@ const html = function (game) {
70
100
  <meta charset="utf-8">
71
101
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
72
102
  <title>${escapeHtml(game.title)}</title>
103
+ <link href="${ICON}" rel="icon">
73
104
  <style>html,body{margin:0;height:100%;overflow:hidden;background:#000}iframe{display:block;width:100%;height:100%;border:0;visibility:hidden}</style>
74
105
  </head>
75
106
  <body>
@@ -117,9 +148,11 @@ const html = function (game) {
117
148
  // [function]
118
149
 
119
150
  // the request handler for the page's origin. `reloads` keeps the pages
120
- // listening for a reload; `remote` has the host.
151
+ // listening for a reload; `remote` has the host; `icon()` resolves with the
152
+ // game's store icon, or null.
121
153
  const pageHandler = function ({
122
154
  game,
155
+ icon = () => Promise.resolve(null),
123
156
  onRemote = () => null,
124
157
  reloads,
125
158
  remote,
@@ -138,6 +171,26 @@ const pageHandler = function ({
138
171
  endWith(req, res, html(game));
139
172
  return Promise.resolve();
140
173
  }
174
+ if (pathname === ICON) {
175
+ // read for each request, so an icon added while serving shows on the
176
+ // next load.
177
+ return icon()
178
+ .catch(() => null)
179
+ .then(async (png) => {
180
+ const body = png ? roundedIcon(png) : await readFavicon();
181
+ res.writeHead(200, {
182
+ ...HEADERS,
183
+ "content-type": png ? "image/svg+xml" : "image/x-icon",
184
+ });
185
+ endWith(req, res, body);
186
+ });
187
+ }
188
+ if (pathname === "/favicon.ico") {
189
+ return readFavicon().then((body) => {
190
+ res.writeHead(200, { ...HEADERS, "content-type": "image/x-icon" });
191
+ endWith(req, res, body);
192
+ });
193
+ }
141
194
  if (pathname === RELOAD) {
142
195
  reloads.listen(res);
143
196
  return Promise.resolve();
@@ -173,4 +226,4 @@ const pageHandler = function ({
173
226
 
174
227
  // [export]
175
228
 
176
- module.exports = { ALLOW, SANDBOX, html, pageHandler };
229
+ module.exports = { ALLOW, FAVICON, SANDBOX, html, pageHandler };
@@ -45,11 +45,20 @@ const listen = function (handler, port, address) {
45
45
  // [function]
46
46
 
47
47
  // starts serving `files` for `info`, `{ id, channel, version, title }`, on
48
- // `port`, with Arcadible's own files from `remote`. With `src`, another
48
+ // `port`, with Arcadible's own files from `remote`, and the tab's icon from
49
+ // `icon()`, which resolves with a PNG or null. With `src`, another
49
50
  // server's page, it frames that instead, and serves no files. Resolves with
50
51
  // `{ page, game, reload(), close() }`: the page's origin, the game's URL,
51
52
  // and the controls.
52
- const serve = async function ({ files, info, onRemote, port, remote, src }) {
53
+ const serve = async function ({
54
+ files,
55
+ icon,
56
+ info,
57
+ onRemote,
58
+ port,
59
+ remote,
60
+ src,
61
+ }) {
53
62
  const origins = {
54
63
  game: `http://127.0.0.1:${port}`,
55
64
  page: `http://localhost:${port}`,
@@ -61,6 +70,7 @@ const serve = async function ({ files, info, onRemote, port, remote, src }) {
61
70
  : null;
62
71
  const forPage = pageHandler({
63
72
  game: { ...info, src: framed },
73
+ icon,
64
74
  onRemote,
65
75
  reloads,
66
76
  remote,