mirror of
https://github.com/expo/expo-cli.git
synced 2026-11-15 03:58:07 -10:00
* Upgraded deps * Update withDevServer.ts * Running with Webpack 5 * New logging * Fixed asset creation * Fixed sockets * Update TerminalUI.ts * Added bundling support * Update TerminalUI.ts * fixed startup logic * Updated dev tools web start * Update startAsync.ts * Update yarn.lock * [wip] working again * remove unused * wip -- native fast refresh * remove unused * wip * fixed fast refresh dev server across web/ios * support arbitrary ports * fixed image loading on web * fixup refresh * wip multi-config -- not working with fast refresh * wip kinda working * wip -- native fast refresh working * wip -- working all platforms * Added asset Metro middleware interop * Disable react-error-overlay * add web stuff back * improve symbolication * added proper patches * patch package * Update webpackHotDevClient.ts * Update __webpack_require__.l.ts * wip -- no merge * wip -- no merge * wip * wip -- no commit * wip -- updated interface * wip -- update runtime * fix versions * Fix types fixed all runtime/ TS errors fix ts errors change redirects drop deprecations Drop native output Update embed.ts Delete embed.ts revert extras remove asset middleware updated ops split out HMR plugin Update webpack.config.ts * Remove patches * Delete webpack-dev-middleware_dist_utils_setupOutputFileSystem.js * Drop extra native features * revert code * Update defaults.ts * added types * fix yarn Update PackagerLogsStream.ts Update yarn.lock * Update getFileAsync.ts * Update webpack.config.ts * Update Webpack.ts * Fix splitChunks * Remove react-dev-utils to add Node LTS (v12) support back Update ModuleNotFoundPlugin.ts Update formatWebpackMessages.ts * fix types * Update createAllLoaders-test.js * Update paths-test.js.snap * fix tests * Update webpack.config-test.js.snap * Fix merge * Fix find-up * Update package.json Update ManifestHandler.ts Update .eslintrc.js Update parseStartOptions.ts fix types Update yarn.lock Delete more unused code and fix types fix types drop unimodules helper (no longer used in Next.js adapter) update tests drop more native support wip -- drop native update deps Update basic-test.js updated tests Update startAsync.ts drop / revert Update package.json * update tests * fix tests * fix tests --------- Co-authored-by: Evan Bacon <[email protected]>
714 lines
17 KiB
TypeScript
714 lines
17 KiB
TypeScript
import { getConfig, writeConfigJsonAsync } from '@expo/config';
|
|
import spawnAsync from '@expo/spawn-async';
|
|
import { makeExecutableSchema } from 'graphql-tools';
|
|
import { $$asyncIterator } from 'iterall';
|
|
import {
|
|
Android,
|
|
ConnectionStatus,
|
|
Env,
|
|
Exp,
|
|
isDevClientPackageInstalled,
|
|
Logger,
|
|
Project,
|
|
ProjectSettings,
|
|
ProjectUtils,
|
|
Simulator,
|
|
UrlUtils,
|
|
UserManager,
|
|
UserSettings,
|
|
Webpack,
|
|
} from 'xdl';
|
|
|
|
import mergeAsyncIterators from '../asynciterators/mergeAsyncIterators';
|
|
|
|
// for prettier
|
|
const graphql = <T>(text: T): T => text;
|
|
|
|
const typeDefs = graphql`
|
|
enum Platform {
|
|
ANDROID
|
|
IOS
|
|
}
|
|
|
|
enum HostType {
|
|
lan
|
|
localhost
|
|
tunnel
|
|
}
|
|
|
|
type User {
|
|
username: String
|
|
}
|
|
|
|
type Project {
|
|
id: ID!
|
|
# Absolute path of the project directory.
|
|
projectDir: String!
|
|
# The URL where the Expo manifest is being served.
|
|
manifestUrl: String
|
|
# Settings specific to this project, e.g. URL settings.
|
|
settings: ProjectSettings!
|
|
# Project configuration from app.json.
|
|
config: ProjectConfig
|
|
# Things that can send messages
|
|
sources: [Source]
|
|
# All messages from all sources
|
|
messages: MessageConnection!
|
|
# Link to the loding page if available.
|
|
interstitialPageUrl: String
|
|
}
|
|
|
|
type ProjectSettings {
|
|
hostType: HostType!
|
|
dev: Boolean!
|
|
minify: Boolean!
|
|
}
|
|
|
|
input ProjectSettingsInput {
|
|
hostType: HostType
|
|
dev: Boolean
|
|
minify: Boolean
|
|
}
|
|
|
|
type ProjectConfig {
|
|
name: String
|
|
description: String
|
|
slug: String
|
|
githubUrl: String
|
|
platforms: [String]
|
|
}
|
|
|
|
input ProjectConfigInput {
|
|
name: String
|
|
description: String
|
|
slug: String
|
|
githubUrl: String
|
|
}
|
|
|
|
type UserSettings {
|
|
id: ID!
|
|
sendTo: String
|
|
}
|
|
|
|
enum SendMedium {
|
|
email
|
|
sms
|
|
}
|
|
|
|
type SendProjectResult {
|
|
medium: SendMedium!
|
|
url: String!
|
|
}
|
|
|
|
union OpenSimulatorResult = OpenSimulatorSuccess | OpenSimulatorError
|
|
|
|
type OpenSimulatorSuccess {
|
|
url: String!
|
|
}
|
|
|
|
type OpenSimulatorError {
|
|
error: String!
|
|
}
|
|
|
|
type OpenWebResult {
|
|
error: String
|
|
}
|
|
|
|
type PublishProjectResult {
|
|
url: String!
|
|
}
|
|
|
|
interface Source {
|
|
id: ID!
|
|
name: String!
|
|
messages: MessageConnection!
|
|
}
|
|
|
|
type Issues implements Source {
|
|
id: ID!
|
|
name: String!
|
|
messages: MessageConnection!
|
|
}
|
|
|
|
type Process implements Source {
|
|
id: ID!
|
|
name: String!
|
|
messages: MessageConnection!
|
|
}
|
|
|
|
type Device implements Source {
|
|
id: ID!
|
|
name: String!
|
|
messages: MessageConnection!
|
|
}
|
|
|
|
enum LogLevel {
|
|
DEBUG
|
|
INFO
|
|
WARN
|
|
ERROR
|
|
}
|
|
|
|
interface Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Source!
|
|
level: LogLevel!
|
|
}
|
|
|
|
type Issue implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Issues!
|
|
level: LogLevel!
|
|
}
|
|
|
|
type LogMessage implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Process!
|
|
level: LogLevel!
|
|
}
|
|
|
|
type DeviceMessage implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Device!
|
|
level: LogLevel!
|
|
includesStack: Boolean
|
|
}
|
|
|
|
type MetroInitializeStarted implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Process!
|
|
level: LogLevel!
|
|
}
|
|
|
|
type BuildProgress implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Process!
|
|
level: LogLevel!
|
|
progress: Int!
|
|
duration: Int!
|
|
}
|
|
|
|
type BuildFinished implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Process!
|
|
level: LogLevel!
|
|
duration: Int!
|
|
}
|
|
|
|
type BuildError implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Process!
|
|
level: LogLevel!
|
|
error: String!
|
|
duration: Int!
|
|
}
|
|
|
|
type TunnelReady implements Message {
|
|
id: ID!
|
|
msg: String!
|
|
time: String!
|
|
source: Process!
|
|
level: LogLevel!
|
|
}
|
|
|
|
type PageInfo {
|
|
lastReadCursor: String
|
|
lastCursor: String
|
|
}
|
|
|
|
type MessageConnection {
|
|
count: Int!
|
|
unreadCount: Int!
|
|
nodes: [Message!]
|
|
pageInfo: PageInfo
|
|
}
|
|
|
|
enum MessagePayloadType {
|
|
ADDED
|
|
UPDATED
|
|
DELETED
|
|
}
|
|
|
|
union MessageSubscriptionPayload = MessagePayload | SourceClearedPayload
|
|
|
|
type MessagePayload {
|
|
type: MessagePayloadType!
|
|
cursor: String
|
|
node: Message!
|
|
}
|
|
|
|
type SourceClearedPayload {
|
|
source: Source
|
|
}
|
|
|
|
type ProjectManagerLayout {
|
|
id: ID!
|
|
selected: Source
|
|
sources: [Source]
|
|
}
|
|
|
|
input ProjectManagerLayoutInput {
|
|
selected: ID
|
|
sources: [ID!]
|
|
}
|
|
|
|
type ProcessInfo {
|
|
isAndroidSimulatorSupported: Boolean
|
|
isIosSimulatorSupported: Boolean
|
|
webAppUrl: String
|
|
}
|
|
|
|
type Query {
|
|
# The project this instance of the XDL server is serving.
|
|
currentProject: Project!
|
|
# Globally persisted user preferences.
|
|
userSettings: UserSettings!
|
|
# Layout of the sections in project manager
|
|
projectManagerLayout: ProjectManagerLayout
|
|
# Information about the current process
|
|
processInfo: ProcessInfo
|
|
# Current logged-in user
|
|
user: User
|
|
}
|
|
|
|
type Mutation {
|
|
# Opens the app in an iOS simulator or and Android device/emulator.
|
|
openSimulator(platform: Platform!): OpenSimulatorResult
|
|
# Starts WebPack server
|
|
openWeb: OpenWebResult
|
|
# Publishes the current project to expo.dev (classic updates)
|
|
publishProject(releaseChannel: String): PublishProjectResult
|
|
# Sends the project URL by email.
|
|
sendProjectUrl(recipient: String!): SendProjectResult
|
|
# Compresses the images in a project
|
|
optimizeAssets: Project
|
|
# Updates specified project settings.
|
|
setProjectSettings(settings: ProjectSettingsInput!): Project
|
|
# Update projectConfig
|
|
setProjectConfig(input: ProjectConfigInput!): Project
|
|
# Update the layout
|
|
setProjectManagerLayout(input: ProjectManagerLayoutInput): ProjectManagerLayout
|
|
# Update a last read status for a source
|
|
updateLastRead(sourceId: ID!, lastReadCursor: String!): Source
|
|
# Clear a log from a source
|
|
clearMessages(sourceId: ID!): Source
|
|
}
|
|
|
|
type Subscription {
|
|
# TODO(freiksenet): per-project log
|
|
messages(after: String): MessageSubscriptionPayload
|
|
}
|
|
`;
|
|
|
|
const messageResolvers = {
|
|
level(message: { level: number }): string {
|
|
if (message.level <= Logger.DEBUG) return 'DEBUG';
|
|
if (message.level <= Logger.INFO) return 'INFO';
|
|
if (message.level <= Logger.WARN) return 'WARN';
|
|
return 'ERROR';
|
|
},
|
|
};
|
|
|
|
const USER_SETTINGS_ID = 'UserSettings';
|
|
const PROJECT_MANAGER_LAYOUT_ID = 'ProjectManagerLayout';
|
|
|
|
const resolvers = {
|
|
OpenSimulatorResult: {
|
|
__resolveType(result) {
|
|
if (result.success) {
|
|
return 'OpenSimulatorSuccess';
|
|
} else {
|
|
return 'OpenSimulatorError';
|
|
}
|
|
},
|
|
},
|
|
Message: {
|
|
__resolveType(message) {
|
|
if (message.tag === 'device') {
|
|
return 'DeviceMessage';
|
|
} else if (message.issueId) {
|
|
return 'Issue';
|
|
} else if (message._metroEventType) {
|
|
switch (message._metroEventType) {
|
|
case 'METRO_INITIALIZE_STARTED': {
|
|
return 'MetroInitializeStarted';
|
|
}
|
|
case 'BUILD_STARTED':
|
|
case 'BUILD_PROGRESS': {
|
|
return 'BuildProgress';
|
|
}
|
|
case 'FINISHED': {
|
|
return 'BuildFinished';
|
|
}
|
|
case 'FAILED': {
|
|
return 'BuildError';
|
|
}
|
|
}
|
|
} else if (message._expoEventType === 'TUNNEL_READY') {
|
|
return 'TunnelReady';
|
|
}
|
|
return 'LogMessage';
|
|
},
|
|
},
|
|
Issue: {
|
|
...messageResolvers,
|
|
id(issue) {
|
|
return `Issue:${issue.id}`;
|
|
},
|
|
source(issue, args, context) {
|
|
return context.getIssuesSource();
|
|
},
|
|
},
|
|
LogMessage: {
|
|
...messageResolvers,
|
|
source(message, args, context) {
|
|
return context.getProcessSource();
|
|
},
|
|
},
|
|
MetroInitializeStarted: {
|
|
...messageResolvers,
|
|
source(message, args, context) {
|
|
return context.getProcessSource();
|
|
},
|
|
},
|
|
BuildProgress: {
|
|
...messageResolvers,
|
|
source(message, args, context) {
|
|
return context.getProcessSource();
|
|
},
|
|
},
|
|
BuildFinished: {
|
|
...messageResolvers,
|
|
source(message, args, context) {
|
|
return context.getProcessSource();
|
|
},
|
|
},
|
|
BuildError: {
|
|
...messageResolvers,
|
|
source(message, args, context) {
|
|
return context.getProcessSource();
|
|
},
|
|
},
|
|
TunnelReady: {
|
|
...messageResolvers,
|
|
source(message, args, context) {
|
|
return context.getProcessSource();
|
|
},
|
|
},
|
|
DeviceMessage: {
|
|
...messageResolvers,
|
|
source(message) {
|
|
return { id: message.deviceId, name: message.deviceName };
|
|
},
|
|
},
|
|
MessageSubscriptionPayload: {
|
|
__resolveType(payload) {
|
|
if (payload.type === 'CLEARED') {
|
|
return 'SourceClearedPayload';
|
|
} else {
|
|
return 'MessagePayload';
|
|
}
|
|
},
|
|
},
|
|
SourceClearedPayload: {
|
|
source(payload, args, context) {
|
|
return context.getSourceById(payload.sourceId);
|
|
},
|
|
},
|
|
Project: {
|
|
id(project) {
|
|
return Buffer.from(project.projectDir).toString('base64');
|
|
},
|
|
async manifestUrl(project) {
|
|
if ((await ProjectSettings.getCurrentStatusAsync(project.projectDir)) === 'running') {
|
|
return UrlUtils.constructDeepLinkAsync(project.projectDir);
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
settings(project) {
|
|
return ProjectSettings.readAsync(project.projectDir);
|
|
},
|
|
async config(project) {
|
|
try {
|
|
const { exp } = getConfig(project.projectDir);
|
|
return exp;
|
|
} catch (error: any) {
|
|
ProjectUtils.logError(project.projectDir, 'expo', error.message);
|
|
return null;
|
|
}
|
|
},
|
|
sources(project, args, context) {
|
|
return context.getSources();
|
|
},
|
|
messages(project, args, context) {
|
|
return context.getMessageConnection();
|
|
},
|
|
async interstitialPageUrl(project) {
|
|
const { devClient } = await ProjectSettings.readAsync(project.projectDir);
|
|
if (
|
|
Env.isInterstitiaLPageEnabled() &&
|
|
!devClient &&
|
|
isDevClientPackageInstalled(project.projectDir)
|
|
) {
|
|
return UrlUtils.constructLoadingUrlAsync(project.projectDir, null);
|
|
}
|
|
return null;
|
|
},
|
|
},
|
|
ProjectSettings: {
|
|
hostType(projectSettings) {
|
|
if (ConnectionStatus.isOffline() && projectSettings.hostType === 'tunnel') {
|
|
return 'lan';
|
|
} else {
|
|
return projectSettings.hostType;
|
|
}
|
|
},
|
|
},
|
|
Source: {
|
|
__resolveType(source) {
|
|
return source.__typename;
|
|
},
|
|
},
|
|
Issues: {
|
|
messages(source, args, context) {
|
|
return context.getMessageConnection(source);
|
|
},
|
|
},
|
|
Process: {
|
|
messages(source, args, context) {
|
|
return context.getMessageConnection(source);
|
|
},
|
|
},
|
|
Device: {
|
|
messages(source, args, context) {
|
|
return context.getMessageConnection(source);
|
|
},
|
|
},
|
|
ProjectManagerLayout: {
|
|
id() {
|
|
return PROJECT_MANAGER_LAYOUT_ID;
|
|
},
|
|
selected(layout, args, context) {
|
|
const sources = context.getSources();
|
|
return sources.find(source => source.id === layout.selected);
|
|
},
|
|
sources(layout, args, context) {
|
|
const sources = context.getSources();
|
|
if (!layout.sources) {
|
|
return [];
|
|
} else {
|
|
return layout.sources
|
|
.map(sourceId => sources.find(source => source.id === sourceId))
|
|
.filter(source => source);
|
|
}
|
|
},
|
|
},
|
|
UserSettings: {
|
|
id() {
|
|
return USER_SETTINGS_ID;
|
|
},
|
|
},
|
|
Query: {
|
|
currentProject(parent, args, context) {
|
|
return context.getCurrentProject();
|
|
},
|
|
userSettings() {
|
|
return UserSettings.readAsync();
|
|
},
|
|
projectManagerLayout(parent, args, context) {
|
|
return context.getProjectManagerLayout();
|
|
},
|
|
async processInfo(parent, args, context) {
|
|
return {
|
|
isAndroidSimulatorSupported: Android.isPlatformSupported(),
|
|
isIosSimulatorSupported: Simulator.isPlatformSupported(),
|
|
};
|
|
},
|
|
async user() {
|
|
const username = await UserManager.getCurrentUsernameAsync();
|
|
return { username };
|
|
},
|
|
},
|
|
Mutation: {
|
|
async openWeb(parent, props, context) {
|
|
const currentProject = context.getCurrentProject();
|
|
try {
|
|
await Project.startWebpackAsync(currentProject.projectDir);
|
|
await Webpack.openAsync(currentProject.projectDir);
|
|
|
|
return {
|
|
success: true,
|
|
error: null,
|
|
};
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
error: error.toString(),
|
|
};
|
|
}
|
|
},
|
|
async openSimulator(parent, { platform }, context) {
|
|
const currentProject = context.getCurrentProject();
|
|
let result;
|
|
if (platform === 'ANDROID') {
|
|
result = await Android.openProjectAsync({
|
|
projectRoot: currentProject.projectDir,
|
|
shouldPrompt: false,
|
|
});
|
|
} else {
|
|
result = await Simulator.openProjectAsync({
|
|
projectRoot: currentProject.projectDir,
|
|
shouldPrompt: false,
|
|
});
|
|
}
|
|
if (result.success) {
|
|
return result;
|
|
} else {
|
|
return {
|
|
success: false,
|
|
error: result.error.toString(),
|
|
};
|
|
}
|
|
},
|
|
async publishProject(parent, { releaseChannel }, context) {
|
|
const { projectDir } = context.getCurrentProject();
|
|
try {
|
|
const result = await Project.publishAsync(projectDir, { releaseChannel });
|
|
return result;
|
|
} catch (error: any) {
|
|
ProjectUtils.logError(projectDir, 'expo', error.message);
|
|
throw error;
|
|
}
|
|
},
|
|
async setProjectSettings(parent, { settings }, context) {
|
|
const currentProject = context.getCurrentProject();
|
|
const previousSettings = await ProjectSettings.readAsync(currentProject.projectDir);
|
|
const updatedSettings = await ProjectSettings.setAsync(currentProject.projectDir, settings);
|
|
|
|
// If 'tunnel' wasn't previously configured and it will be as a result of this request, start tunnels.
|
|
if (previousSettings.hostType !== 'tunnel' && updatedSettings.hostType === 'tunnel') {
|
|
try {
|
|
await Project.startTunnelsAsync(currentProject.projectDir, { autoInstall: true });
|
|
} catch (e: any) {
|
|
ProjectUtils.logWarning(
|
|
currentProject.projectDir,
|
|
'expo',
|
|
`Error starting tunnel ${e.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
...currentProject,
|
|
settings: updatedSettings,
|
|
};
|
|
},
|
|
async optimizeAssets(parent, { settings }, context) {
|
|
const currentProject = context.getCurrentProject();
|
|
|
|
await spawnAsync('npx', ['expo-optimize'], {
|
|
cwd: currentProject.projectDir,
|
|
});
|
|
return {
|
|
...currentProject,
|
|
};
|
|
},
|
|
async setProjectConfig(parent, { input }, context) {
|
|
const currentProject = context.getCurrentProject();
|
|
const filteredInput = {
|
|
...input,
|
|
githubUrl: input.githubUrl.match(/^https:\/\/github.com\//) ? input.githubUrl : undefined,
|
|
};
|
|
const { exp } = await writeConfigJsonAsync(currentProject.projectDir, filteredInput);
|
|
return {
|
|
...currentProject,
|
|
config: exp,
|
|
};
|
|
},
|
|
async sendProjectUrl(parent, { recipient }, context) {
|
|
const currentProject = context.getCurrentProject();
|
|
const url = await UrlUtils.constructManifestUrlAsync(currentProject.projectDir);
|
|
const result = await Exp.sendAsync(recipient, url);
|
|
await UserSettings.setAsync('sendTo', recipient);
|
|
return { medium: result.medium, url }; // medium can be a phone number or email
|
|
},
|
|
setProjectManagerLayout(parent, { input }, context) {
|
|
context.setProjectManagerLayout(input);
|
|
return context.getProjectManagerLayout();
|
|
},
|
|
updateLastRead(parent, { sourceId, lastReadCursor }, context) {
|
|
const source = context.getSourceById(sourceId);
|
|
if (source) {
|
|
context.setLastRead(sourceId, lastReadCursor);
|
|
return source;
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
clearMessages(parent, { sourceId }, context) {
|
|
const source = context.getSourceById(sourceId);
|
|
context.clearMessages(source.id);
|
|
return source;
|
|
},
|
|
},
|
|
Subscription: {
|
|
messages: {
|
|
subscribe(parent, { after }, context) {
|
|
let parsedCursor: number | null = null;
|
|
if (after) {
|
|
parsedCursor = parseInt(after, 10);
|
|
}
|
|
const issueIterator = context.getIssueIterator();
|
|
const messageIterator = context.getMessageIterator(parsedCursor);
|
|
const iterator = mergeAsyncIterators(issueIterator, messageIterator);
|
|
|
|
return {
|
|
async next() {
|
|
const result = await iterator.next();
|
|
const { done, value } = result;
|
|
return {
|
|
value: {
|
|
messages: {
|
|
...value,
|
|
},
|
|
},
|
|
done,
|
|
};
|
|
},
|
|
|
|
return() {
|
|
return iterator.return();
|
|
},
|
|
|
|
throw(error) {
|
|
return iterator.throw(error);
|
|
},
|
|
|
|
[$$asyncIterator]() {
|
|
return this;
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
export default makeExecutableSchema({ typeDefs, resolvers });
|