@driftengine/package 3.61.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.
Files changed (137) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +408 -0
  4. package/bin/drift-package.mjs +82 -0
  5. package/dist/android.d.ts +18 -0
  6. package/dist/android.js +207 -0
  7. package/dist/bootstrap.d.ts +17 -0
  8. package/dist/bootstrap.js +326 -0
  9. package/dist/build.d.ts +11 -0
  10. package/dist/build.js +248 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/flags.d.ts +57 -0
  14. package/dist/flags.js +122 -0
  15. package/dist/identity.d.ts +51 -0
  16. package/dist/identity.js +75 -0
  17. package/dist/index.d.ts +19 -0
  18. package/dist/index.js +24 -0
  19. package/dist/init.d.ts +24 -0
  20. package/dist/init.js +45 -0
  21. package/dist/ios.d.ts +27 -0
  22. package/dist/ios.js +152 -0
  23. package/dist/macCrossSign.d.ts +34 -0
  24. package/dist/macCrossSign.js +298 -0
  25. package/dist/main/consoleForward.d.ts +43 -0
  26. package/dist/main/consoleForward.js +63 -0
  27. package/dist/main/csp.d.ts +4 -0
  28. package/dist/main/csp.js +55 -0
  29. package/dist/main/entry.d.ts +1 -0
  30. package/dist/main/entry.js +56 -0
  31. package/dist/main/ipc.d.ts +28 -0
  32. package/dist/main/ipc.js +284 -0
  33. package/dist/main/links.d.ts +1 -0
  34. package/dist/main/links.js +29 -0
  35. package/dist/main/menu.d.ts +17 -0
  36. package/dist/main/menu.js +49 -0
  37. package/dist/main/serve.d.ts +21 -0
  38. package/dist/main/serve.js +69 -0
  39. package/dist/main/shortcuts.d.ts +28 -0
  40. package/dist/main/shortcuts.js +26 -0
  41. package/dist/main/splash.d.ts +14 -0
  42. package/dist/main/splash.js +41 -0
  43. package/dist/main/splashTiming.d.ts +36 -0
  44. package/dist/main/splashTiming.js +24 -0
  45. package/dist/main/window.d.ts +19 -0
  46. package/dist/main/window.js +297 -0
  47. package/dist/manifest.d.ts +61 -0
  48. package/dist/manifest.js +91 -0
  49. package/dist/mobile/assetCheck.d.ts +1 -0
  50. package/dist/mobile/assetCheck.js +28 -0
  51. package/dist/mobile/bridgeShim.d.ts +83 -0
  52. package/dist/mobile/bridgeShim.js +208 -0
  53. package/dist/mobile/iosShim.d.ts +42 -0
  54. package/dist/mobile/iosShim.js +104 -0
  55. package/dist/mobile/report.d.ts +27 -0
  56. package/dist/mobile/report.js +122 -0
  57. package/dist/mobile/versionCode.d.ts +12 -0
  58. package/dist/mobile/versionCode.js +17 -0
  59. package/dist/outDir.d.ts +30 -0
  60. package/dist/outDir.js +32 -0
  61. package/dist/preload/bridge.d.ts +77 -0
  62. package/dist/preload/bridge.js +2 -0
  63. package/dist/preload/entry.d.ts +1 -0
  64. package/dist/preload/entry.js +77 -0
  65. package/dist/renderer/fileStore.d.ts +23 -0
  66. package/dist/renderer/fileStore.js +33 -0
  67. package/dist/renderer/host.d.ts +45 -0
  68. package/dist/renderer/host.js +30 -0
  69. package/dist/renderer/nativeHost.d.ts +83 -0
  70. package/dist/renderer/nativeHost.js +125 -0
  71. package/dist/resources.d.ts +71 -0
  72. package/dist/resources.js +83 -0
  73. package/dist/runtime.d.ts +38 -0
  74. package/dist/runtime.js +80 -0
  75. package/dist/runtimeVersion.d.ts +10 -0
  76. package/dist/runtimeVersion.js +18 -0
  77. package/dist/signing.d.ts +49 -0
  78. package/dist/signing.js +135 -0
  79. package/dist/stage.d.ts +22 -0
  80. package/dist/stage.js +138 -0
  81. package/dist/steam/cloudStore.d.ts +30 -0
  82. package/dist/steam/cloudStore.js +54 -0
  83. package/dist/steam/depot.d.ts +29 -0
  84. package/dist/steam/depot.js +39 -0
  85. package/dist/steam/main.d.ts +22 -0
  86. package/dist/steam/main.js +41 -0
  87. package/dist/templates/buildWindows.d.ts +16 -0
  88. package/dist/templates/buildWindows.js +161 -0
  89. package/dist/templates/packageWrapper.d.ts +9 -0
  90. package/dist/templates/packageWrapper.js +80 -0
  91. package/dist/toolchain.d.ts +101 -0
  92. package/dist/toolchain.js +186 -0
  93. package/package.json +73 -0
  94. package/src/android.ts +256 -0
  95. package/src/bootstrap.ts +389 -0
  96. package/src/build.ts +285 -0
  97. package/src/cli.ts +367 -0
  98. package/src/flags.ts +171 -0
  99. package/src/identity.ts +87 -0
  100. package/src/index.ts +31 -0
  101. package/src/init.ts +61 -0
  102. package/src/ios.ts +185 -0
  103. package/src/macCrossSign.ts +338 -0
  104. package/src/main/consoleForward.ts +93 -0
  105. package/src/main/csp.ts +57 -0
  106. package/src/main/entry.ts +63 -0
  107. package/src/main/ipc.ts +311 -0
  108. package/src/main/links.ts +29 -0
  109. package/src/main/menu.ts +53 -0
  110. package/src/main/serve.ts +70 -0
  111. package/src/main/shortcuts.ts +55 -0
  112. package/src/main/splash.ts +42 -0
  113. package/src/main/splashTiming.ts +51 -0
  114. package/src/main/window.ts +316 -0
  115. package/src/manifest.ts +157 -0
  116. package/src/mobile/assetCheck.ts +25 -0
  117. package/src/mobile/bridgeShim.ts +292 -0
  118. package/src/mobile/iosShim.ts +148 -0
  119. package/src/mobile/report.ts +121 -0
  120. package/src/mobile/versionCode.ts +17 -0
  121. package/src/outDir.ts +37 -0
  122. package/src/preload/bridge.ts +73 -0
  123. package/src/preload/entry.ts +91 -0
  124. package/src/renderer/fileStore.ts +38 -0
  125. package/src/renderer/host.ts +96 -0
  126. package/src/renderer/nativeHost.ts +146 -0
  127. package/src/resources.ts +150 -0
  128. package/src/runtime.ts +87 -0
  129. package/src/runtimeVersion.ts +16 -0
  130. package/src/signing.ts +206 -0
  131. package/src/stage.ts +180 -0
  132. package/src/steam/cloudStore.ts +73 -0
  133. package/src/steam/depot.ts +69 -0
  134. package/src/steam/main.ts +69 -0
  135. package/src/templates/buildWindows.ts +163 -0
  136. package/src/templates/packageWrapper.ts +82 -0
  137. package/src/toolchain.ts +210 -0
