-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement GUI for selecting eas build configuration #932
Changes from 5 commits
eeb2197
569b76f
1e4361b
6f44582
c46f779
fc4691b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
export interface EasConfig { | ||
build?: EasBuildConfig; | ||
} | ||
|
||
export type EasBuildConfig = { | ||
[key: string]: EasBuildProfile; | ||
}; | ||
|
||
export type EasBuildDistributionType = "internal" | "store"; | ||
|
||
export interface EasBuildProfileIOSSpecific { | ||
simulator?: boolean; | ||
} | ||
|
||
export interface EasBuildProfile { | ||
distribution?: EasBuildDistributionType; | ||
ios?: EasBuildProfileIOSSpecific; | ||
} | ||
|
||
export function isEasConfig(obj: unknown): obj is EasConfig { | ||
if (typeof obj !== "object" || obj === null) { | ||
return false; | ||
} | ||
|
||
return !("build" in obj) || obj.build === undefined || isEasBuildConfig(obj.build); | ||
} | ||
|
||
export function isEasBuildConfig(obj: unknown): obj is EasBuildConfig { | ||
if (typeof obj !== "object" || obj === null) { | ||
return false; | ||
} | ||
|
||
return Object.values(obj).every((v) => v === undefined || isEasBuildProfile(v)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again would you mind adding some comments linking to eas documentation or explaining this checks? |
||
} | ||
|
||
export function isEasBuildProfile(obj: unknown): obj is EasBuildProfile { | ||
if (typeof obj !== "object" || obj === null) { | ||
return false; | ||
} | ||
|
||
if ( | ||
"distribution" in obj && | ||
(typeof obj.distribution !== "string" || !["internal", "store"].includes(obj.distribution)) | ||
) { | ||
return false; | ||
} | ||
|
||
if ("ios" in obj && obj.ios !== undefined) { | ||
if (typeof obj.ios !== "object" || obj.ios === null) { | ||
return false; | ||
} | ||
if ( | ||
"simulator" in obj.ios && | ||
obj.ios.simulator !== undefined && | ||
!(typeof obj.ios.simulator === "boolean") | ||
) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,8 @@ import { getAppRootFolder } from "../utilities/extensionContext"; | |
import { findXcodeProject, findXcodeScheme } from "../utilities/xcode"; | ||
import { Logger } from "../Logger"; | ||
import { getIosSourceDir } from "../builders/buildIOS"; | ||
import { readEasConfig } from "../utilities/eas"; | ||
import { EasBuildConfig } from "../common/EasConfig"; | ||
|
||
export class LaunchConfigController implements Disposable, LaunchConfig { | ||
private config: LaunchConfigurationOptions; | ||
|
@@ -106,6 +108,14 @@ export class LaunchConfigController implements Disposable, LaunchConfig { | |
); | ||
return await findXcodeScheme(xcodeProject); | ||
} | ||
|
||
async getAvailableEasProfiles(): Promise<EasBuildConfig> { | ||
const appRootFolder = getAppRootFolder(); | ||
const easConfig = await readEasConfig(appRootFolder); | ||
const easBuildConfig = easConfig?.build ?? {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
this is an example eas configuration (from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've thought about it. I'm not sure we should hide the "invalid" configs. I feel like ideally we'd show all configs, but disable selecting the "invalid" ones with some information to the user why they are not selectable, but I'm not sure how much work it is to add that functionality to our components. |
||
return easBuildConfig; | ||
} | ||
|
||
async addListener<K extends keyof LaunchConfigEventMap>( | ||
eventType: K, | ||
listener: LaunchConfigEventListener<LaunchConfigEventMap[K]> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { RelativePattern, Uri, workspace } from "vscode"; | ||
import { EasConfig, isEasConfig } from "../common/EasConfig"; | ||
|
||
export async function readEasConfig(appRootFolder: string | Uri): Promise<EasConfig | null> { | ||
const easConfigUri = await workspace.findFiles( | ||
new RelativePattern(appRootFolder, "eas.json"), | ||
null, | ||
1 | ||
); | ||
if (easConfigUri.length === 0) { | ||
return null; | ||
} | ||
try { | ||
const easConfigData = await workspace.fs.readFile(easConfigUri[0]); | ||
const easConfig = JSON.parse(new TextDecoder().decode(easConfigData)); | ||
if (isEasConfig(easConfig)) { | ||
return easConfig; | ||
} | ||
return null; | ||
} catch (err) { | ||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,29 @@ | ||
import "./View.css"; | ||
import "./LaunchConfigurationView.css"; | ||
import { useRef } from "react"; | ||
import { useRef, useState } from "react"; | ||
import Label from "../components/shared/Label"; | ||
import { useLaunchConfig } from "../providers/LaunchConfigProvider"; | ||
import { LaunchConfigUpdater } from "../../common/LaunchConfig"; | ||
import { | ||
EasConfig, | ||
LaunchConfigUpdater, | ||
LaunchConfigurationOptions, | ||
} from "../../common/LaunchConfig"; | ||
import Select from "../components/shared/Select"; | ||
import { Input } from "../components/shared/Input"; | ||
import { EasBuildConfig } from "../../common/EasConfig"; | ||
|
||
function LaunchConfigurationView() { | ||
const { android, appRoot, ios, isExpo, metroConfigPath, update, xcodeSchemes } = | ||
useLaunchConfig(); | ||
const { | ||
android, | ||
appRoot, | ||
ios, | ||
eas, | ||
isExpo, | ||
metroConfigPath, | ||
update, | ||
xcodeSchemes, | ||
easBuildProfiles, | ||
} = useLaunchConfig(); | ||
|
||
return ( | ||
<> | ||
|
@@ -44,6 +58,26 @@ function LaunchConfigurationView() { | |
<IsExpoConfiguration isExpo={isExpo} update={update} /> | ||
|
||
<div className="launch-configuration-section-margin" /> | ||
|
||
{!!easBuildProfiles && ( | ||
<> | ||
<Label>EAS Build</Label> | ||
<EasBuildConfiguration | ||
platform="ios" | ||
eas={eas} | ||
update={update} | ||
easBuildProfiles={easBuildProfiles} | ||
/> | ||
<EasBuildConfiguration | ||
platform="android" | ||
eas={eas} | ||
update={update} | ||
easBuildProfiles={easBuildProfiles} | ||
/> | ||
|
||
<div className="launch-configuration-section-margin" /> | ||
</> | ||
)} | ||
</> | ||
); | ||
} | ||
|
@@ -241,4 +275,142 @@ function IsExpoConfiguration({ isExpo, update }: isExpoConfigurationProps) { | |
); | ||
} | ||
|
||
type EasLaunchConfig = NonNullable<LaunchConfigurationOptions["eas"]>; | ||
type EasPlatform = keyof EasLaunchConfig; | ||
|
||
function prettyPlatformName(platform: EasPlatform): string { | ||
switch (platform) { | ||
case "ios": | ||
return "iOS"; | ||
case "android": | ||
return "Android"; | ||
} | ||
} | ||
|
||
interface easBuildConfigurationProps { | ||
eas?: EasLaunchConfig; | ||
platform: EasPlatform; | ||
update: LaunchConfigUpdater; | ||
easBuildProfiles: EasBuildConfig; | ||
} | ||
|
||
function EasBuildConfiguration({ | ||
maciekstosio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
eas, | ||
platform, | ||
update, | ||
easBuildProfiles, | ||
}: easBuildConfigurationProps) { | ||
const DISABLED = "Disabled" as const; | ||
const CUSTOM = "Custom" as const; | ||
|
||
const profile = eas?.[platform]?.profile; | ||
const buildUUID = eas?.[platform]?.buildUUID; | ||
|
||
const [selectedProfile, setSelectedProfile] = useState<string>(() => { | ||
if (profile === undefined) { | ||
return DISABLED; | ||
} | ||
if (!(profile in easBuildProfiles)) { | ||
return CUSTOM; | ||
} | ||
return profile; | ||
}); | ||
|
||
const buildUUIDInputRef = useRef<HTMLInputElement>(null); | ||
const customBuildProfileInputRef = useRef<HTMLInputElement>(null); | ||
|
||
const updateEasConfig = (configUpdate: Partial<EasConfig>) => { | ||
const currentPlaftormConfig = eas?.[platform] ?? {}; | ||
const newPlatformConfig = Object.fromEntries( | ||
Object.entries({ ...currentPlaftormConfig, ...configUpdate }).filter(([_k, v]) => !!v) | ||
); | ||
if ("profile" in newPlatformConfig) { | ||
update("eas", { ...eas, [platform]: newPlatformConfig }); | ||
} else { | ||
update("eas", { ...eas, [platform]: undefined }); | ||
} | ||
}; | ||
|
||
const updateProfile = (newProfile: string | undefined) => { | ||
const newBuildUUID = buildUUIDInputRef.current?.value || undefined; | ||
updateEasConfig({ profile: newProfile, buildUUID: newBuildUUID }); | ||
}; | ||
|
||
const onProfileSelectionChange = (newProfile: string) => { | ||
setSelectedProfile(newProfile); | ||
|
||
if (newProfile === DISABLED) { | ||
updateEasConfig({ profile: undefined, buildUUID: undefined }); | ||
return; | ||
} | ||
|
||
if (newProfile === CUSTOM) { | ||
newProfile = customBuildProfileInputRef.current?.value ?? profile ?? ""; | ||
} | ||
updateProfile(newProfile); | ||
}; | ||
|
||
const onCustomBuildProfileInputBlur = () => { | ||
const newCustomProfile = customBuildProfileInputRef.current?.value ?? ""; | ||
updateProfile(newCustomProfile); | ||
}; | ||
|
||
const onBuildUUIDInputBlur = () => { | ||
const newBuildUUID = buildUUIDInputRef.current?.value || undefined; | ||
updateEasConfig({ buildUUID: newBuildUUID }); | ||
}; | ||
|
||
const availableEasBuildProfiles = Object.entries(easBuildProfiles).map( | ||
([buildProfile, config]) => { | ||
const canRunInSimulator = | ||
config.distribution === "internal" && | ||
(platform !== "ios" || config.ios?.simulator === true); | ||
return { value: buildProfile, label: buildProfile, disabled: !canRunInSimulator }; | ||
} | ||
); | ||
|
||
availableEasBuildProfiles.push({ value: DISABLED, label: DISABLED, disabled: false }); | ||
availableEasBuildProfiles.push({ value: CUSTOM, label: CUSTOM, disabled: false }); | ||
|
||
return ( | ||
<div className="launch-configuration-container"> | ||
<div className="setting-description">{prettyPlatformName(platform)} Build Profile:</div> | ||
<Select | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should allow additional manual input of a value that is not on the list for users with exotic configurations, the button that does it might be hidden as a last psoittion in the select list for example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly. |
||
value={selectedProfile} | ||
onChange={onProfileSelectionChange} | ||
items={availableEasBuildProfiles} | ||
className="scheme" | ||
/> | ||
{selectedProfile === CUSTOM && ( | ||
<> | ||
<div className="setting-description"> | ||
{prettyPlatformName(platform)} Custom Build Profile: | ||
</div> | ||
<Input | ||
ref={customBuildProfileInputRef} | ||
className="input-configuration" | ||
type="string" | ||
defaultValue={profile ?? ""} | ||
placeholder="Enter the build profile to be used" | ||
onBlur={onCustomBuildProfileInputBlur} | ||
/> | ||
</> | ||
)} | ||
{selectedProfile !== DISABLED && ( | ||
<> | ||
<div className="setting-description">{prettyPlatformName(platform)} Build UUID:</div> | ||
<Input | ||
ref={buildUUIDInputRef} | ||
className="input-configuration" | ||
type="string" | ||
defaultValue={buildUUID ?? ""} | ||
placeholder="Auto (the latest available build will be used)" | ||
onBlur={onBuildUUIDInputBlur} | ||
/> | ||
</> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export default LaunchConfigurationView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is elaborate would you mind commenting why this check is "correct"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just checking if the shape of the object matches what we expect it to be -- is it really that puzzling?
I suppose you might want a reference to some schema here for the
eas.json
file -- previously this was in the same place that read that file, so the context was there to see that's what it's doing, but I suppose now that it's an exported part of a module, some comment might be useful.