66 * LICENSE file in the root directory of this source tree.
77 */
88
9+ import { execFile } from 'child_process' ;
910import { fileURLToPath } from 'url' ;
1011import fs from 'fs' ;
1112import https from 'https' ;
1213import path from 'path' ;
14+ import { promisify } from 'util' ;
15+
16+ const execFileAsync = promisify ( execFile ) ;
1317
1418const __filename = fileURLToPath ( import . meta. url ) ;
1519const __dirname = path . dirname ( __filename ) ;
@@ -21,20 +25,102 @@ const OUTPUT_FILE = path.join(
2125 '../src/gamepad/generated-profiles.ts' ,
2226) ;
2327
24- // Check if we should skip fetching (file exists and --force not passed)
25- const forceRefresh = process . argv . includes ( '--force' ) ;
28+ function readFlagValue ( flag ) {
29+ const prefix = `${ flag } =` ;
30+ const inline = process . argv . find ( ( arg ) => arg . startsWith ( prefix ) ) ;
31+ if ( inline ) {
32+ return inline . slice ( prefix . length ) ;
33+ }
34+
35+ const index = process . argv . indexOf ( flag ) ;
36+ if ( index !== - 1 ) {
37+ return process . argv [ index + 1 ] ;
38+ }
39+
40+ return undefined ;
41+ }
42+
43+ const forceRefresh =
44+ process . argv . includes ( '--force' ) || process . argv . includes ( '--refresh' ) ;
45+ const offline = process . argv . includes ( '--offline' ) ;
46+ const proxyUrl =
47+ readFlagValue ( '--proxy' ) ??
48+ process . env . HTTPS_PROXY ??
49+ process . env . https_proxy ??
50+ process . env . HTTP_PROXY ??
51+ process . env . http_proxy ;
52+
53+ function redactUrlForLogs ( value ) {
54+ try {
55+ const parsed = new URL ( value ) ;
56+ if ( parsed . username || parsed . password ) {
57+ parsed . username = '***' ;
58+ parsed . password = '***' ;
59+ }
60+ return parsed . toString ( ) ;
61+ } catch {
62+ return '<configured>' ;
63+ }
64+ }
65+
66+ function explainNetworkMode ( ) {
67+ if ( offline ) {
68+ console . log (
69+ '🌐 Network mode: offline; using existing generated profiles only.' ,
70+ ) ;
71+ } else if ( proxyUrl ) {
72+ console . log (
73+ `🌐 Network mode: fetching input profiles through proxy ${ redactUrlForLogs ( proxyUrl ) } ` ,
74+ ) ;
75+ } else {
76+ console . log ( '🌐 Network mode: direct HTTPS fetch.' ) ;
77+ }
78+ }
79+
80+ // Check if we should skip fetching (file exists and --force/--refresh not passed)
2681if ( ! forceRefresh && fs . existsSync ( OUTPUT_FILE ) ) {
2782 console . log (
2883 `✅ Input profiles already exist at ${ OUTPUT_FILE } , skipping CDN fetch.` ,
2984 ) ;
30- console . log ( ' Use --force to refresh from CDN.' ) ;
85+ console . log ( ' Use --force or --refresh to refresh from CDN.' ) ;
3186 process . exit ( 0 ) ;
3287}
3388
34- function fetchJson ( url ) {
89+ if ( offline ) {
90+ console . error (
91+ `❌ Offline profile generation requested, but ${ OUTPUT_FILE } does not exist.` ,
92+ ) ;
93+ console . error ( ' Run this command once with network access to create it.' ) ;
94+ process . exit ( 1 ) ;
95+ }
96+
97+ function fetchJsonDirect ( url , redirectsRemaining = 5 ) {
3598 return new Promise ( ( resolve , reject ) => {
3699 https
37100 . get ( url , ( res ) => {
101+ if (
102+ res . statusCode &&
103+ res . statusCode >= 300 &&
104+ res . statusCode < 400 &&
105+ res . headers . location &&
106+ redirectsRemaining > 0
107+ ) {
108+ res . resume ( ) ;
109+ const redirectedUrl = new URL ( res . headers . location , url ) . toString ( ) ;
110+ fetchJsonDirect ( redirectedUrl , redirectsRemaining - 1 )
111+ . then ( resolve )
112+ . catch ( reject ) ;
113+ return ;
114+ }
115+
116+ if ( res . statusCode !== 200 ) {
117+ res . resume ( ) ;
118+ reject (
119+ new Error ( `Request failed for ${ url } : HTTP ${ res . statusCode } ` ) ,
120+ ) ;
121+ return ;
122+ }
123+
38124 let data = '' ;
39125
40126 res . on ( 'data' , ( chunk ) => {
@@ -59,22 +145,39 @@ function fetchJson(url) {
59145 } ) ;
60146}
61147
62- function toCamelCase ( str ) {
63- return str
64- . replace ( / [ - \/ ] / g, ' ' )
65- . replace ( / \. j s o n $ / , '' )
66- . split ( ' ' )
67- . map ( ( word , index ) => {
68- if ( index === 0 ) {
69- return word . toLowerCase ( ) ;
70- }
71- return word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) . toLowerCase ( ) ;
72- } )
73- . join ( '' ) ;
148+ async function fetchJsonWithCurl ( url ) {
149+ const { stdout } = await execFileAsync (
150+ 'curl' ,
151+ [
152+ '--fail' ,
153+ '--silent' ,
154+ '--show-error' ,
155+ '--location' ,
156+ '--proxy' ,
157+ proxyUrl ,
158+ url ,
159+ ] ,
160+ {
161+ maxBuffer : 20 * 1024 * 1024 ,
162+ } ,
163+ ) ;
164+ return JSON . parse ( stdout ) ;
165+ }
166+
167+ async function fetchJson ( url ) {
168+ try {
169+ return proxyUrl ? await fetchJsonWithCurl ( url ) : await fetchJsonDirect ( url ) ;
170+ } catch ( error ) {
171+ const hint = proxyUrl
172+ ? 'Check that the proxy is reachable and that curl is available.'
173+ : 'If you are on a Linux dev server that requires a proxy, set HTTPS_PROXY/HTTP_PROXY or pass --proxy <url>.' ;
174+ throw new Error ( `Failed to fetch ${ url } . ${ hint } ` , { cause : error } ) ;
175+ }
74176}
75177
76178async function generateInputProfiles ( ) {
77179 try {
180+ explainNetworkMode ( ) ;
78181 console . log ( 'Fetching profiles list...' ) ;
79182 const profilesList = await fetchJson ( `${ CDN_BASE_URL } /profilesList.json` ) ;
80183
@@ -150,6 +253,9 @@ export function getProfilesList(): ProfilesList {
150253 console . log ( `✅ Generated ${ OUTPUT_FILE } with ${ profiles . length } profiles` ) ;
151254 } catch ( error ) {
152255 console . error ( '❌ Error generating input profiles:' , error ) ;
256+ if ( error ?. cause ) {
257+ console . error ( 'Caused by:' , error . cause ) ;
258+ }
153259 process . exit ( 1 ) ;
154260 }
155261}
0 commit comments