@v12-engine/create 0.1.1 → 0.4.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 -1
- package/base/_gitignore +5 -0
- package/base/package.json +3 -3
- package/base/src/main.ts +36 -10
- package/base/src/navigation.worker.ts +7 -0
- package/base/vite.config.ts +24 -2
- package/cli.mjs +21 -6
- package/index.mjs +27 -7
- package/layers/desktop/electron/app.mjs +237 -0
- package/layers/desktop/electron/config.mjs +16 -0
- package/layers/desktop/electron/dev.mjs +63 -0
- package/layers/desktop/electron/files.mjs +86 -0
- package/layers/desktop/electron/icons/icon.icns +0 -0
- package/layers/desktop/electron/icons/icon.png +0 -0
- package/layers/desktop/electron/main.mjs +12 -0
- package/layers/desktop/electron/package.mjs +75 -0
- package/layers/desktop/electron/preload.cjs +41 -0
- package/layers/desktop/electron/presentation.m +87 -0
- package/layers/desktop/electron/serve.mjs +76 -0
- package/layers/desktop/electron/tools.mjs +63 -0
- package/layers/desktop/electron/updates.mjs +71 -0
- package/layers/desktop/package.json +13 -0
- package/layers/desktop/src/desktop.ts +175 -0
- package/layers/desktop/src/shell.ts +279 -0
- package/layers/desktop/tests/main.test.ts +189 -0
- package/layers/desktop/tests/shell.test.ts +152 -0
- package/layers/rooms/server.ts +13 -10
- package/layers/server/server.ts +4 -5
- package/layers/source/vite.config.ts +21 -1
- package/package.json +1 -1
- package/starters/campaign/README.md +9 -2
- package/starters/campaign/layers/rooms/server.ts +3 -3
- package/starters/campaign/layers/server/server.ts +3 -5
- package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
- package/starters/campaign/src/campaign/game.ts +26 -5
- package/starters/campaign/src/campaign/index.ts +104 -68
- package/starters/campaign/src/campaign/kills.ts +1 -1
- package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
- package/starters/campaign/src/campaign/missions/siege.ts +6 -6
- package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
- package/starters/campaign/src/campaign/scenes.ts +8 -7
- package/starters/campaign/src/campaign/units.ts +59 -20
- package/starters/campaign/src/campaign/view/attacks.ts +19 -0
- package/starters/campaign/src/campaign/view/fire.ts +2 -2
- package/starters/campaign/src/campaign/view/index.ts +54 -43
- package/starters/campaign/src/campaign/waves.ts +18 -17
- package/starters/campaign/src/game.ts +8 -1
- package/starters/campaign/src/main.ts +38 -19
- package/starters/campaign/src/maps/fort.names.ts +2 -2
- package/starters/campaign/src/maps/fort.vgmap.json +102 -2
- package/starters/campaign/src/maps/homestead.names.ts +2 -2
- package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
- package/starters/campaign/src/maps/pass.names.ts +2 -2
- package/starters/campaign/src/maps/pass.vgmap.json +69 -1
- package/starters/campaign/src/waves-panel.ts +2 -1
- package/starters/empty/README.md +18 -10
- package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
- package/starters/empty/src/game.ts +11 -75
- package/starters/empty/src/generate.worker.ts +5 -5
- package/starters/empty/src/rules.ts +83 -25
- package/starters/empty/src/view.ts +82 -0
- package/starters/examples/README.md +83 -0
- package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
- package/starters/examples/package.json +5 -0
- package/starters/examples/src/examples/abilities/areas.ts +165 -0
- package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
- package/starters/examples/src/examples/abilities/buffs.ts +287 -0
- package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
- package/starters/examples/src/examples/abilities/changing.ts +228 -0
- package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
- package/starters/examples/src/examples/abilities/control.ts +158 -0
- package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
- package/starters/examples/src/examples/abilities/damage.ts +156 -0
- package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
- package/starters/examples/src/examples/abilities/family.ts +29 -0
- package/starters/examples/src/examples/abilities/index.ts +32 -0
- package/starters/examples/src/examples/abilities/kit.ts +179 -0
- package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
- package/starters/examples/src/examples/abilities/looks.ts +47 -0
- package/starters/examples/src/examples/abilities/missiles.ts +203 -0
- package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
- package/starters/examples/src/examples/abilities/moves.ts +206 -0
- package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
- package/starters/examples/src/examples/abilities/recasts.ts +181 -0
- package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
- package/starters/examples/src/examples/abilities/resources.ts +166 -0
- package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
- package/starters/examples/src/examples/abilities/summons.ts +296 -0
- package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
- package/starters/examples/src/examples/abilities/triggers.ts +204 -0
- package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
- package/starters/examples/src/examples/abilities/vision.ts +137 -0
- package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
- package/starters/examples/src/examples/animation/index.ts +235 -0
- package/starters/examples/src/examples/animation/looks.ts +444 -0
- package/starters/examples/src/examples/catalog.ts +52 -0
- package/starters/examples/src/examples/cinematics/index.ts +217 -0
- package/starters/examples/src/examples/cinematics/looks.ts +476 -0
- package/starters/examples/src/examples/cinematics/tour.json +43 -0
- package/starters/examples/src/examples/cinematics/village.ts +88 -0
- package/starters/examples/src/examples/common/looks.ts +65 -0
- package/starters/examples/src/examples/common/units.ts +70 -0
- package/starters/examples/src/examples/construction/index.ts +329 -0
- package/starters/examples/src/examples/construction/looks.ts +275 -0
- package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
- package/starters/examples/src/examples/custom-animation/index.ts +190 -0
- package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
- package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
- package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
- package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
- package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
- package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
- package/starters/examples/src/examples/custom-fx/index.ts +25 -0
- package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
- package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
- package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
- package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
- package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
- package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
- package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
- package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
- package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
- package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
- package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
- package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
- package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
- package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
- package/starters/examples/src/examples/define.ts +86 -0
- package/starters/examples/src/examples/destruction/index.ts +570 -0
- package/starters/examples/src/examples/destruction/looks.ts +447 -0
- package/starters/examples/src/examples/dialogs/index.ts +383 -0
- package/starters/examples/src/examples/dialogs/looks.ts +546 -0
- package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
- package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
- package/starters/examples/src/examples/fx/blood.look.ts +62 -0
- package/starters/examples/src/examples/fx/blood.ts +59 -0
- package/starters/examples/src/examples/fx/feel.look.ts +86 -0
- package/starters/examples/src/examples/fx/feel.ts +57 -0
- package/starters/examples/src/examples/fx/fight.ts +28 -0
- package/starters/examples/src/examples/fx/floats.look.ts +108 -0
- package/starters/examples/src/examples/fx/floats.ts +24 -0
- package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
- package/starters/examples/src/examples/fx/gallery.ts +23 -0
- package/starters/examples/src/examples/fx/index.ts +30 -0
- package/starters/examples/src/examples/fx/links.look.ts +83 -0
- package/starters/examples/src/examples/fx/links.ts +49 -0
- package/starters/examples/src/examples/fx/looks.ts +55 -0
- package/starters/examples/src/examples/fx/map.look.ts +93 -0
- package/starters/examples/src/examples/fx/map.ts +33 -0
- package/starters/examples/src/examples/fx/marks.look.ts +100 -0
- package/starters/examples/src/examples/fx/marks.ts +36 -0
- package/starters/examples/src/examples/fx/shots.look.ts +92 -0
- package/starters/examples/src/examples/fx/shots.ts +71 -0
- package/starters/examples/src/examples/fx/spells.look.ts +87 -0
- package/starters/examples/src/examples/fx/spells.ts +122 -0
- package/starters/examples/src/examples/fx/stage.ts +31 -0
- package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
- package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
- package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
- package/starters/examples/src/examples/gui/index.ts +143 -0
- package/starters/examples/src/examples/gui/keys.ts +91 -0
- package/starters/examples/src/examples/gui/layout.ts +42 -0
- package/starters/examples/src/examples/gui/looks.ts +270 -0
- package/starters/examples/src/examples/gui/minimap.ts +127 -0
- package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
- package/starters/examples/src/examples/gui/plates.ts +87 -0
- package/starters/examples/src/examples/gui/radial.ts +104 -0
- package/starters/examples/src/examples/gui/solidPart.ts +107 -0
- package/starters/examples/src/examples/looks.ts +45 -0
- package/starters/examples/src/examples/mechanics/battle.ts +51 -0
- package/starters/examples/src/examples/mechanics/damage.ts +80 -0
- package/starters/examples/src/examples/mechanics/flying.ts +93 -0
- package/starters/examples/src/examples/mechanics/formations.ts +58 -0
- package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
- package/starters/examples/src/examples/mechanics/index.ts +44 -0
- package/starters/examples/src/examples/mechanics/looks.ts +289 -0
- package/starters/examples/src/examples/mechanics/maze.ts +119 -0
- package/starters/examples/src/examples/mechanics/orders.ts +68 -0
- package/starters/examples/src/examples/mechanics/selection.ts +30 -0
- package/starters/examples/src/examples/moba/index.ts +377 -0
- package/starters/examples/src/examples/moba/looks.ts +349 -0
- package/starters/examples/src/examples/moba/maps.ts +69 -0
- package/starters/examples/src/examples/moba/spells.ts +34 -0
- package/starters/examples/src/examples/moba/units.ts +196 -0
- package/starters/examples/src/examples/nature/camera.ts +15 -0
- package/starters/examples/src/examples/nature/grass.ts +64 -0
- package/starters/examples/src/examples/nature/index.ts +327 -0
- package/starters/examples/src/examples/nature/land.ts +173 -0
- package/starters/examples/src/examples/nature/looks.ts +577 -0
- package/starters/examples/src/examples/nature/maps.ts +370 -0
- package/starters/examples/src/examples/nature/skies.ts +324 -0
- package/starters/examples/src/examples/nature/weather.ts +80 -0
- package/starters/examples/src/examples/performance/index.ts +263 -0
- package/starters/examples/src/examples/performance/looks.ts +372 -0
- package/starters/examples/src/examples/performance/meter.ts +158 -0
- package/starters/examples/src/examples/post/grades.ts +86 -0
- package/starters/examples/src/examples/post/index.ts +202 -0
- package/starters/examples/src/examples/post/looks.ts +884 -0
- package/starters/examples/src/examples/post/passes.ts +213 -0
- package/starters/examples/src/examples/rts/ai.ts +49 -0
- package/starters/examples/src/examples/rts/airlift.ts +43 -0
- package/starters/examples/src/examples/rts/base.ts +129 -0
- package/starters/examples/src/examples/rts/build.ts +65 -0
- package/starters/examples/src/examples/rts/camps.ts +36 -0
- package/starters/examples/src/examples/rts/carry.ts +103 -0
- package/starters/examples/src/examples/rts/creeps.ts +61 -0
- package/starters/examples/src/examples/rts/data.ts +406 -0
- package/starters/examples/src/examples/rts/ferry.ts +41 -0
- package/starters/examples/src/examples/rts/garrison.ts +50 -0
- package/starters/examples/src/examples/rts/harvest.ts +29 -0
- package/starters/examples/src/examples/rts/hero.ts +67 -0
- package/starters/examples/src/examples/rts/index.ts +77 -0
- package/starters/examples/src/examples/rts/looks.ts +721 -0
- package/starters/examples/src/examples/rts/shop.ts +46 -0
- package/starters/examples/src/examples/rts/tech.ts +39 -0
- package/starters/examples/src/examples/rts/train.ts +48 -0
- package/starters/examples/src/examples/rts/waterside.ts +185 -0
- package/starters/examples/src/examples/showcase/index.ts +714 -0
- package/starters/examples/src/examples/showcase/looks.ts +238 -0
- package/starters/examples/src/game.ts +17 -0
- package/starters/examples/src/main.ts +82 -0
- package/starters/examples/src/rules.ts +31 -0
- package/starters/examples/src/shell/look.ts +79 -0
- package/starters/examples/src/shell/registry.ts +7 -0
- package/starters/examples/src/shell/scenes.ts +178 -0
- package/starters/examples/src/shell/shell.ts +290 -0
- package/starters/examples/src/shell/style.ts +202 -0
- package/starters/examples/src/view.ts +50 -0
- package/starters/melee/README.md +12 -3
- package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
- package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
- package/starters/melee/src/game.ts +32 -2
- package/starters/melee/src/maps/bay.names.ts +2 -2
- package/starters/melee/src/maps/bay.vgmap.json +198 -1
- package/starters/melee/src/maps.ts +11 -9
- package/starters/melee/src/melee/abilities.ts +1 -194
- package/starters/melee/src/melee/blight.ts +7 -7
- package/starters/melee/src/melee/cellLayers.json +3 -3
- package/starters/melee/src/melee/data.json +1 -18
- package/starters/melee/src/melee/game.ts +6 -4
- package/starters/melee/src/melee/heroes.ts +1025 -0
- package/starters/melee/src/melee/index.ts +64 -61
- package/starters/melee/src/melee/items.ts +15 -362
- package/starters/melee/src/melee/languages/he.json +14 -2
- package/starters/melee/src/melee/pings.ts +5 -6
- package/starters/melee/src/melee/saves.ts +1 -1
- package/starters/melee/src/melee/undead.ts +15 -230
- package/starters/melee/src/melee/units.ts +37 -171
- package/starters/melee/src/melee/view/attacks.ts +19 -0
- package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
- package/starters/melee/src/melee/view/index.ts +56 -52
- package/starters/melee/src/melee/view/looks.ts +86 -63
- package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
- package/starters/melee/src/melee/view/walls.ts +1 -1
- package/starters.mjs +6 -0
- package/v12.mjs +14 -15
- package/starters/campaign/src/rules.ts +0 -20
- package/starters/empty/src/maps.ts +0 -44
- package/starters/empty/src/tiles.ts +0 -12
- package/starters/melee/src/melee/heroSpells.ts +0 -38
- package/starters/melee/src/rules.ts +0 -24
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// `npm run app`: the game in its shell while it is developed: the dev server started (Vite, on port 3100) and the
|
|
2
|
+
// shell on it, so a change to the page shows as it does in a browser. `npm run app:editor` (`--attach`) starts no
|
|
3
|
+
// server: it opens the shell on the one already running, which is the map editor's Run panel's way, beside the dev
|
|
4
|
+
// server the editor is open on. Closing the app stops what this started.
|
|
5
|
+
// `--port <n>` puts the server on another port, or attaches to one there; `--page <address>` attaches to a page
|
|
6
|
+
// served elsewhere (`--attach` is then implied): `node electron/dev.mjs --page http://localhost:3000/`
|
|
7
|
+
import { spawn } from 'node:child_process'
|
|
8
|
+
import { buildNative, electronBinary, project } from './tools.mjs'
|
|
9
|
+
|
|
10
|
+
const PORT = 3100
|
|
11
|
+
|
|
12
|
+
/** The value given after a flag, as `--port 3200` or `--port=3200` */
|
|
13
|
+
const option = (name) => {
|
|
14
|
+
const at = process.argv.findIndex((argument) => argument === name || argument.startsWith(`${name}=`))
|
|
15
|
+
if (at < 0) return undefined
|
|
16
|
+
const argument = process.argv[at]
|
|
17
|
+
return argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : process.argv[at + 1]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const askedPort = option('--port')
|
|
21
|
+
const port = askedPort === undefined ? PORT : Number(askedPort)
|
|
22
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
23
|
+
console.error(`--port takes a port, not "${askedPort}"`)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
const page = option('--page')
|
|
27
|
+
const attach = process.argv.includes('--attach') || page !== undefined
|
|
28
|
+
let address = `http://localhost:${port}/`
|
|
29
|
+
if (page !== undefined) {
|
|
30
|
+
try {
|
|
31
|
+
address = new URL(page).href
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(`--page takes an address, as http://localhost:3000/, not "${page}"`)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
buildNative()
|
|
39
|
+
const binary = electronBinary()
|
|
40
|
+
let server
|
|
41
|
+
if (attach) {
|
|
42
|
+
const answered = await fetch(address).then(
|
|
43
|
+
(response) => response.ok,
|
|
44
|
+
() => false
|
|
45
|
+
)
|
|
46
|
+
if (!answered) {
|
|
47
|
+
console.error(`No dev server at ${address}: start it first (npm run dev), or run npm run app, which starts one`)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
const { createServer } = await import('vite')
|
|
52
|
+
server = await createServer({ root: project, server: { port, strictPort: true } })
|
|
53
|
+
await server.listen()
|
|
54
|
+
server.printUrls()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const shell = spawn(binary, [project], { stdio: 'inherit', env: { ...process.env, V12_PAGE: address } })
|
|
58
|
+
const stop = async (code) => {
|
|
59
|
+
await server?.close()
|
|
60
|
+
process.exit(code ?? 0)
|
|
61
|
+
}
|
|
62
|
+
shell.on('exit', (code) => void stop(code))
|
|
63
|
+
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => shell.kill(signal))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// What the page may ask of the disk, on the main process's side: records in the folders named here, and the two log
|
|
2
|
+
// files, nothing else. The page's words are not trusted as a path. No Electron here, so that the tests run it in Node.
|
|
3
|
+
import { mkdir, readdir, readFile, rename, rm, stat, writeFile, appendFile } from 'node:fs/promises'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/** The folders of records a page may name */
|
|
8
|
+
export const RECORD_FOLDERS = ['saves', 'settings']
|
|
9
|
+
/** The files of the log a page may name */
|
|
10
|
+
export const LOG_FILES = ['errors.log', 'errors.old.log']
|
|
11
|
+
|
|
12
|
+
function folderIn(base, folder) {
|
|
13
|
+
if (!RECORD_FOLDERS.includes(folder)) throw new Error(`"${folder}" is not a folder of the app's`)
|
|
14
|
+
return join(base, folder)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The name of a record's file, from its id: the id itself where it is a plain name (letters, digits, `_`, `-` and dots,
|
|
19
|
+
* not first), and otherwise the id written in letters and digits (base64url) after a `~`, which no plain name has: a
|
|
20
|
+
* checkpoint's id has colons and braces in it. Nothing that moves out of the folder either way. A very long id is
|
|
21
|
+
* named by its hash
|
|
22
|
+
*/
|
|
23
|
+
export function fileNameOf(id) {
|
|
24
|
+
if (typeof id !== 'string' || !id) throw new Error(`"${id}" is not the id of a record`)
|
|
25
|
+
if (/^[\w-][\w.-]{0,119}$/.test(id)) return id
|
|
26
|
+
const written = Buffer.from(id).toString('base64url')
|
|
27
|
+
return written.length <= 160 ? `~${written}` : `~h${createHash('sha256').update(id).digest('base64url')}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function recordIn(base, folder, id) {
|
|
31
|
+
return join(folderIn(base, folder), `${fileNameOf(id)}.json`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function logIn(base, file) {
|
|
35
|
+
if (!LOG_FILES.includes(file)) throw new Error(`"${file}" is not a file of the log`)
|
|
36
|
+
return join(base, file)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The records under the app's data folder, as text, each whole on the disk or not at all */
|
|
40
|
+
export function recordFiles(base) {
|
|
41
|
+
return {
|
|
42
|
+
async list(folder) {
|
|
43
|
+
const path = folderIn(base, folder)
|
|
44
|
+
let names
|
|
45
|
+
try {
|
|
46
|
+
names = await readdir(path)
|
|
47
|
+
} catch {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
const texts = await Promise.all(
|
|
51
|
+
names.filter((name) => name.endsWith('.json')).map((name) => readFile(join(path, name), 'utf8').catch(() => undefined))
|
|
52
|
+
)
|
|
53
|
+
return texts.filter((text) => text !== undefined)
|
|
54
|
+
},
|
|
55
|
+
read: (folder, id) => readFile(recordIn(base, folder, id), 'utf8').catch(() => undefined),
|
|
56
|
+
async write(folder, id, text) {
|
|
57
|
+
const file = recordIn(base, folder, id)
|
|
58
|
+
await mkdir(folderIn(base, folder), { recursive: true })
|
|
59
|
+
const partial = `${file}.tmp`
|
|
60
|
+
try {
|
|
61
|
+
await writeFile(partial, text)
|
|
62
|
+
await rename(partial, file)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
await rm(partial, { force: true })
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
remove: (folder, id) => rm(recordIn(base, folder, id), { force: true })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The two files of the error log, in the system's folder of the app's logs */
|
|
73
|
+
export function logFiles(base) {
|
|
74
|
+
return {
|
|
75
|
+
async append(file, text) {
|
|
76
|
+
await mkdir(base, { recursive: true })
|
|
77
|
+
await appendFile(logIn(base, file), text)
|
|
78
|
+
},
|
|
79
|
+
size: (file) =>
|
|
80
|
+
stat(logIn(base, file)).then(
|
|
81
|
+
(found) => found.size,
|
|
82
|
+
() => 0
|
|
83
|
+
),
|
|
84
|
+
rename: async (from, to) => rename(logIn(base, from), logIn(base, to))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The app's main process (`main` in `package.json`): the shell, on the built page, or on the dev server where
|
|
2
|
+
// `npm run app` says one (`dev.mjs`). `V12_USER_DATA` puts the app's folder elsewhere, for a check that is not to touch
|
|
3
|
+
// the player's saves
|
|
4
|
+
import { app } from 'electron'
|
|
5
|
+
import { startShell } from './app.mjs'
|
|
6
|
+
|
|
7
|
+
const { V12_PAGE: page, V12_USER_DATA: userData } = process.env
|
|
8
|
+
// (no `await` at the top of the file: Electron's start waits for this file, and the app's being ready waits for that)
|
|
9
|
+
startShell({ ...(page && !app.isPackaged && { page }), ...(userData && { userData }) }).catch((error) => {
|
|
10
|
+
console.error(error)
|
|
11
|
+
app.exit(1)
|
|
12
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// `npm run package`: the app to hand out, by Electron's own "manual packaging" (its Application Packaging guide), which
|
|
2
|
+
// adds no package to the project: the page built (`vite build`), then Electron's own app copied with the main process and
|
|
3
|
+
// the built page under `resources/app`, named and given its version and icon. Into `out/`: on macOS `out/<name>.app`,
|
|
4
|
+
// signed ad hoc so that it opens on this Mac (a zip of it beside, for the updater's feed, with `--zip`); on Windows and
|
|
5
|
+
// Linux a folder `out/<name>-<system>-<processor>` with the app's executable in it. It is made for the system it runs on.
|
|
6
|
+
// Signing for other Macs (Apple's notarization), Windows' certificate and installers are Electron Forge's
|
|
7
|
+
// (`docs/scenes/desktop.md`, Before the app is handed out).
|
|
8
|
+
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { IDENTIFIER, NAME } from './config.mjs'
|
|
12
|
+
import { buildNative, electronDist, electronVersion, project } from './tools.mjs'
|
|
13
|
+
|
|
14
|
+
/** The main process's files: what the app runs, and none of what makes it (this file, the addon's source, the dev server's) */
|
|
15
|
+
const RUNS = ['main.mjs', 'app.mjs', 'config.mjs', 'files.mjs', 'serve.mjs', 'updates.mjs', 'preload.cjs', 'presentation.node', 'icons/icon.png']
|
|
16
|
+
|
|
17
|
+
const manifest = JSON.parse(readFileSync(join(project, 'package.json'), 'utf8'))
|
|
18
|
+
const version = manifest.version ?? '0.0.0'
|
|
19
|
+
const zip = process.argv.includes('--zip')
|
|
20
|
+
|
|
21
|
+
buildNative()
|
|
22
|
+
const dist = electronDist()
|
|
23
|
+
const { build } = await import('vite')
|
|
24
|
+
await build({ root: project, logLevel: 'warn' })
|
|
25
|
+
if (!existsSync(join(project, 'dist/index.html'))) throw new Error('The page was not built: no dist/index.html')
|
|
26
|
+
|
|
27
|
+
const out = join(project, 'out')
|
|
28
|
+
mkdirSync(out, { recursive: true })
|
|
29
|
+
let made
|
|
30
|
+
if (process.platform === 'darwin') {
|
|
31
|
+
made = join(out, `${NAME}.app`)
|
|
32
|
+
rmSync(made, { recursive: true, force: true })
|
|
33
|
+
// (`cp -R` keeps the bundle's links and modes, which a copy by Node may not)
|
|
34
|
+
execFileSync('cp', ['-R', join(dist, 'Electron.app'), made])
|
|
35
|
+
const resources = join(made, 'Contents/Resources')
|
|
36
|
+
putApp(resources)
|
|
37
|
+
cpSync(join(project, 'electron/icons/icon.icns'), join(resources, 'electron.icns'))
|
|
38
|
+
const plist = join(made, 'Contents/Info.plist')
|
|
39
|
+
for (const [key, value] of [
|
|
40
|
+
['CFBundleDisplayName', NAME],
|
|
41
|
+
['CFBundleName', NAME],
|
|
42
|
+
['CFBundleIdentifier', IDENTIFIER],
|
|
43
|
+
['CFBundleShortVersionString', version],
|
|
44
|
+
['CFBundleVersion', version]
|
|
45
|
+
])
|
|
46
|
+
execFileSync('plutil', ['-replace', key, '-string', value, plist])
|
|
47
|
+
// ad hoc: this Mac opens it; another refuses it until it is signed with a certificate and notarized
|
|
48
|
+
execFileSync('codesign', ['--force', '--deep', '--sign', '-', made], { stdio: 'inherit' })
|
|
49
|
+
if (zip) execFileSync('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', made, join(out, `${NAME}-${version}-darwin-${process.arch}.zip`)])
|
|
50
|
+
} else {
|
|
51
|
+
made = join(out, `${NAME}-${process.platform}-${process.arch}`)
|
|
52
|
+
rmSync(made, { recursive: true, force: true })
|
|
53
|
+
cpSync(dist, made, { recursive: true, verbatimSymlinks: true })
|
|
54
|
+
putApp(join(made, 'resources'))
|
|
55
|
+
const executable = process.platform === 'win32' ? 'electron.exe' : 'electron'
|
|
56
|
+
renameSync(join(made, executable), join(made, process.platform === 'win32' ? `${NAME}.exe` : NAME))
|
|
57
|
+
}
|
|
58
|
+
console.log(`${made} (Electron ${electronVersion()}, version ${version})`)
|
|
59
|
+
|
|
60
|
+
/** The app's own files under Electron's resources, in the place of its default app */
|
|
61
|
+
function putApp(resources) {
|
|
62
|
+
rmSync(join(resources, 'default_app.asar'), { force: true })
|
|
63
|
+
const inside = join(resources, 'app')
|
|
64
|
+
rmSync(inside, { recursive: true, force: true })
|
|
65
|
+
for (const file of RUNS) {
|
|
66
|
+
const from = join(project, 'electron', file)
|
|
67
|
+
// (the addon is there on macOS only)
|
|
68
|
+
if (existsSync(from)) cpSync(from, join(inside, 'electron', file))
|
|
69
|
+
}
|
|
70
|
+
cpSync(join(project, 'dist'), join(inside, 'dist'), { recursive: true })
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(inside, 'package.json'),
|
|
73
|
+
`${JSON.stringify({ name: manifest.name, productName: NAME, version, type: 'module', main: 'electron/main.mjs' }, null, '\t')}\n`
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// All that the page may ask of the main process (`app.mjs`), and nothing of Electron's own: `src/shell.ts` is the one
|
|
2
|
+
// file that reads it
|
|
3
|
+
const { contextBridge, ipcRenderer } = require('electron')
|
|
4
|
+
|
|
5
|
+
/** A message of the main process's, to a function of the page's; gives back what stops it */
|
|
6
|
+
const listen = (channel, listener) => {
|
|
7
|
+
const heard = () => listener()
|
|
8
|
+
ipcRenderer.on(channel, heard)
|
|
9
|
+
return () => ipcRenderer.removeListener(channel, heard)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
contextBridge.exposeInMainWorld('v12Shell', {
|
|
13
|
+
fullscreen: (on) => ipcRenderer.invoke('v12:fullscreen', on),
|
|
14
|
+
close: () => ipcRenderer.invoke('v12:close'),
|
|
15
|
+
destroy: () => ipcRenderer.invoke('v12:destroy'),
|
|
16
|
+
onResized: (listener) => listen('v12:resized', listener),
|
|
17
|
+
onCloseRequested: (listener) => {
|
|
18
|
+
ipcRenderer.send('v12:close-is-heard', true)
|
|
19
|
+
const off = listen('v12:close-requested', listener)
|
|
20
|
+
return () => {
|
|
21
|
+
ipcRenderer.send('v12:close-is-heard', false)
|
|
22
|
+
off()
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
openFolder: (folder) => ipcRenderer.invoke('v12:open-folder', folder),
|
|
26
|
+
records: {
|
|
27
|
+
list: (folder) => ipcRenderer.invoke('v12:records', 'list', folder),
|
|
28
|
+
read: (folder, id) => ipcRenderer.invoke('v12:records', 'read', folder, id),
|
|
29
|
+
write: (folder, id, text) => ipcRenderer.invoke('v12:records', 'write', folder, id, text),
|
|
30
|
+
remove: (folder, id) => ipcRenderer.invoke('v12:records', 'remove', folder, id)
|
|
31
|
+
},
|
|
32
|
+
log: {
|
|
33
|
+
append: (file, text) => ipcRenderer.invoke('v12:log', 'append', file, text),
|
|
34
|
+
size: (file) => ipcRenderer.invoke('v12:log', 'size', file),
|
|
35
|
+
rename: (from, to) => ipcRenderer.invoke('v12:log', 'rename', from, to)
|
|
36
|
+
},
|
|
37
|
+
updates: {
|
|
38
|
+
check: () => ipcRenderer.invoke('v12:update-check'),
|
|
39
|
+
install: () => ipcRenderer.invoke('v12:update-install')
|
|
40
|
+
}
|
|
41
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// What Electron's API does not have: the app's presentation options (`NSApp.presentationOptions`), said and read. Without
|
|
2
|
+
// it `setSimpleFullScreen` leaves AutoHideDock|AutoHideMenuBar, whose Dock and menu bar come back at the screen's edges.
|
|
3
|
+
// A Node-API addon, whose functions are stable across Electron's versions, with the few it asks for declared here so that
|
|
4
|
+
// no headers are needed: `tools.mjs` compiles it with the system's clang (Xcode's command line tools) and nothing else,
|
|
5
|
+
// as `npm run app` and `npm run package` start. Electron gives the symbols as the addon is loaded (`-undefined dynamic_lookup`).
|
|
6
|
+
#import <AppKit/AppKit.h>
|
|
7
|
+
#include <stddef.h>
|
|
8
|
+
#include <stdint.h>
|
|
9
|
+
|
|
10
|
+
typedef struct napi_env__ *napi_env;
|
|
11
|
+
typedef struct napi_value__ *napi_value;
|
|
12
|
+
typedef struct napi_callback_info__ *napi_callback_info;
|
|
13
|
+
typedef int napi_status;
|
|
14
|
+
typedef napi_value (*napi_callback)(napi_env, napi_callback_info);
|
|
15
|
+
extern napi_status napi_get_cb_info(napi_env, napi_callback_info, size_t *, napi_value *, napi_value *, void **);
|
|
16
|
+
extern napi_status napi_get_value_uint32(napi_env, napi_value, uint32_t *);
|
|
17
|
+
extern napi_status napi_create_uint32(napi_env, uint32_t, napi_value *);
|
|
18
|
+
extern napi_status napi_create_function(napi_env, const char *, size_t, napi_callback, void *, napi_value *);
|
|
19
|
+
extern napi_status napi_set_named_property(napi_env, napi_value, const char *, napi_value);
|
|
20
|
+
|
|
21
|
+
static uint32_t first_argument(napi_env env, napi_callback_info info) {
|
|
22
|
+
size_t count = 1;
|
|
23
|
+
napi_value argument = NULL;
|
|
24
|
+
uint32_t value = 0;
|
|
25
|
+
napi_get_cb_info(env, info, &count, &argument, NULL, NULL);
|
|
26
|
+
if (count > 0) napi_get_value_uint32(env, argument, &value);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static napi_value number(napi_env env, uint32_t value) {
|
|
31
|
+
napi_value out = NULL;
|
|
32
|
+
napi_create_uint32(env, value, &out);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The app's options said; what they read as after */
|
|
37
|
+
static napi_value set_options(napi_env env, napi_callback_info info) {
|
|
38
|
+
NSApp.presentationOptions = (NSApplicationPresentationOptions)first_argument(env, info);
|
|
39
|
+
return number(env, (uint32_t)NSApp.presentationOptions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The options of this app, whether or not it is the active one */
|
|
43
|
+
static napi_value own_options(napi_env env, napi_callback_info info) {
|
|
44
|
+
return number(env, (uint32_t)NSApp.presentationOptions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The options the system holds now: the active app's */
|
|
48
|
+
static napi_value system_options(napi_env env, napi_callback_info info) {
|
|
49
|
+
return number(env, (uint32_t)NSApp.currentSystemPresentationOptions);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A key pressed and let go, as an AppKit event posted inside the app, by its key code: for the probes alone, as macOS
|
|
54
|
+
* refuses an agent real keys. It comes in by the app's event queue, where the system's own have already been through
|
|
55
|
+
* what the system takes (Cmd+Tab never gets this far)
|
|
56
|
+
*/
|
|
57
|
+
static napi_value post_key(napi_env env, napi_callback_info info) {
|
|
58
|
+
uint32_t code = first_argument(env, info);
|
|
59
|
+
for (int down = 1; down >= 0; down--) {
|
|
60
|
+
NSEvent *event = [NSEvent keyEventWithType:down ? NSEventTypeKeyDown : NSEventTypeKeyUp
|
|
61
|
+
location:NSZeroPoint
|
|
62
|
+
modifierFlags:0
|
|
63
|
+
timestamp:NSProcessInfo.processInfo.systemUptime
|
|
64
|
+
windowNumber:NSApp.keyWindow.windowNumber
|
|
65
|
+
context:nil
|
|
66
|
+
characters:code == 53 ? @"\x1b" : @""
|
|
67
|
+
charactersIgnoringModifiers:code == 53 ? @"\x1b" : @""
|
|
68
|
+
isARepeat:NO
|
|
69
|
+
keyCode:(unsigned short)code];
|
|
70
|
+
[NSApp postEvent:event atStart:NO];
|
|
71
|
+
}
|
|
72
|
+
return number(env, code);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static void put(napi_env env, napi_value exports, const char *name, napi_callback callback) {
|
|
76
|
+
napi_value function = NULL;
|
|
77
|
+
napi_create_function(env, name, (size_t)-1, callback, NULL, &function);
|
|
78
|
+
napi_set_named_property(env, exports, name, function);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
napi_value napi_register_module_v1(napi_env env, napi_value exports) {
|
|
82
|
+
put(env, exports, "setOptions", set_options);
|
|
83
|
+
put(env, exports, "ownOptions", own_options);
|
|
84
|
+
put(env, exports, "systemOptions", system_options);
|
|
85
|
+
put(env, exports, "postKey", post_key);
|
|
86
|
+
return exports;
|
|
87
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The built page, answered from a scheme of the app's own: modules, WebAssembly and what the browser keeps all want an
|
|
2
|
+
// origin, which a `file:` page has not. Parts of a file where a part is asked (music). No Electron here: the tests run it in Node.
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { extname, join, normalize, sep } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const TYPES = {
|
|
7
|
+
'.html': 'text/html; charset=utf-8',
|
|
8
|
+
'.js': 'text/javascript',
|
|
9
|
+
'.mjs': 'text/javascript',
|
|
10
|
+
'.css': 'text/css',
|
|
11
|
+
'.json': 'application/json',
|
|
12
|
+
'.wasm': 'application/wasm',
|
|
13
|
+
'.png': 'image/png',
|
|
14
|
+
'.jpg': 'image/jpeg',
|
|
15
|
+
'.jpeg': 'image/jpeg',
|
|
16
|
+
'.webp': 'image/webp',
|
|
17
|
+
'.ktx2': 'image/ktx2',
|
|
18
|
+
'.svg': 'image/svg+xml',
|
|
19
|
+
'.woff2': 'font/woff2',
|
|
20
|
+
'.woff': 'font/woff',
|
|
21
|
+
'.mp3': 'audio/mpeg',
|
|
22
|
+
'.ogg': 'audio/ogg',
|
|
23
|
+
'.wav': 'audio/wav',
|
|
24
|
+
'.glb': 'model/gltf-binary',
|
|
25
|
+
'.gltf': 'model/gltf+json'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The page's content security policy: its own origin and nothing else, with `blob:` and `data:` for what Babylon makes in
|
|
30
|
+
* the page (textures, sounds, workers), `'wasm-unsafe-eval'` for WebAssembly (the navigation, the physics) and inline
|
|
31
|
+
* styles (the HUD's). `connect`: addresses besides the app's own, such as a game's server
|
|
32
|
+
*/
|
|
33
|
+
export const contentPolicy = (connect = []) =>
|
|
34
|
+
[
|
|
35
|
+
"default-src 'self'",
|
|
36
|
+
"script-src 'self' 'wasm-unsafe-eval'",
|
|
37
|
+
"style-src 'self' 'unsafe-inline'",
|
|
38
|
+
"img-src 'self' data: blob:",
|
|
39
|
+
"font-src 'self' data:",
|
|
40
|
+
"media-src 'self' data: blob:",
|
|
41
|
+
"worker-src 'self' blob:",
|
|
42
|
+
`connect-src 'self' blob: data:${connect.map((address) => ` ${address}`).join('')}`,
|
|
43
|
+
"object-src 'none'",
|
|
44
|
+
"base-uri 'none'",
|
|
45
|
+
"frame-src 'none'"
|
|
46
|
+
].join('; ')
|
|
47
|
+
|
|
48
|
+
/** The answer to a request for a file of the built page in `root`; never a file outside it */
|
|
49
|
+
export async function answer(root, request, policy) {
|
|
50
|
+
const { pathname } = new URL(request.url)
|
|
51
|
+
const asked = pathname === '/' ? 'index.html' : decodeURIComponent(pathname).slice(1)
|
|
52
|
+
const path = normalize(join(root, asked))
|
|
53
|
+
if (!path.startsWith(root.endsWith(sep) ? root : root + sep)) return new Response('Not found', { status: 404 })
|
|
54
|
+
let bytes
|
|
55
|
+
try {
|
|
56
|
+
bytes = await readFile(path)
|
|
57
|
+
} catch {
|
|
58
|
+
return new Response('Not found', { status: 404 })
|
|
59
|
+
}
|
|
60
|
+
const headers = {
|
|
61
|
+
'Content-Type': TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream',
|
|
62
|
+
'Accept-Ranges': 'bytes',
|
|
63
|
+
'Content-Security-Policy': policy
|
|
64
|
+
}
|
|
65
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(request.headers.get('range') ?? '')
|
|
66
|
+
if (range && (range[1] || range[2])) {
|
|
67
|
+
const start = range[1] ? Number(range[1]) : Math.max(0, bytes.length - Number(range[2]))
|
|
68
|
+
const end = range[1] && range[2] ? Math.min(Number(range[2]), bytes.length - 1) : bytes.length - 1
|
|
69
|
+
if (start > end) return new Response(null, { status: 416, headers: { 'Content-Range': `bytes */${bytes.length}` } })
|
|
70
|
+
return new Response(bytes.subarray(start, end + 1), {
|
|
71
|
+
status: 206,
|
|
72
|
+
headers: { ...headers, 'Content-Range': `bytes ${start}-${end}/${bytes.length}` }
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
return new Response(bytes, { status: 200, headers })
|
|
76
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What `dev.mjs` and `package.mjs` share: Electron itself, fetched the first time it is asked for and not by the
|
|
2
|
+
// install (its package has no install step: some 100 to 260 MB, which a project's other work does not pay for), and the
|
|
3
|
+
// presentation addon compiled on macOS.
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
import { existsSync, statSync } from 'node:fs'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
/** The project's folder */
|
|
11
|
+
export const project = fileURLToPath(new URL('..', import.meta.url))
|
|
12
|
+
const require = createRequire(join(project, 'package.json'))
|
|
13
|
+
|
|
14
|
+
/** Electron's executable, fetched where it is not there yet (into Electron's cache, then its package's folder) */
|
|
15
|
+
export const electronBinary = () => require('electron')
|
|
16
|
+
|
|
17
|
+
/** Electron's own folder of the app (`Electron.app` on macOS, the folder of `electron.exe` or `electron` elsewhere), fetched as above */
|
|
18
|
+
export function electronDist() {
|
|
19
|
+
electronBinary()
|
|
20
|
+
return join(dirname(require.resolve('electron/package.json')), 'dist')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Electron's version, which a packaged app says it was made with */
|
|
24
|
+
export const electronVersion = () => require('electron/package.json').version
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `presentation.m` compiled into `electron/presentation.node`, on macOS where it is not there or is older than its
|
|
28
|
+
* source. Nothing elsewhere, which has no such options. Without Xcode's command line tools (`xcode-select --install`)
|
|
29
|
+
* it says so and the shell runs without it, the Dock and the menu bar auto-hiding at the edges
|
|
30
|
+
*/
|
|
31
|
+
export function buildNative() {
|
|
32
|
+
if (process.platform !== 'darwin') return false
|
|
33
|
+
const source = join(project, 'electron/presentation.m')
|
|
34
|
+
const built = join(project, 'electron/presentation.node')
|
|
35
|
+
if (existsSync(built) && statSync(built).mtimeMs >= statSync(source).mtimeMs) return true
|
|
36
|
+
try {
|
|
37
|
+
execFileSync(
|
|
38
|
+
'clang',
|
|
39
|
+
[
|
|
40
|
+
'-bundle',
|
|
41
|
+
'-undefined',
|
|
42
|
+
'dynamic_lookup',
|
|
43
|
+
'-fobjc-arc',
|
|
44
|
+
'-O2',
|
|
45
|
+
'-mmacosx-version-min=12.0',
|
|
46
|
+
'-arch',
|
|
47
|
+
process.arch === 'arm64' ? 'arm64' : 'x86_64',
|
|
48
|
+
'-framework',
|
|
49
|
+
'AppKit',
|
|
50
|
+
source,
|
|
51
|
+
'-o',
|
|
52
|
+
built
|
|
53
|
+
],
|
|
54
|
+
{ stdio: 'inherit' }
|
|
55
|
+
)
|
|
56
|
+
return true
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(
|
|
59
|
+
`The presentation addon was not compiled (${error.message}): the Dock and the menu bar will auto-hide in full screen. xcode-select --install gives clang`
|
|
60
|
+
)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The app's updates, by Electron's own `autoUpdater` (Squirrel) from a folder of static files, the way of Electron's
|
|
2
|
+
// docs' "Updating Applications" that needs no server and no package: on macOS a `RELEASES.json` beside the zipped app,
|
|
3
|
+
// on Windows Squirrel.Windows' `RELEASES` beside its installer's packages. Squirrel.Mac installs only into an app signed
|
|
4
|
+
// with a real certificate, and Squirrel.Windows only into an app its installer put there (Electron Forge's
|
|
5
|
+
// `maker-squirrel`): the ad hoc signed app of `npm run package` finds nothing, and asks nothing while the address is a
|
|
6
|
+
// placeholder (`config.mjs`). No Electron here: it is handed in, so that the tests run it in Node.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The feed for this system and processor (`setFeedURL`'s options), or nothing where there is none: a placeholder's
|
|
10
|
+
* address (a host under `.invalid`), or a system Squirrel does not update (Linux)
|
|
11
|
+
*/
|
|
12
|
+
export function feedOf(address, platform = process.platform, arch = process.arch) {
|
|
13
|
+
let host
|
|
14
|
+
try {
|
|
15
|
+
host = new URL(address).hostname
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
if (host.endsWith('.invalid') || (platform !== 'darwin' && platform !== 'win32')) return undefined
|
|
20
|
+
const folder = `${address.replace(/\/+$/, '')}/${platform}/${arch}`
|
|
21
|
+
return platform === 'darwin' ? { url: `${folder}/RELEASES.json`, serverType: 'json' } : { url: folder }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the page asks of updates: `check()`, a newer version downloaded and ready (its name and notes), or nothing; and
|
|
26
|
+
* `install()`, which quits and starts the new one. `updater` is Electron's `autoUpdater`, `feed` what `feedOf` gave,
|
|
27
|
+
* `packaged` whether this is a packaged app (`app.isPackaged`: one run from the project has nothing to update)
|
|
28
|
+
*/
|
|
29
|
+
export function updatesOf(updater, feed, packaged) {
|
|
30
|
+
let fed = false
|
|
31
|
+
let asking
|
|
32
|
+
return {
|
|
33
|
+
check() {
|
|
34
|
+
if (!feed || !packaged) return Promise.resolve(undefined)
|
|
35
|
+
if (asking) return asking
|
|
36
|
+
if (!fed) {
|
|
37
|
+
updater.setFeedURL(feed)
|
|
38
|
+
fed = true
|
|
39
|
+
}
|
|
40
|
+
asking = new Promise((resolve, reject) => {
|
|
41
|
+
const done = () => {
|
|
42
|
+
updater.off('update-downloaded', downloaded)
|
|
43
|
+
updater.off('update-not-available', none)
|
|
44
|
+
updater.off('error', failed)
|
|
45
|
+
asking = undefined
|
|
46
|
+
}
|
|
47
|
+
// (Squirrel downloads what it finds before it says so: "downloaded" is the one to wait for)
|
|
48
|
+
const downloaded = (_event, notes, name) => {
|
|
49
|
+
done()
|
|
50
|
+
resolve({ version: name || 'new', ...(notes && { notes: String(notes) }) })
|
|
51
|
+
}
|
|
52
|
+
const none = () => {
|
|
53
|
+
done()
|
|
54
|
+
resolve(undefined)
|
|
55
|
+
}
|
|
56
|
+
const failed = (error) => {
|
|
57
|
+
done()
|
|
58
|
+
reject(error)
|
|
59
|
+
}
|
|
60
|
+
updater.on('update-downloaded', downloaded)
|
|
61
|
+
updater.on('update-not-available', none)
|
|
62
|
+
updater.on('error', failed)
|
|
63
|
+
updater.checkForUpdates()
|
|
64
|
+
})
|
|
65
|
+
return asking
|
|
66
|
+
},
|
|
67
|
+
install() {
|
|
68
|
+
updater.quitAndInstall()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"main": "electron/main.mjs",
|
|
3
|
+
"scripts": {
|
|
4
|
+
"dev": "vite --port 3100 --strictPort",
|
|
5
|
+
"app": "node electron/dev.mjs",
|
|
6
|
+
"app:editor": "node electron/dev.mjs --attach",
|
|
7
|
+
"package": "node electron/package.mjs"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"electron": "45.2.1",
|
|
11
|
+
"happy-dom": "^21.0.3"
|
|
12
|
+
}
|
|
13
|
+
}
|