package/dist/flags.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The Electron the Linux WebGPU switch below is written against, and measured on.
3
+ *
4
+ * **Not the version the feature was introduced in, which nobody here has established.** It is the
5
+ * version this repository has run the switch against and watched WebGPU come up: Electron 43.4.1,
6
+ * Chromium 150.0.7871.224, an RX 9070 XT, where `requestAdapter` answers null without the switch
7
+ * and hands back an `amd`/`rdna-4` adapter with it. Reported from outside against Electron 33.4.11,
8
+ * Chromium 130: the feature name is not in that binary at all, and the switch is inert there.
9
+ *
10
+ * So a runtime older than this is *unverified*, not known-broken — and the plan declines to ship an
11
+ * unverified switch rather than shipping one that may do nothing silently.
12
+ */
13
+ export const WEBGPU_INTEROP_ELECTRON = 43;
14
+ /**
15
+ * @param electronMajor The major version of the Electron these switches will be applied to, or
16
+ * `null` where a caller genuinely cannot know. Both callers in this package can: the build reads
17
+ * it out of the installed package, and the main process reads it out of `process.versions`.
18
+ */
19
+ export function planFlags(manifest, target, electronMajor = null) {
20
+ /*
21
+ * **A mobile host has no command line to pass switches on.** The renderer is the platform's own
22
+ * WebView, updated by the store rather than by us, and every decision below is about a Chromium
23
+ * this package ships. Returning nothing is the honest answer rather than switches that would be
24
+ * silently dropped — and it is why the Linux refusal further down cannot fire on a phone.
25
+ */
26
+ if (target === 'android' || target === 'ios')
27
+ return { switches: [], refusal: null, notes: [] };
28
+ const switches = [];
29
+ const notes = [];
30
+ const wantsWebGpu = manifest.backend.webgpu !== 'off';
31
+ const linux = target === 'linux-x64';
32
+ /*
33
+ * **A software rasteriser is never chosen silently.** Plain headless Chromium selects
34
+ * SwiftShader on a development machine, measured, and says nothing about it — every
35
+ * frame it produces is a lie about what a player sees and it saturates the CPU producing it.
36
+ * `requireHardwareGpu()` refuses it in the capture harness; this refuses it in the product.
37
+ */
38
+ if (!manifest.backend.allowSoftwareRenderer) {
39
+ switches.push(['disable-software-rasterizer', '']);
40
+ }
41
+ if (linux && wantsWebGpu) {
42
+ /*
43
+ * **Electron does not enable WebGPU on Linux although Chrome does** (`electron#40929`), so
44
+ * without this there is no WebGPU on that target at all — `requestAdapter` simply answers null
45
+ * and the engine falls back, correctly and invisibly.
46
+ *
47
+ * **The interop feature, and deliberately not `--use-angle=vulkan --enable-features=Vulkan`.**
48
+ * Both yield WebGPU. The second also breaks video encoding under Wayland: measured on an
49
+ * RX 9070 XT, canvas frames encode as all-zero YUV — a green clip — with nothing reporting it,
50
+ * since `VideoEncoder.error` never fires and the muxer finalises a file of the expected size.
51
+ * `ForceEnableWebGpuInterop` composites WebGPU through GL instead of moving ANGLE onto Vulkan,
52
+ * and both survive: measured in a packaged build on this machine, WebGPU drew and a vp8 round
53
+ * trip came back magenta rather than green.
54
+ *
55
+ * **This is why clip export and WebGPU are no longer refused together.** They were, for as
56
+ * long as the only switch known to work was the one that broke encoding.
57
+ *
58
+ * **What would make it wrong** is a Chromium that retires the feature, at which point WebGPU
59
+ * quietly stops being offered on Linux and the acceptance probe reports the fallback — which
60
+ * is the failure mode to watch for rather than a green clip.
61
+ *
62
+ * **Reported from outside as hanging a Wayland session, and it does not do that here.** The
63
+ * report is a packaged game that never paints on AMD under Wayland: an adapter is offered and
64
+ * the renderer's creation never returns. Chromium does print
65
+ * `'--ozone-platform=wayland' is not compatible with Vulkan` on stderr while this switch is
66
+ * applied, on this machine too. But run against the engine's own starter — an RX 9070 XT on a
67
+ * Wayland session under COSMIC, Electron 43.4.1, Chromium 150 — the acquisition completes in
68
+ * **153 ms, three runs of three**, and the example draws with WebGPU. The bare path completes
69
+ * as well: adapter, device, canvas context, swapchain, submit, and a one-pixel readback that
70
+ * comes back magenta.
71
+ *
72
+ * **Chromium's own advice is worse here.** Pairing this with `--ozone-platform=x11`, which is
73
+ * what that message suggests, takes the GPU process down on this machine: `exit_code=139`, and
74
+ * a screenshot request answers `UnknownVizError`. So it is not offered.
75
+ *
76
+ * The switch is therefore kept, because withdrawing it on every Wayland session would take a
77
+ * measured-working backend away from every machine like this one. What the report is right
78
+ * about is that a stall had no floor under it, and that is closed where it belongs: see
79
+ * `BACKEND_TIMEOUT_MS` in the engine's backend selection, which answers WebGL2 rather than
80
+ * waiting for a device that is not coming.
81
+ */
82
+ /*
83
+ * **And it is only asked for on a runtime it has been seen to work on.** The switch was
84
+ * verified once and then travelled, unchecked, into whatever Electron a consumer's install
85
+ * happened to resolve — which is how a build reached a Chromium whose binary does not contain
86
+ * the feature name at all. Nothing reported it, because nothing could: an ignored feature name
87
+ * and an absent adapter produce the same product.
88
+ */
89
+ /*
90
+ * **And there is no second-best route on an older runtime, deliberately.**
91
+ *
92
+ * There is one that works: measured on Electron 33.4.11 against this machine, WebGPU comes up
93
+ * under `--enable-unsafe-webgpu` *and* `--enable-features=Vulkan` together — neither alone
94
+ * produces an adapter, nor does `--use-angle=vulkan` beside them. It is not offered, for two
95
+ * reasons that hold together. This package declares the Electron it is written against, so a
96
+ * build resolving an older one has an install to fix rather than a workaround to adopt; and the
97
+ * route costs video encoding under Wayland, where canvas frames encode as all-zero YUV — a
98
+ * green clip, with `VideoEncoder.error` never firing and the muxer finalising a file of the
99
+ * expected size. Shipping an unsafe flag and that trade, to a configuration this package says
100
+ * it does not support, is a worse answer than saying so.
101
+ */
102
+ if (electronMajor === null || electronMajor >= WEBGPU_INTEROP_ELECTRON) {
103
+ switches.push(['enable-features', 'ForceEnableWebGpuInterop']);
104
+ }
105
+ else if (manifest.backend.webgpu === 'require') {
106
+ return {
107
+ switches,
108
+ notes,
109
+ refusal: `backend.webgpu is "require" and Electron ${electronMajor} cannot deliver it on Linux. ` +
110
+ `This package declares Electron ${WEBGPU_INTEROP_ELECTRON}; an install resolving an older ` +
111
+ 'one is the thing to fix. Otherwise set backend.webgpu to "prefer" or "off" to ship ' +
112
+ 'WebGL2 on this target deliberately.',
113
+ };
114
+ }
115
+ else {
116
+ notes.push(`no WebGPU on Linux: Electron ${electronMajor} predates the interop switch this package is ` +
117
+ `written against (${WEBGPU_INTEROP_ELECTRON}), so this build falls back to WebGL2 there. ` +
118
+ 'Install the Electron this package declares to get it back.');
119
+ }
120
+ }
121
+ return { switches, refusal: null, notes };
122
+ }
@@ -0,0 +1,51 @@
1
+ /** The two fields Node compares to decide whether two paths are the same thing. */
2
+ export interface Identity {
3
+ readonly dev: bigint;
4
+ readonly ino: bigint;
5
+ }
6
+ /**
7
+ * Node's own `areIdentical`, which is what decides whether a copy is refused.
8
+ *
9
+ * The two zeros are excluded there and here: a filesystem that hands out no index answers 0, and
10
+ * Node declines to believe it.
11
+ *
12
+ * **`bigint` and not `number`.** A Windows file index is 64 bits and loses precision as a double
13
+ * exactly in the high half where those values live, so two different indices can arrive equal and
14
+ * two equal ones different — the check would be wrong in both directions on precisely the platform
15
+ * it exists for.
16
+ */
17
+ export declare function sameIdentity(a: Identity, b: Identity): boolean;
18
+ /**
19
+ * Refuse a filesystem that cannot tell a file from a directory, before a build spends its minutes.
20
+ *
21
+ * **The failure this replaces is a sentence about the one thing that is not happening.** `stage.ts`
22
+ * copies `drift.package.json` into the staging directory, and Node's `fs.cp` refuses to copy a
23
+ * thing into itself — deciding that by walking the destination's ancestors and comparing each
24
+ * one's `dev` and `ino` against the source's. On a filesystem that reports those honestly it never
25
+ * fires. On a network share it can, and what comes out is
26
+ *
27
+ * cannot copy …\drift.package.json to a subdirectory of self …\out\win-x64\stage\…
28
+ *
29
+ * ten minutes into a build, about the thing furthest from the cause you could imagine. Reported
30
+ * from a Windows VM building over a mapped drive, where a file and a directory both answered
31
+ * `dev=66313 ino=-112686486700016`: a redirector that gets no file index from the server invents a
32
+ * constant one, and on Windows `dev` is already the volume serial number.
33
+ *
34
+ * **The witnesses are not the directories Node will really compare, and that is fine.**
35
+ * `checkParentPaths` walks up from `out/<target>/stage` and stops at the source's parent, so it
36
+ * compares `stage`, `<target>` and `out` — which on a first build do not exist yet and cannot be
37
+ * stat'ed here. What is being established is not *which* directory collides but that this volume
38
+ * gives a file and a directory the same identity, which is a property of the volume. The project
39
+ * root and `out/` are witnesses as good as any.
40
+ *
41
+ * **The remedy it names is `--out=<dir>`, and that is what changed.** Until that flag existed the
42
+ * only answer was to clone onto a local disk, which for a project with tens of gigabytes of assets
43
+ * is a real cost. Now the output can simply be sent somewhere that tells the truth, and the copy is
44
+ * the second suggestion rather than the first.
45
+ *
46
+ * **`look` is a seam and exists for one reason**: the condition cannot be reproduced on any
47
+ * filesystem this suite can create, so without it the refusal — the whole point of the function —
48
+ * would ship having never been seen to fire. The default is `statSync` and no caller passes
49
+ * anything else.
50
+ */
51
+ export declare function refuseLyingFilesystem(cwd: string, outRoot: string | undefined, look?: (path: string) => Identity): void;
@@ -0,0 +1,75 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ /**
4
+ * Node's own `areIdentical`, which is what decides whether a copy is refused.
5
+ *
6
+ * The two zeros are excluded there and here: a filesystem that hands out no index answers 0, and
7
+ * Node declines to believe it.
8
+ *
9
+ * **`bigint` and not `number`.** A Windows file index is 64 bits and loses precision as a double
10
+ * exactly in the high half where those values live, so two different indices can arrive equal and
11
+ * two equal ones different — the check would be wrong in both directions on precisely the platform
12
+ * it exists for.
13
+ */
14
+ export function sameIdentity(a, b) {
15
+ return a.ino !== 0n && a.dev !== 0n && a.ino === b.ino && a.dev === b.dev;
16
+ }
17
+ /**
18
+ * Refuse a filesystem that cannot tell a file from a directory, before a build spends its minutes.
19
+ *
20
+ * **The failure this replaces is a sentence about the one thing that is not happening.** `stage.ts`
21
+ * copies `drift.package.json` into the staging directory, and Node's `fs.cp` refuses to copy a
22
+ * thing into itself — deciding that by walking the destination's ancestors and comparing each
23
+ * one's `dev` and `ino` against the source's. On a filesystem that reports those honestly it never
24
+ * fires. On a network share it can, and what comes out is
25
+ *
26
+ * cannot copy …\drift.package.json to a subdirectory of self …\out\win-x64\stage\…
27
+ *
28
+ * ten minutes into a build, about the thing furthest from the cause you could imagine. Reported
29
+ * from a Windows VM building over a mapped drive, where a file and a directory both answered
30
+ * `dev=66313 ino=-112686486700016`: a redirector that gets no file index from the server invents a
31
+ * constant one, and on Windows `dev` is already the volume serial number.
32
+ *
33
+ * **The witnesses are not the directories Node will really compare, and that is fine.**
34
+ * `checkParentPaths` walks up from `out/<target>/stage` and stops at the source's parent, so it
35
+ * compares `stage`, `<target>` and `out` — which on a first build do not exist yet and cannot be
36
+ * stat'ed here. What is being established is not *which* directory collides but that this volume
37
+ * gives a file and a directory the same identity, which is a property of the volume. The project
38
+ * root and `out/` are witnesses as good as any.
39
+ *
40
+ * **The remedy it names is `--out=<dir>`, and that is what changed.** Until that flag existed the
41
+ * only answer was to clone onto a local disk, which for a project with tens of gigabytes of assets
42
+ * is a real cost. Now the output can simply be sent somewhere that tells the truth, and the copy is
43
+ * the second suggestion rather than the first.
44
+ *
45
+ * **`look` is a seam and exists for one reason**: the condition cannot be reproduced on any
46
+ * filesystem this suite can create, so without it the refusal — the whole point of the function —
47
+ * would ship having never been seen to fire. The default is `statSync` and no caller passes
48
+ * anything else.
49
+ */
50
+ export function refuseLyingFilesystem(cwd, outRoot, look = (path) => statSync(path, { bigint: true })) {
51
+ /* Already pointed off this volume: whatever it answers about itself no longer decides a copy. */
52
+ if (outRoot !== undefined && !resolve(process.cwd(), outRoot).startsWith(resolve(cwd)))
53
+ return;
54
+ const manifest = join(cwd, 'drift.package.json');
55
+ if (!existsSync(manifest))
56
+ return;
57
+ const its = look(manifest);
58
+ for (const witness of [cwd, join(cwd, 'out')]) {
59
+ if (!existsSync(witness))
60
+ continue;
61
+ const theirs = look(witness);
62
+ if (!sameIdentity(its, theirs))
63
+ continue;
64
+ throw new Error(`${cwd} is on a filesystem that gives a file and a directory the same identity.\n\n` +
65
+ ` ${manifest}\n dev=${its.dev} ino=${its.ino}\n` +
66
+ ` ${witness}\n dev=${theirs.dev} ino=${theirs.ino}\n\n` +
67
+ 'Node uses those two fields to avoid copying something into itself, so the first copy of ' +
68
+ 'the staging step fails with "cannot copy … to a subdirectory of self …" — a message ' +
69
+ 'about the thing furthest from the cause, ten minutes into a build. It is what a network ' +
70
+ 'share does: the redirector invents a constant file index when the server gives it none.\n\n' +
71
+ 'Send the output somewhere that tells the truth:\n\n' +
72
+ ' drift-package build --out=/a/local/disk\n\n' +
73
+ 'Or build from a local copy of the project, if the assets are small enough to move.');
74
+ }
75
+ }
@@ -0,0 +1,19 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * What a game imports from the packager.
4
+ *
5
+ * **The renderer half only.** The main process, the CLI, the builder and the staging step are
6
+ * Node and are reached through the `drift-package` command rather than through this barrel — a
7
+ * game bundling `electron` or `esbuild` into its own payload is the failure this boundary exists
8
+ * to prevent.
9
+ *
10
+ * `createHost` is the one call most consumers need; the classes beside it are for a game that
11
+ * assembles its capabilities itself.
12
+ */
13
+ export { createHost } from './renderer/host.ts';
14
+ export type { Host } from './renderer/host.ts';
15
+ export { FileStore } from './renderer/fileStore.ts';
16
+ export { NativeDisplay, NativeFileDialogs, NativeLifecycle, hostBridge, } from './renderer/nativeHost.ts';
17
+ export { NativePlatformServices } from './renderer/nativeHost.ts';
18
+ export { BRIDGE_KEY } from './preload/bridge.ts';
19
+ export type { DriftHostBridge } from './preload/bridge.ts';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * What a game imports from the packager.
4
+ *
5
+ * **The renderer half only.** The main process, the CLI, the builder and the staging step are
6
+ * Node and are reached through the `drift-package` command rather than through this barrel — a
7
+ * game bundling `electron` or `esbuild` into its own payload is the failure this boundary exists
8
+ * to prevent.
9
+ *
10
+ * `createHost` is the one call most consumers need; the classes beside it are for a game that
11
+ * assembles its capabilities itself.
12
+ */
13
+ export { createHost } from './renderer/host.js';
14
+ export { FileStore } from './renderer/fileStore.js';
15
+ export { NativeDisplay, NativeFileDialogs, NativeLifecycle, hostBridge, } from './renderer/nativeHost.js';
16
+ /*
17
+ * A store's own features, when a store answered.
18
+ *
19
+ * `createHost().services` is how a game reaches these — null in a browser, null where the manifest
20
+ * named no app, and null when the copy was launched outside the store. The SDK itself is not here:
21
+ * it is a native Node module held by the main process, because the renderer has no Node at all.
22
+ */
23
+ export { NativePlatformServices } from './renderer/nativeHost.js';
24
+ export { BRIDGE_KEY } from './preload/bridge.js';
package/dist/init.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { PackageManifest } from './manifest.ts';
2
+ /** A file `init` writes, and where it goes relative to the project. */
3
+ export interface InitFile {
4
+ readonly path: string;
5
+ readonly content: string;
6
+ }
7
+ /**
8
+ * The files a consumer cannot avoid owning, and deliberately only those.
9
+ *
10
+ * **The rule that decides what is here**: if it can be done once inside `drift-package`, it is not
11
+ * allowed to be in a file somebody copies. A Windows build needs a `.ps1` because something has to
12
+ * start Node on that machine and that something cannot live inside a Node program; a project needs
13
+ * a launcher because the engine may be vendored, where `npx drift-package` resolves nothing.
14
+ * Everything else the first three-platform consumer wrote — 421 lines of it — is now `runtime.ts`,
15
+ * `identity.ts`, `outDir.ts` and the deletion at the top of `cli.ts`.
16
+ */
17
+ export declare function initFiles(manifest: PackageManifest): readonly InitFile[];
18
+ /**
19
+ * Write them into a project, refusing to overwrite without `--force`.
20
+ *
21
+ * **The refusal names the file rather than the directory**, because a consumer who has edited one
22
+ * of these has edited a specific one and wants to know which is in the way.
23
+ */
24
+ export declare function init(cwd: string, manifest: PackageManifest, force: boolean): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,45 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { buildWindowsScript } from './templates/buildWindows.js';
5
+ import { packageWrapperScript } from './templates/packageWrapper.js';
6
+ /**
7
+ * The files a consumer cannot avoid owning, and deliberately only those.
8
+ *
9
+ * **The rule that decides what is here**: if it can be done once inside `drift-package`, it is not
10
+ * allowed to be in a file somebody copies. A Windows build needs a `.ps1` because something has to
11
+ * start Node on that machine and that something cannot live inside a Node program; a project needs
12
+ * a launcher because the engine may be vendored, where `npx drift-package` resolves nothing.
13
+ * Everything else the first three-platform consumer wrote — 421 lines of it — is now `runtime.ts`,
14
+ * `identity.ts`, `outDir.ts` and the deletion at the top of `cli.ts`.
15
+ */
16
+ export function initFiles(manifest) {
17
+ return [
18
+ { path: 'scripts/build-windows.ps1', content: buildWindowsScript(manifest) },
19
+ { path: 'scripts/package.mjs', content: packageWrapperScript(manifest) },
20
+ ];
21
+ }
22
+ /**
23
+ * Write them into a project, refusing to overwrite without `--force`.
24
+ *
25
+ * **The refusal names the file rather than the directory**, because a consumer who has edited one
26
+ * of these has edited a specific one and wants to know which is in the way.
27
+ */
28
+ export async function init(cwd, manifest, force) {
29
+ const files = initFiles(manifest);
30
+ if (!force) {
31
+ const existing = files.map((f) => join(cwd, f.path)).filter((p) => existsSync(p));
32
+ if (existing.length > 0) {
33
+ throw new Error(`init would overwrite:\n${existing.map((p) => ` ${p}`).join('\n')}\n\n` +
34
+ 'These are yours once you have edited them. Pass --force to replace them anyway, or ' +
35
+ 'move them aside first and diff.');
36
+ }
37
+ }
38
+ for (const file of files) {
39
+ const full = join(cwd, file.path);
40
+ await mkdir(dirname(full), { recursive: true });
41
+ await writeFile(full, file.content, 'utf8');
42
+ console.log(`ok: ${full}`);
43
+ }
44
+ console.log(`\n node scripts/package.mjs doctor\n node scripts/package.mjs build --target=${manifest.targets[0] ?? 'linux-x64'}\n`);
45
+ }
package/dist/ios.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { PackageManifest } from './manifest.ts';
2
+ import type { ResourceOptions } from './resources.ts';
3
+ /**
4
+ * An iOS application, from the same web build every other target uses.
5
+ *
6
+ * **Unsigned unless a team is named, and that is the useful default.** Apple's free tier signs a
7
+ * build for your own device for seven days at a time and does it through Xcode's own UI rather
8
+ * than through anything a script can drive. So this produces an archive and an unsigned `.ipa` —
9
+ * the shape a sideloading tool re-signs — and takes the signed path only when `APPLE_TEAM_ID` is
10
+ * in the environment, where automatic signing has something to work with.
11
+ *
12
+ * **Written, and not yet run.** No Mac has built this: the Swift, the spec and this file were
13
+ * written against Apple's documented behaviour, and the first person to run it should expect to
14
+ * fix something. What is *not* guessed is the shape of the bridge — that is shared with Android
15
+ * and tested — and the containment rule in the scheme handler, which mirrors the desktop shell's.
16
+ *
17
+ * **One question is still open and it gates the whole tier**: whether WebKit treats a custom
18
+ * scheme as a *secure context*. If it does not, `crossOriginIsolated` is false and the answer is a
19
+ * loopback server instead — `SchemeHandler.swift` is the only file that changes.
20
+ * `drift://localhost/__drift/originProbe.html` answers it in one look on a device. **That URL is
21
+ * the answer as much as the page is**: the host has to be `localhost`, because that is the whole
22
+ * of why the origin is trustworthy, and the shell's own assets are served under `__drift/`
23
+ * rather than from a second host, because a second host would not be `localhost`. Every earlier
24
+ * spelling of this in the documentation named the desktop shell's `drift://shell/` form, which
25
+ * on this platform resolves against the game's bundle root and answers with nothing.
26
+ */
27
+ export declare function buildIos(manifest: PackageManifest, projectDir: string, outDir: string, options?: ResourceOptions): Promise<void>;
package/dist/ios.js ADDED
@@ -0,0 +1,152 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { build as esbuild } from 'esbuild';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { missingReferences } from './mobile/assetCheck.js';
7
+ import { versionCodeFor } from './mobile/versionCode.js';
8
+ import { resourceDir } from './resources.js';
9
+ import { findXcodeGen } from './toolchain.js';
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ /**
12
+ * An iOS application, from the same web build every other target uses.
13
+ *
14
+ * **Unsigned unless a team is named, and that is the useful default.** Apple's free tier signs a
15
+ * build for your own device for seven days at a time and does it through Xcode's own UI rather
16
+ * than through anything a script can drive. So this produces an archive and an unsigned `.ipa` —
17
+ * the shape a sideloading tool re-signs — and takes the signed path only when `APPLE_TEAM_ID` is
18
+ * in the environment, where automatic signing has something to work with.
19
+ *
20
+ * **Written, and not yet run.** No Mac has built this: the Swift, the spec and this file were
21
+ * written against Apple's documented behaviour, and the first person to run it should expect to
22
+ * fix something. What is *not* guessed is the shape of the bridge — that is shared with Android
23
+ * and tested — and the containment rule in the scheme handler, which mirrors the desktop shell's.
24
+ *
25
+ * **One question is still open and it gates the whole tier**: whether WebKit treats a custom
26
+ * scheme as a *secure context*. If it does not, `crossOriginIsolated` is false and the answer is a
27
+ * loopback server instead — `SchemeHandler.swift` is the only file that changes.
28
+ * `drift://localhost/__drift/originProbe.html` answers it in one look on a device. **That URL is
29
+ * the answer as much as the page is**: the host has to be `localhost`, because that is the whole
30
+ * of why the origin is trustworthy, and the shell's own assets are served under `__drift/`
31
+ * rather than from a second host, because a second host would not be `localhost`. Every earlier
32
+ * spelling of this in the documentation named the desktop shell's `drift://shell/` form, which
33
+ * on this platform resolves against the game's bundle root and answers with nothing.
34
+ */
35
+ export async function buildIos(manifest, projectDir, outDir, options = {}) {
36
+ if (process.platform !== 'darwin') {
37
+ throw new Error('ios is built on macOS: Xcode is not available anywhere else');
38
+ }
39
+ const xcodegen = await findXcodeGen();
40
+ if (xcodegen === null) {
41
+ throw new Error('XcodeGen is not here. Run `drift-package bootstrap --target=ios`, which fetches it; ' +
42
+ 'Xcode itself comes from the App Store.');
43
+ }
44
+ const projectRoot = join(outDir, 'project');
45
+ await rm(projectRoot, { recursive: true, force: true });
46
+ await mkdir(projectRoot, { recursive: true });
47
+ await cp(await resourceDir('ios', options.resourceRoot), projectRoot, { recursive: true });
48
+ /* The game keeps its own directory here, unlike Android: `SchemeHandler` resolves against a
49
+ `www` root inside the bundle, so absolute references land where they point. */
50
+ const distDir = resolve(projectDir, dirname(manifest.entry));
51
+ await cp(distDir, join(projectRoot, 'www'), { recursive: true });
52
+ await checkReferences(join(projectRoot, 'www'), basename(manifest.entry));
53
+ /* Served under `drift://localhost/__drift/`, so the origin probe and the mark are reachable
54
+ without a second host — which would not be `localhost`, and so would not be secure. */
55
+ await cp(await resourceDir('assets', options.resourceRoot), join(projectRoot, 'shell'), {
56
+ recursive: true,
57
+ });
58
+ await esbuild({
59
+ stdin: {
60
+ contents: `import { attachIosShim } from ${JSON.stringify(join(HERE, 'mobile', 'iosShim.ts'))};\nattachIosShim('__driftHost');\n`,
61
+ resolveDir: HERE,
62
+ loader: 'ts',
63
+ },
64
+ outfile: join(projectRoot, 'drift-shim.js'),
65
+ bundle: true,
66
+ format: 'iife',
67
+ platform: 'browser',
68
+ target: 'es2022',
69
+ logLevel: 'warning',
70
+ });
71
+ const version = await consumerVersion(projectDir);
72
+ const team = process.env.APPLE_TEAM_ID ?? '';
73
+ const spec = (await readFile(join(projectRoot, 'project.yml'), 'utf8'))
74
+ .replace(/__DRIFT_APP_NAME__/g, manifest.name)
75
+ .replace(/__DRIFT_APP_ID__/g, manifest.id)
76
+ .replace(/__DRIFT_VERSION_NAME__/g, version)
77
+ .replace(/__DRIFT_VERSION_CODE__/g, String(versionCodeFor(version)))
78
+ .replace(/__DRIFT_TEAM_ID__/g, team);
79
+ await writeFile(join(projectRoot, 'project.yml'), spec, 'utf8');
80
+ console.log(`[drift-package] ios: ${team === '' ? 'unsigned — a sideloading tool re-signs it' : `signing with team ${team}`}`);
81
+ await runTool(xcodegen, ['generate', '--spec', 'project.yml'], projectRoot);
82
+ const archive = join(outDir, 'DriftHost.xcarchive');
83
+ await runTool('xcodebuild', [
84
+ '-project',
85
+ 'DriftHost.xcodeproj',
86
+ '-scheme',
87
+ 'DriftHost',
88
+ '-configuration',
89
+ 'Release',
90
+ '-destination',
91
+ 'generic/platform=iOS',
92
+ '-archivePath',
93
+ archive,
94
+ 'archive',
95
+ ...(team === ''
96
+ ? ['CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO', 'CODE_SIGN_IDENTITY=']
97
+ : [`DEVELOPMENT_TEAM=${team}`]),
98
+ ], projectRoot);
99
+ /*
100
+ * An `.ipa` is a zip with the app inside a `Payload` directory, which is all `xcodebuild
101
+ * -exportArchive` produces for a development build — and that step needs a provisioning profile
102
+ * this may not have. Built by hand so an unsigned archive still leaves something installable by
103
+ * a tool that signs it afterwards.
104
+ */
105
+ const payload = join(outDir, 'Payload');
106
+ await rm(payload, { recursive: true, force: true });
107
+ await mkdir(payload, { recursive: true });
108
+ await cp(join(archive, 'Products', 'Applications', `${manifest.name}.app`), join(payload, `${manifest.name}.app`), { recursive: true });
109
+ const ipa = join(outDir, `${manifest.name.replace(/[^\w.-]+/g, '-')}-${version}.ipa`);
110
+ await rm(ipa, { force: true });
111
+ await runTool('zip', ['-qry', ipa, 'Payload'], outDir);
112
+ await rm(payload, { recursive: true, force: true });
113
+ console.log(`[drift-package] ios: ${ipa}`);
114
+ }
115
+ async function checkReferences(root, entryName) {
116
+ const html = await readFile(join(root, entryName), 'utf8');
117
+ const { readdir } = await import('node:fs/promises');
118
+ const { relative } = await import('node:path');
119
+ const walk = async (dir) => {
120
+ const out = [];
121
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
122
+ const full = join(dir, entry.name);
123
+ if (entry.isDirectory())
124
+ out.push(...(await walk(full)));
125
+ else
126
+ out.push(relative(root, full).split('\\').join('/'));
127
+ }
128
+ return out;
129
+ };
130
+ const missing = missingReferences(html, new Set(await walk(root)));
131
+ if (missing.length === 0)
132
+ return;
133
+ throw new Error(`the entry page references ${missing.length} file(s) that are not in the bundle: ` +
134
+ `${missing.slice(0, 5).join(', ')}.`);
135
+ }
136
+ async function consumerVersion(projectDir) {
137
+ try {
138
+ const raw = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'));
139
+ const version = raw.version;
140
+ return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
141
+ }
142
+ catch {
143
+ return '0.0.0';
144
+ }
145
+ }
146
+ async function runTool(command, args, cwd) {
147
+ await new Promise((done, fail) => {
148
+ const child = spawn(command, [...args], { stdio: 'inherit', cwd });
149
+ child.on('error', fail);
150
+ child.on('close', (code) => code === 0 ? done() : fail(new Error(`${command} exited with ${String(code)}`)));
151
+ });
152
+ }
@@ -0,0 +1,34 @@
1
+ export interface CrossSignOptions {
2
+ /** The `.app` directory, which is what rcodesign is pointed at. */
3
+ readonly app: string;
4
+ /** The rcodesign binary, from the cache or from the machine. */
5
+ readonly rcodesign: string;
6
+ /** The slice the artifact is supposed to carry, so a silently wrong one is caught here. */
7
+ readonly arch: 'arm64' | 'x64';
8
+ }
9
+ /**
10
+ * Ad-hoc sign a `.app` in place, then refuse it if the result is not what was asked for.
11
+ *
12
+ * Ad-hoc is the only mode offered: a Developer ID would need a certificate this host cannot use,
13
+ * and `planSigning` refuses that combination before a build gets this far.
14
+ */
15
+ export declare function crossSignMacApp(options: CrossSignOptions): Promise<void>;
16
+ /**
17
+ * The `.zip` a cross-built Mac artifact ships as, archived here rather than by electron-builder.
18
+ *
19
+ * **electron-builder cannot produce this one correctly off a Mac, and says so in its own source**:
20
+ * its archiver picks 7-Zip everywhere except macOS, guarded by a comment reading *"7zip
21
+ * dereferences symlinks, corrupting .framework structure and breaking codesign"*. The guard is
22
+ * written as `process.platform === "darwin"`, so on Linux it takes the 7-Zip path regardless.
23
+ *
24
+ * **A `.framework` is symlinks by construction.** `Electron Framework` points into
25
+ * `Versions/Current`, which points at `Versions/A`. Dereferenced, the archive carries the
26
+ * framework's 192 MB payload twice and no links at all, and macOS rejects that layout as
27
+ * unrecognised — the signature that was correct on disk no longer validates against it. Measured
28
+ * on this repository before this function existed: a 404 MB archive with zero symlink entries,
29
+ * against 353 MB on disk.
30
+ *
31
+ * `zip -y` stores a symlink as a symlink, `-r` recurses, and Unix permission bits are kept, which
32
+ * every executable inside the bundle depends on.
33
+ */
34
+ export declare function zipMacApp(app: string, into: string): Promise<void>;