arcadible 1.0.9 → 1.0.11
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/bin/arc-deploy.js +7 -22
- package/package.json +1 -1
- package/src/action/deploy/wait.js +24 -36
- package/src/action/predeploy/tool-git.js +22 -10
- package/src/action/predeploy/tool.js +2 -0
- package/src/action/serve/tool.js +14 -0
- package/src/api.js +0 -10
- package/src/config/code.js +3 -2
- package/src/phrase.js +16 -10
- package/src/serve/favicon.ico +0 -0
- package/src/serve/page.js +56 -3
- package/src/serve/server.js +12 -2
- package/src/util/video.js +11 -1
package/bin/arc-deploy.js
CHANGED
|
@@ -136,14 +136,6 @@ const report = function (
|
|
|
136
136
|
|
|
137
137
|
// [function]
|
|
138
138
|
|
|
139
|
-
// the account, or null when there's none or it can't be refreshed: for a git
|
|
140
|
-
// deploy, which needs none.
|
|
141
|
-
const whoamiIfAny = function (opts) {
|
|
142
|
-
return whoami(opts).catch(() => null);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// [function]
|
|
146
|
-
|
|
147
139
|
// the release, as `--json` prints it.
|
|
148
140
|
const releaseJson = function (row) {
|
|
149
141
|
if (!row) {
|
|
@@ -168,7 +160,7 @@ const waitAndReport = async function (
|
|
|
168
160
|
current,
|
|
169
161
|
deployed,
|
|
170
162
|
result,
|
|
171
|
-
{ git, json,
|
|
163
|
+
{ git, json, quiet, root, since, timeout },
|
|
172
164
|
) {
|
|
173
165
|
let sw = new SpinnerWheel(phrase.releaseWaiting(git));
|
|
174
166
|
sw.start(json || quiet);
|
|
@@ -178,7 +170,7 @@ const waitAndReport = async function (
|
|
|
178
170
|
waited = await wait(
|
|
179
171
|
current,
|
|
180
172
|
deployed,
|
|
181
|
-
{ git,
|
|
173
|
+
{ git, root, since, timeout },
|
|
182
174
|
{
|
|
183
175
|
onRelease() {
|
|
184
176
|
// found: GitHub Actions is done, and Arcadible publishes it now.
|
|
@@ -253,9 +245,6 @@ const start = function (argv) {
|
|
|
253
245
|
const timeout = Number(argv.timeout ?? TIMEOUT[git ? "git" : "direct"]);
|
|
254
246
|
const root = resolve(process.cwd(), argv.cwd);
|
|
255
247
|
let pb = null;
|
|
256
|
-
// the game's account, when Arcadible could say: a git deploy's owner waits
|
|
257
|
-
// among their own releases.
|
|
258
|
-
let owner = null;
|
|
259
248
|
// whether the handle rules held the handle back from this release, so the
|
|
260
249
|
// game's links are by id.
|
|
261
250
|
let held = false;
|
|
@@ -277,14 +266,12 @@ const start = function (argv) {
|
|
|
277
266
|
sw.stop(true);
|
|
278
267
|
warn(warnings, { json, quiet });
|
|
279
268
|
sw = new SpinnerWheel("Checking… %s");
|
|
280
|
-
// git deploys need no login: anyone who can push to
|
|
281
|
-
// release, and the Arcadible GitHub App publishes
|
|
282
|
-
//
|
|
283
|
-
//
|
|
269
|
+
// git deploys need no login, and never use one: anyone who can push to
|
|
270
|
+
// the repository can release, and the Arcadible GitHub App publishes
|
|
271
|
+
// the release. Every call is public, so it's the same for whoever runs
|
|
272
|
+
// it, and the saved session isn't read, or refreshed.
|
|
284
273
|
if (git) {
|
|
285
|
-
return
|
|
286
|
-
handler: handler({ json, pb, quiet, sw, warnings }),
|
|
287
|
-
});
|
|
274
|
+
return null;
|
|
288
275
|
}
|
|
289
276
|
return whoami({ handler: handler({ json, pb, quiet, sw, warnings }) });
|
|
290
277
|
})
|
|
@@ -309,7 +296,6 @@ const start = function (argv) {
|
|
|
309
296
|
})
|
|
310
297
|
.then(([current, predeployed]) => {
|
|
311
298
|
sw.stop(true);
|
|
312
|
-
owner = predeployed.owner ?? null;
|
|
313
299
|
held = predeployed.held ?? false;
|
|
314
300
|
sw = new SpinnerWheel("Building… %s");
|
|
315
301
|
sw.start(git || json || quiet);
|
|
@@ -394,7 +380,6 @@ const start = function (argv) {
|
|
|
394
380
|
return waitAndReport(current, deployed, result, {
|
|
395
381
|
git,
|
|
396
382
|
json,
|
|
397
|
-
owner,
|
|
398
383
|
quiet,
|
|
399
384
|
root,
|
|
400
385
|
since,
|
package/package.json
CHANGED
|
@@ -63,27 +63,24 @@ const findDirect = function (id, headers) {
|
|
|
63
63
|
// [function]
|
|
64
64
|
|
|
65
65
|
// a git deploy's release: GitHub's, for this version, made since the tag was
|
|
66
|
-
// pushed. Null until GitHub Actions has built it.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
const findGit = function ({ account, game, headers, since, version }) {
|
|
72
|
-
const newest = account
|
|
73
|
-
? () => api.accountReleases(account, { limit: 20, select: SELECT }, headers)
|
|
74
|
-
: () =>
|
|
75
|
-
api.discoverGameReleases(game, { limit: 10, select: SELECT, version });
|
|
66
|
+
// pushed. Null until GitHub Actions has built it. Looked for among the
|
|
67
|
+
// game's releases, which anyone can read, with no token: a git deploy asks
|
|
68
|
+
// what's true for whoever runs it. A release with no game (a repository that
|
|
69
|
+
// isn't connected, which the predeploy refuses first) isn't among them.
|
|
70
|
+
const findGit = function ({ game, since, version }) {
|
|
76
71
|
return () =>
|
|
77
|
-
|
|
78
|
-
(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
r
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
72
|
+
api
|
|
73
|
+
.discoverGameReleases(game, { limit: 10, select: SELECT, version })
|
|
74
|
+
.then(
|
|
75
|
+
(rows) =>
|
|
76
|
+
(rows ?? []).find(
|
|
77
|
+
(r) =>
|
|
78
|
+
r.source === "github" &&
|
|
79
|
+
r.version === version &&
|
|
80
|
+
r.game === game &&
|
|
81
|
+
Date.parse(r.created_at) >= since - SKEW,
|
|
82
|
+
) ?? null,
|
|
83
|
+
);
|
|
87
84
|
};
|
|
88
85
|
|
|
89
86
|
// [function]
|
|
@@ -123,28 +120,19 @@ const pollRelease = function (find, timeout, onRelease) {
|
|
|
123
120
|
// [function]
|
|
124
121
|
|
|
125
122
|
// waits on a deploy's release. `deployed` is what the deploy resolved: a
|
|
126
|
-
// direct deploy's has the release's id, and a git deploy's its game.
|
|
127
|
-
//
|
|
128
|
-
//
|
|
123
|
+
// direct deploy's has the release's id, and a git deploy's its game. A git
|
|
124
|
+
// deploy has no `current`: it never uses a login. Studio's link is for
|
|
125
|
+
// everyone, since Studio asks whoever opens it to log in.
|
|
129
126
|
const main = function (
|
|
130
127
|
current,
|
|
131
128
|
deployed,
|
|
132
|
-
{ git: isGit,
|
|
129
|
+
{ git: isGit, root, since, timeout },
|
|
133
130
|
{ onRelease } = {},
|
|
134
131
|
) {
|
|
135
|
-
const headers = api.auth(current?.session?.access_token);
|
|
136
132
|
const game = deployed.game;
|
|
137
|
-
const account = current?.token ? null : (current?.user?.id ?? null);
|
|
138
|
-
const owns = !isGit || (account !== null && account === owner);
|
|
139
133
|
const find = isGit
|
|
140
|
-
? findGit({
|
|
141
|
-
|
|
142
|
-
game,
|
|
143
|
-
headers,
|
|
144
|
-
since,
|
|
145
|
-
version: deployed.version.to,
|
|
146
|
-
})
|
|
147
|
-
: findDirect(deployed.release, headers);
|
|
134
|
+
? findGit({ game, since, version: deployed.version.to })
|
|
135
|
+
: findDirect(deployed.release, api.auth(current?.session?.access_token));
|
|
148
136
|
return Promise.all([
|
|
149
137
|
pollRelease(find, timeout, onRelease),
|
|
150
138
|
isGit ? actionsUrl(root) : null,
|
|
@@ -152,7 +140,7 @@ const main = function (
|
|
|
152
140
|
.then(([{ release, timedOut }, actions]) => ({
|
|
153
141
|
actions,
|
|
154
142
|
release,
|
|
155
|
-
studio:
|
|
143
|
+
studio: studioUrl(game, "builds"),
|
|
156
144
|
timedOut,
|
|
157
145
|
}))
|
|
158
146
|
.catch(ended(current));
|
|
@@ -39,11 +39,12 @@ const answerOf = function (request) {
|
|
|
39
39
|
// [function]
|
|
40
40
|
|
|
41
41
|
// what Arcadible would refuse in the release, refused before the tag is pushed: a
|
|
42
|
-
// game that was never deployed (a release can't create one),
|
|
43
|
-
// that's already published
|
|
42
|
+
// game that was never deployed (a release can't create one), a version
|
|
43
|
+
// that's already published, and a handle that isn't the game's (only a
|
|
44
|
+
// direct deploy changes one, so a collaborator's push can't). The lookups
|
|
45
|
+
// are public, so this needs no login.
|
|
44
46
|
// when Arcadible can't be reached, it warns and pushes anyway: a release
|
|
45
47
|
// shouldn't depend on the API, and Arcadible still checks.
|
|
46
|
-
// resolves with the game's owner, or null when Arcadible can't be reached.
|
|
47
48
|
const ensurePublishable = function ({ handle, id, version }, handler) {
|
|
48
49
|
return Promise.all([
|
|
49
50
|
answerOf(api.discoverGame(id)),
|
|
@@ -64,9 +65,17 @@ const ensurePublishable = function ({ handle, id, version }, handler) {
|
|
|
64
65
|
}
|
|
65
66
|
return null;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (build.status === 200) {
|
|
69
|
+
return published.refuse(id, version);
|
|
70
|
+
}
|
|
71
|
+
const current = game.data?.handle ?? null;
|
|
72
|
+
if ((handle ?? null) !== current) {
|
|
73
|
+
throw new ArcError(
|
|
74
|
+
cfgcode.GAME_HANDLE_CHANGED,
|
|
75
|
+
phrase.gameHandleChanged(handle, current),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
70
79
|
});
|
|
71
80
|
};
|
|
72
81
|
|
|
@@ -116,17 +125,20 @@ const ensureConnected = function (root, { handle, id }) {
|
|
|
116
125
|
// [function]
|
|
117
126
|
|
|
118
127
|
// the release's checks: its tag is free, Arcadible would publish it, and
|
|
119
|
-
// origin is its connected repository. resolves with the version
|
|
120
|
-
// game's owner, who waits on the release among their own.
|
|
128
|
+
// origin is its connected repository. resolves with the version.
|
|
121
129
|
const ensureRelease = function (root, game, handler) {
|
|
122
130
|
return ensureTagFree(root, game.version)
|
|
123
131
|
.then(() => ensurePublishable(game, handler))
|
|
124
|
-
.then((
|
|
125
|
-
.then((
|
|
132
|
+
.then(() => ensureConnected(root, game))
|
|
133
|
+
.then(() => ({ to: game.version }));
|
|
126
134
|
};
|
|
127
135
|
|
|
128
136
|
// [function]
|
|
129
137
|
|
|
138
|
+
// a git deploy needs no login, and never uses one: anyone who can push can
|
|
139
|
+
// release, and so can tools other than arc. Every lookup is public, so its
|
|
140
|
+
// answer is the same whoever runs it, and true when it's asked; the release
|
|
141
|
+
// pipeline has the final word.
|
|
130
142
|
const main = function (conf, { onPredeployBegin, ...handler }) {
|
|
131
143
|
const root = conf.root;
|
|
132
144
|
if (onPredeployBegin) {
|
|
@@ -12,6 +12,8 @@ const ensureUnpublished = ac.ensureUnpublished;
|
|
|
12
12
|
|
|
13
13
|
// [function]
|
|
14
14
|
|
|
15
|
+
// a direct deploy needs a login. Each lookup uses its token or not, as it
|
|
16
|
+
// needs, and its answer is true when it's asked.
|
|
15
17
|
const main = function (current, conf, { onPredeployBegin, ...handler }) {
|
|
16
18
|
if (onPredeployBegin) {
|
|
17
19
|
onPredeployBegin({ root: conf.root });
|
package/src/action/serve/tool.js
CHANGED
|
@@ -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,
|
package/src/api.js
CHANGED
|
@@ -136,19 +136,9 @@ const release = function (id, params, headers) {
|
|
|
136
136
|
return request.get(endpoint(pathOf`/releases/${id}`, params), headers);
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
// [function]
|
|
140
|
-
|
|
141
|
-
const accountReleases = function (id, params, headers) {
|
|
142
|
-
return request.get(
|
|
143
|
-
endpoint(pathOf`/accounts/${id}/releases`, params),
|
|
144
|
-
headers,
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
139
|
// [export]
|
|
149
140
|
|
|
150
141
|
module.exports = {
|
|
151
|
-
accountReleases,
|
|
152
142
|
auth,
|
|
153
143
|
discoverGame,
|
|
154
144
|
discoverGameBuild,
|
package/src/config/code.js
CHANGED
|
@@ -49,10 +49,11 @@ const CLI = {
|
|
|
49
49
|
};
|
|
50
50
|
// a release's reasons, from the pipeline (the API's
|
|
51
51
|
// db/type/release_reason.sql). The CLI shares them, so a name means the same
|
|
52
|
-
// thing wherever it appears: GAME_UNAVAILABLE, INTERNAL,
|
|
53
|
-
// and VERSION_EXISTS are the CLI's too.
|
|
52
|
+
// thing wherever it appears: GAME_HANDLE_CHANGED, GAME_UNAVAILABLE, INTERNAL,
|
|
53
|
+
// as well as REPO_NOT_CONNECTED and VERSION_EXISTS, are the CLI's too.
|
|
54
54
|
const RELEASE = {
|
|
55
55
|
BUILD_RATE_LIMITED: "build_rate_limited",
|
|
56
|
+
GAME_HANDLE_CHANGED: "game_handle_changed",
|
|
56
57
|
GAME_LIMIT_REACHED: "game_limit_reached",
|
|
57
58
|
GAME_MISMATCH: "game_mismatch",
|
|
58
59
|
GAME_UNAVAILABLE: "game_unavailable",
|
package/src/phrase.js
CHANGED
|
@@ -225,6 +225,17 @@ const gameHandleTaken = function (handle) {
|
|
|
225
225
|
|
|
226
226
|
// [function]
|
|
227
227
|
|
|
228
|
+
// a git release that would change the game's handle, which only a direct
|
|
229
|
+
// deploy does.
|
|
230
|
+
const gameHandleChanged = function (handle, current) {
|
|
231
|
+
return blocks(
|
|
232
|
+
`The handle in arcadible.toml (${handle ?? "none"}) isn't the game's on ${TITLE} (${current ?? "none"}).`,
|
|
233
|
+
`Only ${cmdDeploy()} changes a handle: deploy with it, or set the handle back.`,
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// [function]
|
|
238
|
+
|
|
228
239
|
const gameHandleLapsed = function (handle) {
|
|
229
240
|
return `Handle ${handle} lapsed, so deploying without it.\nA production release takes it back.`;
|
|
230
241
|
};
|
|
@@ -764,12 +775,12 @@ const packageUncompressedTooLarge = function (size) {
|
|
|
764
775
|
|
|
765
776
|
// a failed release's message, a sentence to a line, then its reason code
|
|
766
777
|
// and where to see it.
|
|
767
|
-
// `studio` is the game's builds in Studio,
|
|
768
|
-
//
|
|
778
|
+
// `studio` is the game's builds in Studio, which asks whoever opens it to
|
|
779
|
+
// log in.
|
|
769
780
|
const releaseFailed = function ({ message }, studio) {
|
|
770
781
|
return blocks(
|
|
771
782
|
(message ?? "No reason given.").split(/(?<=\.)\s+/u).join("\n"),
|
|
772
|
-
|
|
783
|
+
[`${TITLE} Studio:`, studio],
|
|
773
784
|
);
|
|
774
785
|
};
|
|
775
786
|
|
|
@@ -791,18 +802,12 @@ const releaseTimedOut = function (
|
|
|
791
802
|
{ actions, found, git, studio },
|
|
792
803
|
) {
|
|
793
804
|
const head = `Waited ${waited(seconds)} for v${version}.`;
|
|
794
|
-
if (
|
|
805
|
+
if (found || !git) {
|
|
795
806
|
return blocks(head, [
|
|
796
807
|
`It may still finish. See it in ${TITLE} Studio:`,
|
|
797
808
|
studio,
|
|
798
809
|
]);
|
|
799
810
|
}
|
|
800
|
-
if (found) {
|
|
801
|
-
return blocks(head, [
|
|
802
|
-
"It may still finish.",
|
|
803
|
-
`The ${TITLE} check on the tagged commit says how it went.`,
|
|
804
|
-
]);
|
|
805
|
-
}
|
|
806
811
|
return blocks(
|
|
807
812
|
head,
|
|
808
813
|
[
|
|
@@ -1092,6 +1097,7 @@ module.exports = {
|
|
|
1092
1097
|
docMediaType,
|
|
1093
1098
|
docTagUnallowed,
|
|
1094
1099
|
errorCode,
|
|
1100
|
+
gameHandleChanged,
|
|
1095
1101
|
gameHandleLapsed,
|
|
1096
1102
|
gameHandleLimit,
|
|
1097
1103
|
gameHandleTaken,
|
|
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 };
|
package/src/serve/server.js
CHANGED
|
@@ -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
|
|
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 ({
|
|
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,
|
package/src/util/video.js
CHANGED
|
@@ -6,8 +6,8 @@ const path = require("node:path");
|
|
|
6
6
|
|
|
7
7
|
// [import]
|
|
8
8
|
|
|
9
|
-
const createFile = require("mp4box").createFile;
|
|
10
9
|
const fs = require("fs-extra");
|
|
10
|
+
const mp4box = require("mp4box");
|
|
11
11
|
|
|
12
12
|
// [import]
|
|
13
13
|
|
|
@@ -18,6 +18,7 @@ const phrase = require("../phrase");
|
|
|
18
18
|
|
|
19
19
|
// [use]
|
|
20
20
|
|
|
21
|
+
const createFile = mp4box.createFile;
|
|
21
22
|
const checkAspect = media.checkAspect;
|
|
22
23
|
const checkSize = media.checkSize;
|
|
23
24
|
const extname = path.extname;
|
|
@@ -30,6 +31,15 @@ const statSync = fs.statSync; // eslint-disable-line no-sync
|
|
|
30
31
|
|
|
31
32
|
const NO_TRACK = "no-video-track";
|
|
32
33
|
|
|
34
|
+
// [configure]
|
|
35
|
+
|
|
36
|
+
// mp4box prints what it recovers from at its error level, which setLogLevel
|
|
37
|
+
// can't turn down: its colr parser misreads an ICC profile by the box's
|
|
38
|
+
// header, and says so for a video it reads correctly. arc reports a bad video
|
|
39
|
+
// itself, so mp4box only passes on the errors it sends to the file's onError.
|
|
40
|
+
mp4box.Log.error = (mod, msg, isofile) => isofile?.onError?.(mod, msg);
|
|
41
|
+
mp4box.Log.warn = () => {};
|
|
42
|
+
|
|
33
43
|
// [function]
|
|
34
44
|
|
|
35
45
|
const readMp4 = function (file) {
|