@makefully/adaptfully 3.1.0 → 3.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 3.3.0 — 2026-06-23
6
+
7
+ ### Added
8
+
9
+ - **Packager classes** — `Packager`, `WebPackager`, `ElectronPackager`, `CordovaPackager`, and `CapacitorPackager` encapsulate per-packager prebuild behavior behind a shared API (`validate`, `applyTemplates`, `applyHtmlExtras`, `prebuild`).
10
+ - **`createPackagerForPlatform(platformKey, pkg, options)`** — factory that resolves the configured packager and returns an instance. Options include `platforms` (target platform keys, e.g. `ios` + `android` for Cordova) and `platformKey` (active prebuild platform).
11
+ - **Plugin detection** — `collectUsedPlugins()` and `usesPlugin(id)` inspect standard Adaptfully auth/storage registrations across targeted platforms so packagers can adapt output (e.g. Electron injects steamworks init when `steam-auth` is registered).
12
+ - **Cordova prebuild** — writes `cordova.js` stub, injects CSP/viewport meta tags and `game-config.js` script tags into HTML.
13
+ - **`buildElectronMain()` / `buildElectronPreload()`** — exported helpers that compose Electron shell files (used by `ElectronPackager` and available for tests or extensions).
14
+
15
+ ### Changed
16
+
17
+ - Prebuild runs packager work through a single `packager.prebuild(dest, htmlPaths)` call instead of separate template/HTML helper functions.
18
+ - Electron **`main.js`** and **`preload.js`** are composed from embedded strings in `ElectronPackager` rather than read from disk templates.
19
+ - **`web`** packager writes **`game-config.js`** during prebuild when the platform key is **`uwp`**.
20
+
21
+ ### Removed
22
+
23
+ - **`lib/templates/`** — packager output is generated in code; no template files are shipped with the package.
24
+ - **`getTemplatesDir()`**, **`resolvePackagerTemplateDir()`**, and **`applyTemplateMarker()`** from the public API.
25
+
26
+ ## 3.2.0 — 2026-06-16
27
+
28
+ ### Added
29
+
30
+ - **`config.platforms.<platform>.packager`** — `web` (default), `electron`, `cordova`, or `capacitor`. Controls which template files are added during prebuild.
31
+ - **`lib/templates/`** — packager-specific templates (starting with `electron/main.js` and `electron/preload.js`).
32
+ - Electron prebuild writes **`main.js`** automatically. When **`steam-auth`** is registered, Adaptfully also writes **`preload.js`** with steamworks.js init and overlay setup in `main.js`.
33
+ - **`steam-auth` requires `packager: "electron"`** — prebuild throws if steam-auth is used on a non-electron platform or without `config.steamId`.
34
+
35
+ ## 3.1.1 — 2026-06-16
36
+
37
+ ### Added
38
+
39
+ - **steam-auth** integrates with [steamworks.js](https://github.com/ceifa/steamworks.js): reads Steam ID and persona name from shell-provided bridges (`__ADAPTFULLY_STEAMWORKS__`, `steamworks`, `electronAPI`).
40
+ - Steam auth supports `autoLogin()` and `supportsAutoLogin()` when the Steam client is available.
41
+
5
42
  ## 3.1.0 — 2026-06-18
6
43
 
7
44
  ### Added
package/README.md CHANGED
@@ -65,7 +65,7 @@ storage.getObject('currentGame');
65
65
  | Plugin key | Registration | Runtime |
66
66
  |------------|--------------|---------|
67
67
  | `google-auth` | `adaptfully.register('auth', adaptfully.auth.Google)` | Web, Android, iOS |
68
- | `steam-auth` | `adaptfully.register('auth', adaptfully.auth.Steam)` | Steam / Electron |
68
+ | `steam-auth` | `adaptfully.register('auth', adaptfully.auth.Steam)` | Steam / Electron (via [steamworks.js](https://github.com/ceifa/steamworks.js)) |
69
69
  | `dev-auth` | `adaptfully.register('auth', adaptfully.auth.Dev)` | Local dev (test user) |
70
70
 
71
71
  Use plugin keys in `config.platforms.<platform>.registrations`. Custom deploy scripts use a path relative to the deploy folder instead:
@@ -101,11 +101,55 @@ Standard plugin keys load bundled Adaptfully runtime scripts and emit an inline
101
101
 
102
102
  Wrapfully builders (`steam`, `win`, `mac`, `android`, etc.) map to platform keys via defaults (`win` → `steam`) or an explicit `builders` array on the platform config.
103
103
 
104
+ Each platform entry may set a **`packager`** (`web`, `electron`, `cordova`, or `capacitor`). Defaults to **`web`** — copy deploy and inject HTML only. Packagers are implemented as classes (`WebPackager`, `ElectronPackager`, `CordovaPackager`, `CapacitorPackager`) that handle prebuild output for their target platforms.
105
+
106
+ | Packager | Prebuild adds |
107
+ |----------|---------------|
108
+ | `web` | `game-config.js` for **`uwp`** platform prebuilds |
109
+ | `electron` | `main.js` (Electron shell); `preload.js` when **`steam-auth`** is registered |
110
+ | `cordova` | `cordova.js` stub, `game-config.js`, CSP/viewport HTML extras |
111
+ | `capacitor` | `game-config.js` (more Capacitor-specific output planned) |
112
+
113
+ **`steam-auth` requires `packager: "electron"`** and `config.steamId`.
114
+
115
+ ```json
116
+ {
117
+ "config": {
118
+ "steamId": 719140,
119
+ "platforms": {
120
+ "web": {
121
+ "packager": "web",
122
+ "registrations": { "auth": "google-auth" }
123
+ },
124
+ "steam": {
125
+ "packager": "electron",
126
+ "registrations": { "auth": "steam-auth" }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ #### Steam auth (`steam-auth`)
134
+
135
+ When `steam-auth` is registered on an **`electron`** platform, Adaptfully prebuild writes:
136
+
137
+ - **`main.js`** — Electron shell with Steam overlay enabled and a preload script wired in
138
+ - **`preload.js`** — initializes [steamworks.js](https://github.com/ceifa/steamworks.js) with `config.steamId` and exposes `window.__ADAPTFULLY_STEAMWORKS__`
139
+
140
+ The renderer `steam-auth` plugin reads the Steam ID from that bridge. When Steam is available, `autoLogin()` succeeds immediately with `{ id: steamId64, email: '' }`. Optional config keys:
141
+
142
+ | Key | Default | Purpose |
143
+ |-----|---------|---------|
144
+ | `autoLoginStorageKey` | `lastLoggedIn` | Storage key written with the Steam ID on login |
145
+ | `steamReadyTimeoutMs` | `10000` | Max wait when a bridge exists but identity is not yet ready |
146
+
104
147
  ### Node API
105
148
 
106
149
  ```javascript
107
150
  import {
108
151
  prebuildPlatform,
152
+ createPackagerForPlatform,
109
153
  resolveHtmlInjections,
110
154
  runAdaptfullyStage,
111
155
  buildAdaptfullyInjection,
@@ -121,6 +165,7 @@ import {
121
165
  ```
122
166
 
123
167
  - **`prebuildPlatform(deployFolder, platformKey, pkg)`** — copy `deploy/` to `output/<platform>-prebuild/` and inject registrations into `config.htmlInjections` (default: `index.html`).
168
+ - **`createPackagerForPlatform(platformKey, pkg, { platforms, log })`** — get a packager instance for custom prebuild or future build/deploy integration. The instance exposes `usesPlugin('steam-auth')`, `collectUsedPlugins()`, and `prebuild(dest, htmlPaths)`.
124
169
  - **`resolveRegistrationAssets(registrations)`** — resolve runtime script paths, inline registration JS, and external script tags for a registration map (useful for Vite dev servers).
125
170
  - **`runAdaptfullyStage('prebuild' | 'build' | 'deploy', platformKey, options)`** — run a pipeline stage programmatically.
126
171
  - **`platform.autoLogin(callback)`** — restore a prior session without UI when the auth plugin supports it (Google uses `lastLoggedIn` in storage and a cached OAuth token).
@@ -306,6 +351,7 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
306
351
  }
307
352
  },
308
353
  "steam": {
354
+ "packager": "electron",
309
355
  "registrations": {
310
356
  "auth": "steam-auth",
311
357
  "storage": "javascript/adaptfully-bridge.js"
@@ -338,6 +384,7 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
338
384
  | `platforms` | Prebuild | Per-platform registration maps (see [Adaptfully runtime](#adaptfully-runtime)) |
339
385
  | `platforms.<name>.builder` | Build/deploy | Override Wrapfully builder for a platform (default: `web` → `webapp`, others match platform key) |
340
386
  | `platforms.<name>.builders` | wrapfully-deploy | Map additional Wrapfully builder names to a platform |
387
+ | `platforms.<name>.packager` | Prebuild | `web` (default), `electron`, `cordova`, or `capacitor` — selects the packager class that adds platform-specific files during prebuild |
341
388
  | `properties` | Cordova | Cordova config.xml entries (plugins, allow-navigation, etc.) |
342
389
 
343
390
  ### `wrapfully.json`
package/lib/node/index.js CHANGED
@@ -4,6 +4,22 @@ export { send } from './deploy.js';
4
4
  export { adaptfullyFromCli, runAdaptfullyStage } from './pipeline.js';
5
5
  export { prebuildPlatform, prebuildOutputDir, resolveHtmlInjections } from './prebuild.js';
6
6
  export { getPackageRoot, getRuntimeDir, resolveRuntimeScript } from './paths.js';
7
+ export {
8
+ CapacitorPackager,
9
+ CordovaPackager,
10
+ ElectronPackager,
11
+ Packager,
12
+ VALID_PACKAGERS,
13
+ WebPackager,
14
+ applyPackagerHtmlExtras,
15
+ applyPackagerTemplates,
16
+ buildElectronMain,
17
+ buildElectronPreload,
18
+ createPackagerForPlatform,
19
+ resolvePlatformPackager,
20
+ usesSteamAuth,
21
+ validatePlatformPackager,
22
+ } from './packagers.js';
7
23
  export {
8
24
  STANDARD_PLUGINS,
9
25
  DEFAULT_BUILDER_PLATFORMS,
@@ -0,0 +1,303 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { STANDARD_PLUGINS } from '../registrations.js';
4
+
5
+ /** @typedef {import('../registrations.js').RegistrationMap} RegistrationMap */
6
+ /** @typedef {'web' | 'electron' | 'cordova' | 'capacitor'} PackagerName */
7
+
8
+ /** @type {PackagerName[]} */
9
+ export const VALID_PACKAGERS = ['web', 'electron', 'cordova', 'capacitor'];
10
+
11
+ const PACKAGER_MARKER = '<!-- adaptfully-packager -->';
12
+ const PACKAGER_END_MARKER = '<!-- /adaptfully-packager -->';
13
+
14
+ /**
15
+ * @typedef {Object} PackagerOptions
16
+ * @property {string[]} [platforms] Platform keys this packager targets (e.g. ios + android for Cordova)
17
+ * @property {string} [platformKey] Active platform for a single prebuild run
18
+ * @property {(message: string) => void} [log]
19
+ */
20
+
21
+ /**
22
+ * @param {string} value
23
+ */
24
+ function isDeployPath(value) {
25
+ return value.startsWith('/') || value.startsWith('./') || value.includes('/');
26
+ }
27
+
28
+ /**
29
+ * @param {string} platformKey
30
+ */
31
+ function gameConfigPlatform(platformKey) {
32
+ if (platformKey === 'uwp') {
33
+ return 'ms';
34
+ }
35
+ return platformKey;
36
+ }
37
+
38
+ /**
39
+ * @param {string} dest
40
+ * @param {string} platformKey
41
+ * @param {{ name: string, version: string, config?: { title?: string } }} pkg
42
+ * @param {(message: string) => void} log
43
+ */
44
+ function writeGameConfig(dest, platformKey, pkg, log) {
45
+ const content = `window.gameConfig = ${JSON.stringify({
46
+ title: pkg.config?.title,
47
+ version: pkg.version,
48
+ id: pkg.name,
49
+ platform: gameConfigPlatform(platformKey),
50
+ }, null, 4)};`;
51
+
52
+ fs.writeFileSync(path.join(dest, 'game-config.js'), content);
53
+ log('adaptfully: write game-config.js');
54
+ }
55
+
56
+ /**
57
+ * @param {string} html
58
+ * @param {string} injection
59
+ */
60
+ function injectPackagerExtras(html, injection) {
61
+ if (!injection) {
62
+ return html;
63
+ }
64
+
65
+ const markerPattern = new RegExp(
66
+ `${escapeRegExp(PACKAGER_MARKER)}[\\s\\S]*?${escapeRegExp(PACKAGER_END_MARKER)}\\n?`,
67
+ );
68
+
69
+ if (markerPattern.test(html)) {
70
+ return html.replace(markerPattern, injection);
71
+ }
72
+
73
+ if (html.includes(PACKAGER_MARKER)) {
74
+ return html.replace(PACKAGER_MARKER, `${injection}${PACKAGER_MARKER}`);
75
+ }
76
+
77
+ if (html.includes('<!-- adaptfully -->')) {
78
+ return html.replace('<!-- adaptfully -->', `${injection}<!-- adaptfully -->`);
79
+ }
80
+
81
+ if (html.includes('<!-- scripts -->')) {
82
+ return html.replace('<!-- scripts -->', `${injection}<!-- scripts -->`);
83
+ }
84
+
85
+ if (html.includes('</head>')) {
86
+ return html.replace('</head>', `${injection}</head>`);
87
+ }
88
+
89
+ throw new Error(
90
+ 'Cannot inject packager extras: HTML needs '
91
+ + '<!-- adaptfully-packager -->…<!-- /adaptfully-packager -->, '
92
+ + '<!-- adaptfully -->, <!-- scripts -->, or </head>',
93
+ );
94
+ }
95
+
96
+ function escapeRegExp(value) {
97
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
98
+ }
99
+
100
+ export class Packager {
101
+ /** @type {PackagerName} */
102
+ static id = 'web';
103
+
104
+ /** @type {string[]} */
105
+ static defaultPlatforms = [];
106
+
107
+ /**
108
+ * @param {object} pkg
109
+ * @param {PackagerOptions} [options]
110
+ */
111
+ constructor(pkg, options = {}) {
112
+ this.pkg = pkg;
113
+ this.log = options.log ?? (() => {});
114
+ this.platformKey = options.platformKey ?? null;
115
+
116
+ const configured = Packager.resolveConfiguredPlatforms(this.constructor.id, pkg);
117
+ if (options.platforms?.length) {
118
+ this.platforms = [...options.platforms];
119
+ } else if (configured.length) {
120
+ this.platforms = configured;
121
+ } else {
122
+ this.platforms = [...this.constructor.defaultPlatforms];
123
+ }
124
+
125
+ if (this.platformKey && !this.platforms.includes(this.platformKey)) {
126
+ this.platforms.push(this.platformKey);
127
+ }
128
+ }
129
+
130
+ /** @returns {PackagerName} */
131
+ get name() {
132
+ return /** @type {PackagerName} */ (this.constructor.id);
133
+ }
134
+
135
+ /**
136
+ * @param {PackagerName} packagerId
137
+ * @param {{ config?: { platforms?: Record<string, { packager?: string }> } }} pkg
138
+ * @returns {string[]}
139
+ */
140
+ static resolveConfiguredPlatforms(packagerId, pkg) {
141
+ const platforms = pkg.config?.platforms ?? {};
142
+ return Object.entries(platforms)
143
+ .filter(([, config]) => (config?.packager ?? 'web') === packagerId)
144
+ .map(([key]) => key);
145
+ }
146
+
147
+ /**
148
+ * @param {string} platformKey
149
+ * @param {{ config?: { platforms?: Record<string, { packager?: string }> } }} pkg
150
+ * @returns {PackagerName}
151
+ */
152
+ static resolvePlatformPackager(platformKey, pkg) {
153
+ const platform = pkg.config?.platforms?.[platformKey];
154
+ const packager = platform?.packager ?? 'web';
155
+ return /** @type {PackagerName} */ (packager);
156
+ }
157
+
158
+ /**
159
+ * Merged registrations from every platform in {@link this.platforms}.
160
+ * @returns {RegistrationMap}
161
+ */
162
+ collectRegistrations() {
163
+ /** @type {RegistrationMap} */
164
+ const merged = {};
165
+ for (const key of this.platforms) {
166
+ const registrations = this.pkg.config?.platforms?.[key]?.registrations ?? {};
167
+ Object.assign(merged, registrations);
168
+ }
169
+ return merged;
170
+ }
171
+
172
+ /**
173
+ * Registrations for the active prebuild platform, or merged when none is set.
174
+ * @returns {RegistrationMap}
175
+ */
176
+ getActiveRegistrations() {
177
+ if (this.platformKey) {
178
+ return this.pkg.config?.platforms?.[this.platformKey]?.registrations ?? {};
179
+ }
180
+ return this.collectRegistrations();
181
+ }
182
+
183
+ /**
184
+ * Standard Adaptfully plugin ids (auth, storage, etc.) used across targeted platforms.
185
+ * Custom deploy paths are excluded.
186
+ * @returns {Set<string>}
187
+ */
188
+ collectUsedPlugins() {
189
+ const plugins = new Set();
190
+ for (const key of this.platforms) {
191
+ const registrations = this.pkg.config?.platforms?.[key]?.registrations ?? {};
192
+ for (const value of Object.values(registrations)) {
193
+ if (typeof value === 'string' && !isDeployPath(value) && STANDARD_PLUGINS[value]) {
194
+ plugins.add(value);
195
+ }
196
+ }
197
+ }
198
+ return plugins;
199
+ }
200
+
201
+ /**
202
+ * @param {string} pluginId Standard plugin id (e.g. steam-auth, dev-auth, localStorage)
203
+ * @param {{ scope?: 'active' | 'all' }} [options]
204
+ */
205
+ usesPlugin(pluginId, options = {}) {
206
+ const registrations = options.scope === 'all'
207
+ ? this.collectRegistrations()
208
+ : this.getActiveRegistrations();
209
+ return Object.values(registrations).includes(pluginId);
210
+ }
211
+
212
+ validate() {
213
+ if (!VALID_PACKAGERS.includes(this.name)) {
214
+ throw new Error(
215
+ `Invalid packager "${this.name}". Expected one of: ${VALID_PACKAGERS.join(', ')}`,
216
+ );
217
+ }
218
+
219
+ if (this.usesPlugin('steam-auth', { scope: 'all' }) && this.name !== 'electron') {
220
+ const platformLabel = this.platformKey ?? this.platforms[0] ?? 'unknown';
221
+ throw new Error(
222
+ `Platform "${platformLabel}" uses steam-auth but packager is "${this.name}". `
223
+ + `Set config.platforms.${platformLabel}.packager to "electron".`,
224
+ );
225
+ }
226
+ }
227
+
228
+ needsGameConfig() {
229
+ return false;
230
+ }
231
+
232
+ /**
233
+ * @param {string[]} headExtras
234
+ * @param {string[]} bodyScripts
235
+ * @returns {string}
236
+ */
237
+ formatHtmlInjection(headExtras, bodyScripts) {
238
+ if (headExtras.length === 0 && bodyScripts.length === 0) {
239
+ return '';
240
+ }
241
+
242
+ let block = `${PACKAGER_MARKER}\n`;
243
+ for (const extra of headExtras) {
244
+ block += `${extra}\n`;
245
+ }
246
+ for (const script of bodyScripts) {
247
+ block += `${script}\n`;
248
+ }
249
+ block += `${PACKAGER_END_MARKER}\n`;
250
+ return block;
251
+ }
252
+
253
+ /** @param {string} dest */
254
+ applyTemplates(dest) {
255
+ if (this.needsGameConfig() && this.platformKey) {
256
+ writeGameConfig(dest, this.platformKey, this.pkg, this.log);
257
+ }
258
+ }
259
+
260
+ /** @returns {string} */
261
+ buildHtmlInjection() {
262
+ const bodyScripts = [];
263
+ if (this.needsGameConfig()) {
264
+ bodyScripts.push('<script src="game-config.js"></script>');
265
+ }
266
+ return this.formatHtmlInjection([], bodyScripts);
267
+ }
268
+
269
+ /**
270
+ * @param {string} dest
271
+ * @param {string[]} htmlPaths
272
+ */
273
+ applyHtmlExtras(dest, htmlPaths) {
274
+ const injection = this.buildHtmlInjection();
275
+ if (!injection) {
276
+ return;
277
+ }
278
+
279
+ for (const relativePath of htmlPaths) {
280
+ const htmlPath = path.join(dest, relativePath);
281
+ if (!fs.existsSync(htmlPath)) {
282
+ throw new Error(`htmlInjections file not found in deploy output: ${relativePath}`);
283
+ }
284
+
285
+ const html = fs.readFileSync(htmlPath, 'utf8');
286
+ const updated = injectPackagerExtras(html, injection);
287
+ if (updated !== html) {
288
+ this.log(`adaptfully: inject packager extras into ${relativePath}`);
289
+ fs.writeFileSync(htmlPath, updated);
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * @param {string} dest
296
+ * @param {string[]} htmlPaths
297
+ */
298
+ prebuild(dest, htmlPaths) {
299
+ this.validate();
300
+ this.applyTemplates(dest);
301
+ this.applyHtmlExtras(dest, htmlPaths);
302
+ }
303
+ }
@@ -0,0 +1,13 @@
1
+ import { Packager } from './base.js';
2
+
3
+ export class CapacitorPackager extends Packager {
4
+ /** @type {'capacitor'} */
5
+ static id = 'capacitor';
6
+
7
+ /** @type {string[]} */
8
+ static defaultPlatforms = ['ios', 'android'];
9
+
10
+ needsGameConfig() {
11
+ return true;
12
+ }
13
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Packager } from './base.js';
4
+
5
+ const CORDOVA_CSP = '<meta http-equiv="Content-Security-Policy" content="default-src * \'self\' data: gap: \'unsafe-inline\' \'unsafe-eval\'; style-src * \'self\' \'unsafe-inline\' \'unsafe-eval\' gap:; script-src * \'self\' \'unsafe-inline\' \'unsafe-eval\' gap:; frame-src *;" />';
6
+
7
+ const CORDOVA_VIEWPORT = '<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />';
8
+
9
+ const CORDOVA_STUB = `/**
10
+ * Stub for non-Cordova environments. Cordova replaces this at runtime on device builds.
11
+ */
12
+ window.cordova = null;
13
+ `;
14
+
15
+ export class CordovaPackager extends Packager {
16
+ /** @type {'cordova'} */
17
+ static id = 'cordova';
18
+
19
+ /** @type {string[]} */
20
+ static defaultPlatforms = ['ios', 'android'];
21
+
22
+ needsGameConfig() {
23
+ return true;
24
+ }
25
+
26
+ /** @param {string} dest */
27
+ applyTemplates(dest) {
28
+ fs.writeFileSync(path.join(dest, 'cordova.js'), CORDOVA_STUB);
29
+ this.log('adaptfully: write cordova.js');
30
+
31
+ super.applyTemplates(dest);
32
+ }
33
+
34
+ buildHtmlInjection() {
35
+ const headExtras = [CORDOVA_CSP, CORDOVA_VIEWPORT];
36
+ const bodyScripts = ['<script src="cordova.js"></script>'];
37
+
38
+ if (this.needsGameConfig()) {
39
+ bodyScripts.push('<script src="game-config.js"></script>');
40
+ }
41
+
42
+ return this.formatHtmlInjection(headExtras, bodyScripts);
43
+ }
44
+ }
@@ -0,0 +1,214 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Packager } from './base.js';
4
+
5
+ const STEAM_INIT = `const path = require('path');
6
+
7
+ try {
8
+ require('steamworks.js').electronEnableSteamOverlay();
9
+ } catch (err) {
10
+ console.error('Steam overlay unavailable:', err);
11
+ }
12
+ `;
13
+
14
+ const STEAM_PRELOAD = `,
15
+ preload: path.join(__dirname, 'preload.js')`;
16
+
17
+ const ELECTRON_MAIN_HEAD = `/* eslint-disable no-sync */
18
+ /* global process, require */
19
+ const
20
+ {app, shell, BrowserWindow, Menu} = require('electron'),
21
+ fs = require('fs');
22
+
23
+ `;
24
+
25
+ const ELECTRON_MAIN_BODY = `
26
+ /**
27
+ * Opens external URLs in the system browser and blocks in-app navigation.
28
+ * @param {Electron.BrowserWindow} win
29
+ */
30
+ function attachExternalLinkHandlers(win) {
31
+ win.webContents.setWindowOpenHandler(({url}) => {
32
+ shell.openExternal(url);
33
+ return {action: 'deny'};
34
+ });
35
+
36
+ win.webContents.on('will-navigate', (event, url) => {
37
+ event.preventDefault();
38
+ shell.openExternal(url);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Creates window.
44
+ */
45
+ function createWindow () {
46
+ // Create the browser window.
47
+ const
48
+ debugMode = false,
49
+ lastState = (function () {
50
+ let state = null;
51
+
52
+ try {
53
+ state = JSON.parse(fs.readFileSync(app.getPath('userData') + '/window-state.json', 'utf8'));
54
+ } catch (e) {}
55
+
56
+ return state || {
57
+ bounds: {
58
+ width: 800,
59
+ height: 600
60
+ },
61
+ maximized: false,
62
+ fullscreen: false
63
+ };
64
+ })(),
65
+ win = new BrowserWindow({
66
+ backgroundColor: '#E4378E',
67
+ width: lastState.bounds.width,
68
+ height: lastState.bounds.height,
69
+ minWidth: 200,
70
+ minHeight: 100,
71
+ webPreferences: {
72
+ devTools: debugMode,
73
+ nodeIntegration: false,
74
+ contextIsolation: true,
75
+ sandbox: true`;
76
+
77
+ const ELECTRON_MAIN_TAIL = `
78
+ },
79
+ show: false
80
+ });
81
+
82
+ if (lastState.maximized) {
83
+ win.maximize();
84
+ }
85
+ if (lastState.fullscreen) {
86
+ win.setFullScreen(true);
87
+ }
88
+
89
+ if (!debugMode) {
90
+ Menu.setApplicationMenu(null);
91
+ }
92
+
93
+ win.once('ready-to-show', () => { // helps prevent flicker-loading
94
+ win.show();
95
+ });
96
+
97
+ // and load the index.html of the app.
98
+ win.loadFile('index.html');
99
+
100
+ attachExternalLinkHandlers(win);
101
+
102
+ //win.webContents.openDevTools();
103
+
104
+ win.on('close', () => {
105
+ fs.writeFileSync(app.getPath('userData') + '/window-state.json', JSON.stringify({
106
+ bounds: win.getNormalBounds(),
107
+ maximized: win.isMaximized(),
108
+ fullscreen: win.isFullScreen()
109
+ }));
110
+ });
111
+ }
112
+
113
+ // This method will be called when Electron has finished
114
+ // initialization and is ready to create browser windows.
115
+ // Some APIs can only be used after this event occurs.
116
+ app.whenReady().then(createWindow);
117
+
118
+ // Quit when all windows are closed.
119
+ app.on('window-all-closed', () => {
120
+ // On macOS it is common for applications and their menu bar
121
+ // to stay active until the user quits explicitly with Cmd + Q
122
+ if (process.platform !== 'darwin') {
123
+ app.quit();
124
+ }
125
+ });
126
+
127
+ app.on('activate', () => {
128
+ // On macOS it's common to re-create a window in the app when the
129
+ // dock icon is clicked and there are no other windows open.
130
+ if (BrowserWindow.getAllWindows().length === 0) {
131
+ createWindow();
132
+ }
133
+ });
134
+ `;
135
+
136
+ const ELECTRON_PRELOAD_HEAD = `/* global require */
137
+ const { contextBridge } = require('electron');
138
+
139
+ let steamClient = null;
140
+
141
+ try {
142
+ steamClient = require('steamworks.js').init(`;
143
+
144
+ const ELECTRON_PRELOAD_TAIL = `);
145
+ } catch (err) {
146
+ console.error('Steamworks init failed:', err);
147
+ }
148
+
149
+ if (steamClient) {
150
+ contextBridge.exposeInMainWorld('__ADAPTFULLY_STEAMWORKS__', steamClient);
151
+ }
152
+ `;
153
+
154
+ /**
155
+ * @param {boolean} withSteam
156
+ */
157
+ export function buildElectronMain(withSteam) {
158
+ const steamInit = withSteam ? STEAM_INIT : '';
159
+ const steamPreload = withSteam ? STEAM_PRELOAD : '';
160
+ return ELECTRON_MAIN_HEAD + steamInit + ELECTRON_MAIN_BODY + steamPreload + ELECTRON_MAIN_TAIL;
161
+ }
162
+
163
+ /**
164
+ * @param {number | string} steamAppId
165
+ */
166
+ export function buildElectronPreload(steamAppId) {
167
+ return ELECTRON_PRELOAD_HEAD + steamAppId + ELECTRON_PRELOAD_TAIL;
168
+ }
169
+
170
+ export class ElectronPackager extends Packager {
171
+ /** @type {'electron'} */
172
+ static id = 'electron';
173
+
174
+ /** @type {string[]} */
175
+ static defaultPlatforms = ['steam'];
176
+
177
+ validate() {
178
+ super.validate();
179
+
180
+ if (this.usesPlugin('steam-auth') && !this.pkg.config?.steamId) {
181
+ const platformLabel = this.platformKey ?? this.platforms.join(', ');
182
+ throw new Error(
183
+ `Platform "${platformLabel}" uses steam-auth but config.steamId is not set.`,
184
+ );
185
+ }
186
+ }
187
+
188
+ needsGameConfig() {
189
+ return true;
190
+ }
191
+
192
+ /** @param {string} dest */
193
+ applyTemplates(dest) {
194
+ this.writeElectronMain(dest);
195
+ super.applyTemplates(dest);
196
+ }
197
+
198
+ /** @param {string} dest */
199
+ writeElectronMain(dest) {
200
+ const withSteam = this.usesPlugin('steam-auth');
201
+ const mainContent = buildElectronMain(withSteam);
202
+
203
+ if (withSteam) {
204
+ fs.writeFileSync(
205
+ path.join(dest, 'preload.js'),
206
+ buildElectronPreload(this.pkg.config.steamId),
207
+ );
208
+ this.log('adaptfully: write preload.js (steam-auth)');
209
+ }
210
+
211
+ fs.writeFileSync(path.join(dest, 'main.js'), mainContent);
212
+ this.log('adaptfully: write main.js (electron)');
213
+ }
214
+ }
@@ -0,0 +1,107 @@
1
+ import { CapacitorPackager } from './capacitor.js';
2
+ import { CordovaPackager } from './cordova.js';
3
+ import { ElectronPackager, buildElectronMain, buildElectronPreload } from './electron.js';
4
+ import { Packager, VALID_PACKAGERS } from './base.js';
5
+ import { WebPackager } from './web.js';
6
+
7
+ export { Packager, VALID_PACKAGERS } from './base.js';
8
+ export { WebPackager } from './web.js';
9
+ export { ElectronPackager, buildElectronMain, buildElectronPreload } from './electron.js';
10
+ export { CordovaPackager } from './cordova.js';
11
+ export { CapacitorPackager } from './capacitor.js';
12
+
13
+ /** @typedef {import('./base.js').PackagerName} PackagerName */
14
+
15
+ /** @type {Record<PackagerName, typeof Packager>} */
16
+ const PACKAGER_REGISTRY = {
17
+ web: WebPackager,
18
+ electron: ElectronPackager,
19
+ cordova: CordovaPackager,
20
+ capacitor: CapacitorPackager,
21
+ };
22
+
23
+ /**
24
+ * @param {string} platformKey
25
+ * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string> }>, steamId?: number } }} pkg
26
+ * @param {{ log?: (message: string) => void, platforms?: string[] }} [options]
27
+ * @returns {Packager}
28
+ */
29
+ export function createPackagerForPlatform(platformKey, pkg, options = {}) {
30
+ const packagerId = Packager.resolvePlatformPackager(platformKey, pkg);
31
+
32
+ if (!VALID_PACKAGERS.includes(packagerId)) {
33
+ throw new Error(
34
+ `Invalid packager "${packagerId}" for platform "${platformKey}". `
35
+ + `Expected one of: ${VALID_PACKAGERS.join(', ')}`,
36
+ );
37
+ }
38
+
39
+ const PackagerClass = PACKAGER_REGISTRY[packagerId];
40
+ return new PackagerClass(pkg, {
41
+ ...options,
42
+ platformKey,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * @param {string} platformKey
48
+ * @param {{ config?: { platforms?: Record<string, { packager?: string }> } }} pkg
49
+ * @returns {PackagerName}
50
+ */
51
+ export function resolvePlatformPackager(platformKey, pkg) {
52
+ return Packager.resolvePlatformPackager(platformKey, pkg);
53
+ }
54
+
55
+ /**
56
+ * @param {string} platformKey
57
+ * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string> }>, steamId?: number } }} pkg
58
+ */
59
+ export function validatePlatformPackager(platformKey, pkg) {
60
+ createPackagerForPlatform(platformKey, pkg).validate();
61
+ }
62
+
63
+ /**
64
+ * @param {Record<string, string>} registrations
65
+ */
66
+ export function usesSteamAuth(registrations) {
67
+ return Object.values(registrations).includes('steam-auth');
68
+ }
69
+
70
+ /**
71
+ * @param {string} dest
72
+ * @param {string} platformKey
73
+ * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string> }>, steamId?: number } }} pkg
74
+ * @param {{ log?: (message: string) => void, platforms?: string[] }} [options]
75
+ */
76
+ export function applyPackagerTemplates(dest, platformKey, pkg, options = {}) {
77
+ createPackagerForPlatform(platformKey, pkg, options).applyTemplates(dest);
78
+ }
79
+
80
+ /**
81
+ * @param {string} dest
82
+ * @param {string} platformKey
83
+ * @param {{ config?: { platforms?: Record<string, { packager?: string }> } }} pkg
84
+ * @param {string[]} htmlPaths
85
+ * @param {{ log?: (message: string) => void, platforms?: string[] }} [options]
86
+ */
87
+ export function applyPackagerHtmlExtras(dest, platformKey, pkg, htmlPaths, options = {}) {
88
+ createPackagerForPlatform(platformKey, pkg, options).applyHtmlExtras(dest, htmlPaths);
89
+ }
90
+
91
+ /**
92
+ * @param {PackagerName} packager
93
+ * @param {string} platformKey
94
+ */
95
+ export function needsGameConfig(packager, platformKey) {
96
+ const instance = new PACKAGER_REGISTRY[packager]({}, { platformKey });
97
+ return instance.needsGameConfig();
98
+ }
99
+
100
+ /**
101
+ * @param {PackagerName} packager
102
+ * @param {string} platformKey
103
+ */
104
+ export function buildPackagerHtmlInjection(packager, platformKey) {
105
+ const instance = new PACKAGER_REGISTRY[packager]({}, { platformKey });
106
+ return instance.buildHtmlInjection();
107
+ }
@@ -0,0 +1,13 @@
1
+ import { Packager } from './base.js';
2
+
3
+ export class WebPackager extends Packager {
4
+ /** @type {'web'} */
5
+ static id = 'web';
6
+
7
+ /** @type {string[]} */
8
+ static defaultPlatforms = ['web', 'uwp', 'pwa'];
9
+
10
+ needsGameConfig() {
11
+ return this.platformKey === 'uwp';
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ export * from './packagers/index.js';
@@ -4,6 +4,7 @@ import {
4
4
  adaptfullyInjectionForPlatform,
5
5
  injectAdaptfullyRegistrations,
6
6
  } from './registrations.js';
7
+ import { createPackagerForPlatform } from './packagers.js';
7
8
  import { copyRecursiveSync, emptyDirSync } from './fs-utils.js';
8
9
 
9
10
  /** @typedef {'prebuild' | 'build' | 'deploy'} AdaptfullyStage */
@@ -73,9 +74,13 @@ export function prebuildPlatform(deployFolder, platformKey, pkg, options = {}) {
73
74
  emptyDirSync(dest);
74
75
  copyRecursiveSync(source, dest);
75
76
 
77
+ const packager = createPackagerForPlatform(platformKey, pkg, { log });
78
+ const htmlInjections = resolveHtmlInjections(pkg);
79
+ packager.prebuild(dest, htmlInjections);
80
+
76
81
  const injection = adaptfullyInjectionForPlatform(platformKey, pkg, { log });
77
82
  if (injection) {
78
- for (const relativePath of resolveHtmlInjections(pkg)) {
83
+ for (const relativePath of htmlInjections) {
79
84
  const htmlPath = path.join(dest, relativePath);
80
85
  if (!fs.existsSync(htmlPath)) {
81
86
  throw new Error(`htmlInjections file not found in deploy output: ${relativePath}`);
@@ -1,40 +1,226 @@
1
1
  /* global window */
2
2
 
3
3
  /**
4
- * Steam auth plugin placeholder for Electron / NW.js Steam deployments.
5
- * Wire Steamworks login here; games only talk to adaptfully.get('auth').
4
+ * Steam auth via steamworks.js. Adaptfully prebuild writes Electron main.js and
5
+ * preload.js (when packager is "electron") to expose __ADAPTFULLY_STEAMWORKS__.
6
+ * Fallback bridges: window.steamworks, window.electronAPI, etc.
6
7
  */
7
8
  (function registerSteamAuth(ns) {
8
- const { getStorage } = ns.auth.helpers;
9
+ const { configValue, getStorage } = ns.auth.helpers;
10
+
11
+ const DEFAULT_AUTO_LOGIN_KEY = 'lastLoggedIn';
12
+ const READY_POLL_MS = 50;
13
+ const READY_TIMEOUT_MS = 10000;
14
+
15
+ function normalizeSteamId(value) {
16
+ if (value == null || value === '') {
17
+ return '';
18
+ }
19
+ if (typeof value === 'bigint') {
20
+ return value.toString();
21
+ }
22
+ return String(value);
23
+ }
24
+
25
+ function readSyncIdentity(client) {
26
+ if (!client?.localplayer) {
27
+ return null;
28
+ }
29
+
30
+ try {
31
+ const steamId = client.localplayer.getSteamId?.();
32
+ const id = normalizeSteamId(steamId?.steamId64 ?? steamId);
33
+ if (!id) {
34
+ return null;
35
+ }
36
+
37
+ const name = client.localplayer.getName?.() || '';
38
+ return {
39
+ id,
40
+ email: '',
41
+ displayName: name,
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async function readAsyncIdentity(bridge) {
49
+ if (!bridge) {
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ let steamIdRaw = bridge.getSteamId?.();
55
+ if (steamIdRaw && typeof steamIdRaw.then === 'function') {
56
+ steamIdRaw = await steamIdRaw;
57
+ }
58
+
59
+ const id = normalizeSteamId(steamIdRaw?.steamId64 ?? steamIdRaw);
60
+ if (!id) {
61
+ return null;
62
+ }
63
+
64
+ let name = '';
65
+ if (typeof bridge.getSteamName === 'function') {
66
+ name = await bridge.getSteamName();
67
+ } else if (typeof bridge.getName === 'function') {
68
+ const resolved = bridge.getName();
69
+ name = resolved && typeof resolved.then === 'function' ? await resolved : resolved;
70
+ }
71
+
72
+ return {
73
+ id,
74
+ email: '',
75
+ displayName: name || '',
76
+ };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function resolveSteamworksClient() {
83
+ if (typeof window.__ADAPTFULLY_STEAMWORKS__ !== 'undefined') {
84
+ return window.__ADAPTFULLY_STEAMWORKS__;
85
+ }
86
+ if (window.steamworks?.localplayer) {
87
+ return window.steamworks;
88
+ }
89
+ if (window.steamworksClient?.localplayer) {
90
+ return window.steamworksClient;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function resolveAsyncBridge() {
96
+ if (window.electronAPI?.getSteamId) {
97
+ return window.electronAPI;
98
+ }
99
+ if (window.adaptfullySteam?.getSteamId) {
100
+ return window.adaptfullySteam;
101
+ }
102
+ return null;
103
+ }
9
104
 
10
105
  class SteamAuthPlugin {
11
106
  constructor() {
12
107
  this.name = 'steam';
13
108
  this.user = null;
14
109
  this.authenticated = false;
110
+ this.online = false;
111
+ this.#client = null;
112
+ }
113
+
114
+ /** @type {object | null} */
115
+ #client;
116
+
117
+ supportsAutoLogin() {
118
+ return true;
119
+ }
120
+
121
+ #autoLoginStorageKey() {
122
+ return configValue('autoLoginStorageKey', DEFAULT_AUTO_LOGIN_KEY);
123
+ }
124
+
125
+ #persistLogin(user) {
126
+ const storage = getStorage();
127
+ storage?.set(this.#autoLoginStorageKey(), user.id);
128
+ }
129
+
130
+ #applyIdentity(identity) {
131
+ if (!identity?.id) {
132
+ this.user = null;
133
+ this.authenticated = false;
134
+ this.online = false;
135
+ return false;
136
+ }
137
+
138
+ this.user = {
139
+ id: identity.id,
140
+ email: identity.email || '',
141
+ displayName: identity.displayName || '',
142
+ };
143
+ this.authenticated = true;
144
+ this.online = true;
145
+ this.#persistLogin(this.user);
146
+ return true;
147
+ }
148
+
149
+ async #resolveIdentity() {
150
+ const client = this.#client ?? resolveSteamworksClient();
151
+ if (client) {
152
+ this.#client = client;
153
+ return readSyncIdentity(client);
154
+ }
155
+
156
+ return readAsyncIdentity(resolveAsyncBridge());
157
+ }
158
+
159
+ #hasSteamBridge() {
160
+ return !!(resolveSteamworksClient() || resolveAsyncBridge());
15
161
  }
16
162
 
17
163
  whenReady(done) {
18
- done();
164
+ if (!this.#hasSteamBridge()) {
165
+ this.online = false;
166
+ done({ error: 'Steamworks client not available' });
167
+ return;
168
+ }
169
+
170
+ const started = Date.now();
171
+ const timeoutMs = Number(configValue('steamReadyTimeoutMs', READY_TIMEOUT_MS)) || READY_TIMEOUT_MS;
172
+
173
+ const finish = async () => {
174
+ const identity = await this.#resolveIdentity();
175
+ if (identity) {
176
+ this.#applyIdentity(identity);
177
+ done();
178
+ return;
179
+ }
180
+
181
+ if (Date.now() - started >= timeoutMs) {
182
+ this.online = false;
183
+ done({ error: 'Steamworks client not available' });
184
+ return;
185
+ }
186
+
187
+ window.setTimeout(finish, READY_POLL_MS);
188
+ };
189
+
190
+ finish();
19
191
  }
20
192
 
21
- login(callback) {
22
- callback({ authenticated: this.authenticated, user: this.getUser() });
193
+ #complete(callback) {
194
+ callback({
195
+ authenticated: this.authenticated,
196
+ user: this.getUser(),
197
+ });
23
198
  }
24
199
 
25
- autoLogin(callback) {
26
- callback({ authenticated: this.authenticated, user: this.getUser() });
200
+ login(callback) {
201
+ this.#resolveIdentity()
202
+ .then((identity) => {
203
+ this.#applyIdentity(identity);
204
+ this.#complete(callback);
205
+ })
206
+ .catch(() => {
207
+ this.user = null;
208
+ this.authenticated = false;
209
+ this.online = false;
210
+ this.#complete(callback);
211
+ });
27
212
  }
28
213
 
29
- supportsAutoLogin() {
30
- return false;
214
+ autoLogin(callback) {
215
+ this.login(callback);
31
216
  }
32
217
 
33
218
  logout(callback) {
34
219
  const storage = getStorage();
35
220
  this.user = null;
36
221
  this.authenticated = false;
37
- storage?.remove('lastLoggedIn');
222
+ this.online = false;
223
+ storage?.remove(this.#autoLoginStorageKey());
38
224
  callback();
39
225
  }
40
226
 
@@ -42,12 +228,20 @@
42
228
  if (!this.authenticated || !this.user) {
43
229
  return null;
44
230
  }
45
- return { id: this.user.id, email: this.user.email || '' };
231
+ return {
232
+ id: this.user.id,
233
+ email: this.user.email || '',
234
+ };
46
235
  }
47
236
 
48
237
  isAuthenticated() {
49
238
  return !!this.authenticated;
50
239
  }
240
+
241
+ /** @returns {object | null} Initialized steamworks.js client, when exposed synchronously */
242
+ getSteamworksClient() {
243
+ return this.#client ?? resolveSteamworksClient();
244
+ }
51
245
  }
52
246
 
53
247
  ns.auth.Steam = () => new SteamAuthPlugin();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/adaptfully",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Platform abstraction and Wrapfully deploy client for Makefully games",
5
5
  "type": "module",
6
6
  "main": "./lib/node/index.js",