@axonpack/preview 0.0.0-stage → 0.1.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 +13 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/android/build.gradle +21 -0
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/java/expo/modules/axonpackpreview/AxonpackPreviewModule.kt +72 -0
- package/android/src/main/java/expo/modules/axonpackpreview/BundleLoaderOverride.kt +41 -0
- package/android/src/main/java/expo/modules/axonpackpreview/HermesHeader.kt +24 -0
- package/android/src/main/java/expo/modules/axonpackpreview/PreviewPackage.kt +76 -0
- package/android/src/main/java/expo/modules/axonpackpreview/PreviewState.kt +295 -0
- package/app.plugin.js +112 -0
- package/bin/preview.js +2 -0
- package/build/cli.cjs +1465 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.d.ts.map +1 -0
- package/build/cli.js +164 -0
- package/build/cli.js.map +1 -0
- package/build/components/preview-list/preview-list.component.d.ts +13 -0
- package/build/components/preview-list/preview-list.component.d.ts.map +1 -0
- package/build/components/preview-list/preview-list.component.js +316 -0
- package/build/components/preview-list/preview-list.component.js.map +1 -0
- package/build/components/preview-list/preview-list.types.d.ts +59 -0
- package/build/components/preview-list/preview-list.types.d.ts.map +1 -0
- package/build/components/preview-list/preview-list.types.js +2 -0
- package/build/components/preview-list/preview-list.types.js.map +1 -0
- package/build/components/preview-provider/preview-provider.component.d.ts +12 -0
- package/build/components/preview-provider/preview-provider.component.d.ts.map +1 -0
- package/build/components/preview-provider/preview-provider.component.js +19 -0
- package/build/components/preview-provider/preview-provider.component.js.map +1 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts +26 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts.map +1 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.js +9 -0
- package/build/constants/dev-preview-servers/dev-preview-servers.const.js.map +1 -0
- package/build/constants/preview-list/theme.const.d.ts +5 -0
- package/build/constants/preview-list/theme.const.d.ts.map +1 -0
- package/build/constants/preview-list/theme.const.js +65 -0
- package/build/constants/preview-list/theme.const.js.map +1 -0
- package/build/index.d.ts +24 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +16 -0
- package/build/index.js.map +1 -0
- package/build/services/cli/config.service.d.ts +13 -0
- package/build/services/cli/config.service.d.ts.map +1 -0
- package/build/services/cli/config.service.js +39 -0
- package/build/services/cli/config.service.js.map +1 -0
- package/build/services/cli/project.service.d.ts +25 -0
- package/build/services/cli/project.service.d.ts.map +1 -0
- package/build/services/cli/project.service.js +81 -0
- package/build/services/cli/project.service.js.map +1 -0
- package/build/services/cli/title.service.d.ts +15 -0
- package/build/services/cli/title.service.d.ts.map +1 -0
- package/build/services/cli/title.service.js +24 -0
- package/build/services/cli/title.service.js.map +1 -0
- package/build/services/dev-preview-servers/discover.service.d.ts +14 -0
- package/build/services/dev-preview-servers/discover.service.d.ts.map +1 -0
- package/build/services/dev-preview-servers/discover.service.js +106 -0
- package/build/services/dev-preview-servers/discover.service.js.map +1 -0
- package/build/services/native/build-info.service.d.ts +11 -0
- package/build/services/native/build-info.service.d.ts.map +1 -0
- package/build/services/native/build-info.service.js +18 -0
- package/build/services/native/build-info.service.js.map +1 -0
- package/build/services/native/native.service.d.ts +24 -0
- package/build/services/native/native.service.d.ts.map +1 -0
- package/build/services/native/native.service.js +13 -0
- package/build/services/native/native.service.js.map +1 -0
- package/build/services/previews/download.service.d.ts +14 -0
- package/build/services/previews/download.service.d.ts.map +1 -0
- package/build/services/previews/download.service.js +66 -0
- package/build/services/previews/download.service.js.map +1 -0
- package/build/services/previews/local-previews.service.d.ts +25 -0
- package/build/services/previews/local-previews.service.d.ts.map +1 -0
- package/build/services/previews/local-previews.service.js +50 -0
- package/build/services/previews/local-previews.service.js.map +1 -0
- package/build/services/previews/reload.service.d.ts +6 -0
- package/build/services/previews/reload.service.d.ts.map +1 -0
- package/build/services/previews/reload.service.js +13 -0
- package/build/services/previews/reload.service.js.map +1 -0
- package/build/services/previews/remote.service.d.ts +11 -0
- package/build/services/previews/remote.service.d.ts.map +1 -0
- package/build/services/previews/remote.service.js +34 -0
- package/build/services/previews/remote.service.js.map +1 -0
- package/build/services/previews/select.service.d.ts +11 -0
- package/build/services/previews/select.service.d.ts.map +1 -0
- package/build/services/previews/select.service.js +25 -0
- package/build/services/previews/select.service.js.map +1 -0
- package/build/services/previews/use-preview.service.d.ts +21 -0
- package/build/services/previews/use-preview.service.d.ts.map +1 -0
- package/build/services/previews/use-preview.service.js +24 -0
- package/build/services/previews/use-preview.service.js.map +1 -0
- package/build/services/previews/use-running.service.d.ts +4 -0
- package/build/services/previews/use-running.service.d.ts.map +1 -0
- package/build/services/previews/use-running.service.js +21 -0
- package/build/services/previews/use-running.service.js.map +1 -0
- package/build/services/publish/bundle.service.d.ts +11 -0
- package/build/services/publish/bundle.service.d.ts.map +1 -0
- package/build/services/publish/bundle.service.js +46 -0
- package/build/services/publish/bundle.service.js.map +1 -0
- package/build/services/publish/commands.service.d.ts +23 -0
- package/build/services/publish/commands.service.d.ts.map +1 -0
- package/build/services/publish/commands.service.js +130 -0
- package/build/services/publish/commands.service.js.map +1 -0
- package/build/services/publish/eas-env.service.d.ts +9 -0
- package/build/services/publish/eas-env.service.d.ts.map +1 -0
- package/build/services/publish/eas-env.service.js +49 -0
- package/build/services/publish/eas-env.service.js.map +1 -0
- package/build/services/registry/registry.service.d.ts +27 -0
- package/build/services/registry/registry.service.d.ts.map +1 -0
- package/build/services/registry/registry.service.js +89 -0
- package/build/services/registry/registry.service.js.map +1 -0
- package/build/services/s3/s3.service.d.ts +59 -0
- package/build/services/s3/s3.service.d.ts.map +1 -0
- package/build/services/s3/s3.service.js +154 -0
- package/build/services/s3/s3.service.js.map +1 -0
- package/build/services/start/start.service.d.ts +48 -0
- package/build/services/start/start.service.d.ts.map +1 -0
- package/build/services/start/start.service.js +172 -0
- package/build/services/start/start.service.js.map +1 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts +21 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts.map +1 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.js +91 -0
- package/build/stores/dev-preview-servers/dev-preview-servers.store.js.map +1 -0
- package/build/stores/previews/previews.store.d.ts +36 -0
- package/build/stores/previews/previews.store.d.ts.map +1 -0
- package/build/stores/previews/previews.store.js +134 -0
- package/build/stores/previews/previews.store.js.map +1 -0
- package/build/utils/config/config.util.d.ts +39 -0
- package/build/utils/config/config.util.d.ts.map +1 -0
- package/build/utils/config/config.util.js +24 -0
- package/build/utils/config/config.util.js.map +1 -0
- package/build/utils/config/expo-config.util.d.ts +30 -0
- package/build/utils/config/expo-config.util.d.ts.map +1 -0
- package/build/utils/config/expo-config.util.js +43 -0
- package/build/utils/config/expo-config.util.js.map +1 -0
- package/build/utils/dev-preview-servers/merge-servers.util.d.ts +47 -0
- package/build/utils/dev-preview-servers/merge-servers.util.d.ts.map +1 -0
- package/build/utils/dev-preview-servers/merge-servers.util.js +37 -0
- package/build/utils/dev-preview-servers/merge-servers.util.js.map +1 -0
- package/build/utils/preview-list/theme.util.d.ts +5 -0
- package/build/utils/preview-list/theme.util.d.ts.map +1 -0
- package/build/utils/preview-list/theme.util.js +12 -0
- package/build/utils/preview-list/theme.util.js.map +1 -0
- package/build/utils/previews/layout.util.d.ts +89 -0
- package/build/utils/previews/layout.util.d.ts.map +1 -0
- package/build/utils/previews/layout.util.js +30 -0
- package/build/utils/previews/layout.util.js.map +1 -0
- package/build/utils/previews/preview-items.util.d.ts +23 -0
- package/build/utils/previews/preview-items.util.d.ts.map +1 -0
- package/build/utils/previews/preview-items.util.js +39 -0
- package/build/utils/previews/preview-items.util.js.map +1 -0
- package/build/utils/previews/preview-title.util.d.ts +2 -0
- package/build/utils/previews/preview-title.util.d.ts.map +1 -0
- package/build/utils/previews/preview-title.util.js +6 -0
- package/build/utils/previews/preview-title.util.js.map +1 -0
- package/build/utils/previews/running.util.d.ts +23 -0
- package/build/utils/previews/running.util.d.ts.map +1 -0
- package/build/utils/previews/running.util.js +23 -0
- package/build/utils/previews/running.util.js.map +1 -0
- package/build/utils/publish/changelog.util.d.ts +5 -0
- package/build/utils/publish/changelog.util.d.ts.map +1 -0
- package/build/utils/publish/changelog.util.js +19 -0
- package/build/utils/publish/changelog.util.js.map +1 -0
- package/build/utils/publish/indexes.util.d.ts +12 -0
- package/build/utils/publish/indexes.util.d.ts.map +1 -0
- package/build/utils/publish/indexes.util.js +52 -0
- package/build/utils/publish/indexes.util.js.map +1 -0
- package/build/utils/s3/sigv4.util.d.ts +24 -0
- package/build/utils/s3/sigv4.util.d.ts.map +1 -0
- package/build/utils/s3/sigv4.util.js +67 -0
- package/build/utils/s3/sigv4.util.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/AxonpackPreview.podspec +30 -0
- package/ios/AxonpackPreviewModule.swift +55 -0
- package/ios/PreviewReactDelegateHandler.swift +147 -0
- package/ios/PreviewState.swift +236 -0
- package/notes/README.md +36 -0
- package/notes/bundles.md +35 -0
- package/notes/dev-preview-servers.md +67 -0
- package/notes/distribution.md +138 -0
- package/notes/native-loader.md +56 -0
- package/notes/storage.md +145 -0
- package/notes/testing.md +97 -0
- package/package.json +107 -4
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { EMPTY_SHA256, sha256Hex, signRequest, } from "../../utils/s3/sigv4.util";
|
|
2
|
+
/** MinIO and R2 accept it too, so a region is only needed for AWS buckets elsewhere. */
|
|
3
|
+
export const DEFAULT_REGION = "us-east-1";
|
|
4
|
+
const trimSlash = (s) => s.replace(/\/+$/, "");
|
|
5
|
+
const encodeKey = (key) => key.split("/").map(encodeURIComponent).join("/");
|
|
6
|
+
/**
|
|
7
|
+
* Splits one URL, `<endpoint>/<bucket>[/<prefix>]`, so a single setting says where everything
|
|
8
|
+
* lives: `http://localhost:9000/previews-public`, `https://<account>.r2.cloudflarestorage.com/previews/my-app`.
|
|
9
|
+
*/
|
|
10
|
+
export function parseBucketUrl(value) {
|
|
11
|
+
// Plain string parsing rather than `URL`, so the CLI and the app read it the same way whatever
|
|
12
|
+
// URL implementation the JS engine has. Empty segments are dropped, so slashes at the end or
|
|
13
|
+
// doubled in the middle change nothing.
|
|
14
|
+
const match = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]+)([^?#]*)/i.exec(value.trim());
|
|
15
|
+
if (!match)
|
|
16
|
+
throw new Error(`${value} is not a URL: use <endpoint>/<bucket>`);
|
|
17
|
+
const [, scheme, host, path] = match;
|
|
18
|
+
const [bucket, ...rest] = path.split("/").filter(Boolean);
|
|
19
|
+
if (!bucket)
|
|
20
|
+
throw new Error(`${value} has no bucket: use <endpoint>/<bucket>`);
|
|
21
|
+
return {
|
|
22
|
+
endpoint: `${scheme.toLowerCase()}://${host}`,
|
|
23
|
+
bucket,
|
|
24
|
+
prefix: rest.length > 0 ? rest.join("/") : undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const withPrefix = (config, key) => config.prefix ? `${config.prefix}/${key}` : key;
|
|
28
|
+
const bucketUrl = (config) => config.bucket
|
|
29
|
+
? `${trimSlash(config.endpoint)}/${config.bucket}`
|
|
30
|
+
: trimSlash(config.endpoint);
|
|
31
|
+
export const objectUrl = (config, key) => `${bucketUrl(config)}/${encodeKey(withPrefix(config, key))}`;
|
|
32
|
+
export class S3Error extends Error {
|
|
33
|
+
status;
|
|
34
|
+
key;
|
|
35
|
+
constructor(status, key, body) {
|
|
36
|
+
super(`S3 ${status} for ${key}: ${body.slice(0, 300)}`);
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.key = key;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function s3Fetch(config, request) {
|
|
42
|
+
const base = request.key === undefined
|
|
43
|
+
? bucketUrl(config)
|
|
44
|
+
: objectUrl(config, request.key);
|
|
45
|
+
const url = new URL(base);
|
|
46
|
+
for (const [k, v] of Object.entries(request.query ?? {}))
|
|
47
|
+
url.searchParams.set(k, v);
|
|
48
|
+
let headers = { ...config.headers, ...request.headers };
|
|
49
|
+
if (config.credentials) {
|
|
50
|
+
headers = signRequest({
|
|
51
|
+
method: request.method,
|
|
52
|
+
url: url.toString(),
|
|
53
|
+
region: config.region,
|
|
54
|
+
credentials: config.credentials,
|
|
55
|
+
headers,
|
|
56
|
+
payloadHash: request.body === undefined ? EMPTY_SHA256 : sha256Hex(request.body),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return fetch(url, {
|
|
60
|
+
method: request.method,
|
|
61
|
+
headers,
|
|
62
|
+
body: request.body,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Null when the object does not exist. The ETag is what a conditional put needs. */
|
|
66
|
+
export async function getJson(config, key) {
|
|
67
|
+
const res = await s3Fetch(config, { method: "GET", key });
|
|
68
|
+
if (res.status === 404 || (res.status === 403 && !config.credentials))
|
|
69
|
+
return null;
|
|
70
|
+
const body = await res.text();
|
|
71
|
+
if (!res.ok)
|
|
72
|
+
throw new S3Error(res.status, key, body);
|
|
73
|
+
try {
|
|
74
|
+
return { data: JSON.parse(body), etag: res.headers.get("etag") };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new Error(notJsonMessage(objectUrl(config, key), res, body));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Something in front of the bucket answered instead of it: most often a private tunnel or proxy
|
|
82
|
+
* sending a login page. Say what came back and from where, instead of a bare JSON parse error.
|
|
83
|
+
*/
|
|
84
|
+
export function notJsonMessage(url, res, body) {
|
|
85
|
+
const type = res.headers.get("content-type")?.split(";")[0];
|
|
86
|
+
const got = type || JSON.stringify(body.trim().slice(0, 80));
|
|
87
|
+
const redirected = res.url && res.url !== url ? `, redirected to ${res.url}` : "";
|
|
88
|
+
return `Expected JSON from ${url} but got ${got} (status ${res.status}${redirected}). Is the URL public, or does it need a read key or headers?`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* False when a condition failed (someone else wrote first), so the caller can read again and
|
|
92
|
+
* retry. `etag: null` means "only if it does not exist yet".
|
|
93
|
+
*/
|
|
94
|
+
export async function putObject(config, key, body, options = {}) {
|
|
95
|
+
const headers = {
|
|
96
|
+
"content-type": options.contentType ?? "application/octet-stream",
|
|
97
|
+
};
|
|
98
|
+
if (options.etag !== undefined) {
|
|
99
|
+
if (options.etag === null)
|
|
100
|
+
headers["if-none-match"] = "*";
|
|
101
|
+
else
|
|
102
|
+
headers["if-match"] = options.etag;
|
|
103
|
+
}
|
|
104
|
+
const res = await s3Fetch(config, { method: "PUT", key, body, headers });
|
|
105
|
+
if (res.status === 412 || res.status === 409)
|
|
106
|
+
return false;
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
throw new S3Error(res.status, key, await res.text());
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
export async function exists(config, key) {
|
|
112
|
+
const res = await s3Fetch(config, { method: "HEAD", key });
|
|
113
|
+
if (res.status === 404)
|
|
114
|
+
return false;
|
|
115
|
+
if (!res.ok)
|
|
116
|
+
throw new S3Error(res.status, key, "");
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
export async function deleteObject(config, key) {
|
|
120
|
+
const res = await s3Fetch(config, { method: "DELETE", key });
|
|
121
|
+
if (!res.ok && res.status !== 404)
|
|
122
|
+
throw new S3Error(res.status, key, await res.text());
|
|
123
|
+
}
|
|
124
|
+
const unescapeXml = (s) => s
|
|
125
|
+
.replace(/</g, "<")
|
|
126
|
+
.replace(/>/g, ">")
|
|
127
|
+
.replace(/"/g, '"')
|
|
128
|
+
.replace(/'/g, "'")
|
|
129
|
+
.replace(/&/g, "&");
|
|
130
|
+
/** Every key under the prefix (ListObjectsV2, following continuation tokens). */
|
|
131
|
+
export async function listKeys(config, prefix) {
|
|
132
|
+
const keys = [];
|
|
133
|
+
const strip = config.prefix ? config.prefix.length + 1 : 0;
|
|
134
|
+
let token;
|
|
135
|
+
do {
|
|
136
|
+
const query = {
|
|
137
|
+
"list-type": "2",
|
|
138
|
+
prefix: withPrefix(config, prefix),
|
|
139
|
+
};
|
|
140
|
+
if (token)
|
|
141
|
+
query["continuation-token"] = token;
|
|
142
|
+
const res = await s3Fetch(config, { method: "GET", query });
|
|
143
|
+
const xml = await res.text();
|
|
144
|
+
if (!res.ok)
|
|
145
|
+
throw new S3Error(res.status, prefix, xml);
|
|
146
|
+
for (const match of xml.matchAll(/<Key>([^<]*)<\/Key>/g))
|
|
147
|
+
keys.push(unescapeXml(match[1]).slice(strip));
|
|
148
|
+
token = /<IsTruncated>true<\/IsTruncated>/.test(xml)
|
|
149
|
+
? unescapeXml(/<NextContinuationToken>([^<]*)</.exec(xml)?.[1] ?? "")
|
|
150
|
+
: undefined;
|
|
151
|
+
} while (token);
|
|
152
|
+
return keys;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=s3.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.service.js","sourceRoot":"","sources":["../../../src/services/s3/s3.service.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Credentials,\n EMPTY_SHA256,\n sha256Hex,\n signRequest,\n} from \"../../utils/s3/sigv4.util\";\n\n/** Any S3-compatible bucket: AWS S3, Cloudflare R2, MinIO and others. Path-style URLs. */\nexport type S3Config = {\n endpoint: string;\n /** Left out when `endpoint` already is the bucket's base, as for the app's unsigned reads. */\n bucket?: string;\n /** Folder inside the bucket that everything goes under, so several apps can share one bucket. */\n prefix?: string;\n region: string;\n /** Write key in the CLI, read-only key in the app. Without one, requests go unsigned. */\n credentials?: Credentials;\n /** Sent with every request, such as a header a proxy or tunnel in front of the bucket wants. */\n headers?: Record<string, string>;\n};\n\n/** MinIO and R2 accept it too, so a region is only needed for AWS buckets elsewhere. */\nexport const DEFAULT_REGION = \"us-east-1\";\n\nconst trimSlash = (s: string) => s.replace(/\\/+$/, \"\");\nconst encodeKey = (key: string) =>\n key.split(\"/\").map(encodeURIComponent).join(\"/\");\n\n/**\n * Splits one URL, `<endpoint>/<bucket>[/<prefix>]`, so a single setting says where everything\n * lives: `http://localhost:9000/previews-public`, `https://<account>.r2.cloudflarestorage.com/previews/my-app`.\n */\nexport function parseBucketUrl(\n value: string,\n): Pick<S3Config, \"endpoint\" | \"bucket\" | \"prefix\"> {\n // Plain string parsing rather than `URL`, so the CLI and the app read it the same way whatever\n // URL implementation the JS engine has. Empty segments are dropped, so slashes at the end or\n // doubled in the middle change nothing.\n const match = /^([a-z][a-z0-9+.-]*):\\/\\/([^/?#]+)([^?#]*)/i.exec(\n value.trim(),\n );\n if (!match) throw new Error(`${value} is not a URL: use <endpoint>/<bucket>`);\n const [, scheme, host, path] = match;\n const [bucket, ...rest] = path.split(\"/\").filter(Boolean);\n if (!bucket)\n throw new Error(`${value} has no bucket: use <endpoint>/<bucket>`);\n return {\n endpoint: `${scheme.toLowerCase()}://${host}`,\n bucket,\n prefix: rest.length > 0 ? rest.join(\"/\") : undefined,\n };\n}\n\nconst withPrefix = (config: S3Config, key: string) =>\n config.prefix ? `${config.prefix}/${key}` : key;\n\nconst bucketUrl = (config: S3Config) =>\n config.bucket\n ? `${trimSlash(config.endpoint)}/${config.bucket}`\n : trimSlash(config.endpoint);\n\nexport const objectUrl = (config: S3Config, key: string) =>\n `${bucketUrl(config)}/${encodeKey(withPrefix(config, key))}`;\n\nexport class S3Error extends Error {\n constructor(\n readonly status: number,\n readonly key: string,\n body: string,\n ) {\n super(`S3 ${status} for ${key}: ${body.slice(0, 300)}`);\n }\n}\n\ntype Request = {\n method: string;\n key?: string;\n query?: Record<string, string>;\n body?: Uint8Array | string;\n headers?: Record<string, string>;\n};\n\nexport async function s3Fetch(\n config: S3Config,\n request: Request,\n): Promise<Response> {\n const base =\n request.key === undefined\n ? bucketUrl(config)\n : objectUrl(config, request.key);\n const url = new URL(base);\n for (const [k, v] of Object.entries(request.query ?? {}))\n url.searchParams.set(k, v);\n let headers = { ...config.headers, ...request.headers };\n if (config.credentials) {\n headers = signRequest({\n method: request.method,\n url: url.toString(),\n region: config.region,\n credentials: config.credentials,\n headers,\n payloadHash:\n request.body === undefined ? EMPTY_SHA256 : sha256Hex(request.body),\n });\n }\n return fetch(url, {\n method: request.method,\n headers,\n body: request.body as BodyInit | undefined,\n });\n}\n\n/** Null when the object does not exist. The ETag is what a conditional put needs. */\nexport async function getJson<T>(\n config: S3Config,\n key: string,\n): Promise<{ data: T; etag: string | null } | null> {\n const res = await s3Fetch(config, { method: \"GET\", key });\n if (res.status === 404 || (res.status === 403 && !config.credentials))\n return null;\n const body = await res.text();\n if (!res.ok) throw new S3Error(res.status, key, body);\n try {\n return { data: JSON.parse(body) as T, etag: res.headers.get(\"etag\") };\n } catch {\n throw new Error(notJsonMessage(objectUrl(config, key), res, body));\n }\n}\n\n/**\n * Something in front of the bucket answered instead of it: most often a private tunnel or proxy\n * sending a login page. Say what came back and from where, instead of a bare JSON parse error.\n */\nexport function notJsonMessage(\n url: string,\n res: Response,\n body: string,\n): string {\n const type = res.headers.get(\"content-type\")?.split(\";\")[0];\n const got = type || JSON.stringify(body.trim().slice(0, 80));\n const redirected =\n res.url && res.url !== url ? `, redirected to ${res.url}` : \"\";\n return `Expected JSON from ${url} but got ${got} (status ${res.status}${redirected}). Is the URL public, or does it need a read key or headers?`;\n}\n\n/**\n * False when a condition failed (someone else wrote first), so the caller can read again and\n * retry. `etag: null` means \"only if it does not exist yet\".\n */\nexport async function putObject(\n config: S3Config,\n key: string,\n body: Uint8Array | string,\n options: { contentType?: string; etag?: string | null } = {},\n): Promise<boolean> {\n const headers: Record<string, string> = {\n \"content-type\": options.contentType ?? \"application/octet-stream\",\n };\n if (options.etag !== undefined) {\n if (options.etag === null) headers[\"if-none-match\"] = \"*\";\n else headers[\"if-match\"] = options.etag;\n }\n const res = await s3Fetch(config, { method: \"PUT\", key, body, headers });\n if (res.status === 412 || res.status === 409) return false;\n if (!res.ok) throw new S3Error(res.status, key, await res.text());\n return true;\n}\n\nexport async function exists(config: S3Config, key: string): Promise<boolean> {\n const res = await s3Fetch(config, { method: \"HEAD\", key });\n if (res.status === 404) return false;\n if (!res.ok) throw new S3Error(res.status, key, \"\");\n return true;\n}\n\nexport async function deleteObject(\n config: S3Config,\n key: string,\n): Promise<void> {\n const res = await s3Fetch(config, { method: \"DELETE\", key });\n if (!res.ok && res.status !== 404)\n throw new S3Error(res.status, key, await res.text());\n}\n\nconst unescapeXml = (s: string) =>\n s\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/&/g, \"&\");\n\n/** Every key under the prefix (ListObjectsV2, following continuation tokens). */\nexport async function listKeys(\n config: S3Config,\n prefix: string,\n): Promise<string[]> {\n const keys: string[] = [];\n const strip = config.prefix ? config.prefix.length + 1 : 0;\n let token: string | undefined;\n do {\n const query: Record<string, string> = {\n \"list-type\": \"2\",\n prefix: withPrefix(config, prefix),\n };\n if (token) query[\"continuation-token\"] = token;\n const res = await s3Fetch(config, { method: \"GET\", query });\n const xml = await res.text();\n if (!res.ok) throw new S3Error(res.status, prefix, xml);\n for (const match of xml.matchAll(/<Key>([^<]*)<\\/Key>/g))\n keys.push(unescapeXml(match[1]).slice(strip));\n token = /<IsTruncated>true<\\/IsTruncated>/.test(xml)\n ? unescapeXml(/<NextContinuationToken>([^<]*)</.exec(xml)?.[1] ?? \"\")\n : undefined;\n } while (token);\n return keys;\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Platform } from "../../utils/previews/layout.util";
|
|
2
|
+
import { type RegisteredServer } from "../../constants/dev-preview-servers/dev-preview-servers.const";
|
|
3
|
+
import { type AppConfig } from "../../utils/config/expo-config.util";
|
|
4
|
+
/**
|
|
5
|
+
* The first port in the range nothing listens on and no other `start` has claimed. A claim counts
|
|
6
|
+
* because Metro takes a few seconds to bind, and two starts close together would pick the same port.
|
|
7
|
+
*/
|
|
8
|
+
export declare function freePort([from, to]: [number, number], claimed?: Set<number>): Promise<number>;
|
|
9
|
+
/** What the registry is told; the keys are made the way `publish` makes them. */
|
|
10
|
+
export declare function registration(input: {
|
|
11
|
+
config: AppConfig & {
|
|
12
|
+
slug?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
ios?: {
|
|
15
|
+
bundleIdentifier?: string;
|
|
16
|
+
};
|
|
17
|
+
android?: {
|
|
18
|
+
package?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
port: number;
|
|
22
|
+
branch: string;
|
|
23
|
+
worktree: string;
|
|
24
|
+
/** What the app's list shows. Default: the branch's readable title, or the app's name. */
|
|
25
|
+
title?: string;
|
|
26
|
+
fingerprints?: Partial<Record<Platform, string>>;
|
|
27
|
+
}): RegisteredServer;
|
|
28
|
+
export type StartOptions = {
|
|
29
|
+
projectRoot: string;
|
|
30
|
+
config: AppConfig & {
|
|
31
|
+
slug?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
};
|
|
34
|
+
branch: string;
|
|
35
|
+
worktree: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
ports: [number, number];
|
|
38
|
+
registryPort: number;
|
|
39
|
+
expoArgs: string[];
|
|
40
|
+
bin: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Runs `expo start` on a free port and announces it, so the app's list shows it. The announcement
|
|
44
|
+
* goes when Metro stops: on exit, on Ctrl+C, and through the registry's heartbeat timeout when the
|
|
45
|
+
* process is killed outright.
|
|
46
|
+
*/
|
|
47
|
+
export declare function start(options: StartOptions): Promise<void>;
|
|
48
|
+
//# sourceMappingURL=start.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.service.d.ts","sourceRoot":"","sources":["../../../src/services/start/start.service.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,+DAA+D,CAAC;AAEvE,OAAO,EACL,KAAK,SAAS,EAGf,MAAM,qCAAqC,CAAC;AAa7C;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,GAAE,GAAG,CAAC,MAAM,CAAa,GAC/B,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,MAAM,EAAE,SAAS,GAAG;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE;YAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACpC,OAAO,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAChC,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD,GAAG,gBAAgB,CA2BnB;AAyDD,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,iBAoEhD"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { PLATFORMS, compatibilityKey, } from "../../utils/previews/layout.util";
|
|
5
|
+
import { HEARTBEAT_MS, } from "../../constants/dev-preview-servers/dev-preview-servers.const";
|
|
6
|
+
import { previewTitle } from "../../utils/previews/preview-title.util";
|
|
7
|
+
import { appVersionOf, compatibilityOf, } from "../../utils/config/expo-config.util";
|
|
8
|
+
import { fingerprint } from "../cli/project.service";
|
|
9
|
+
const log = (message) => process.stdout.write(`preview: ${message}\n`);
|
|
10
|
+
const portIsFree = (port) => new Promise((resolve) => {
|
|
11
|
+
const probe = createServer();
|
|
12
|
+
probe.once("error", () => resolve(false));
|
|
13
|
+
probe.once("listening", () => probe.close(() => resolve(true)));
|
|
14
|
+
probe.listen(port);
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* The first port in the range nothing listens on and no other `start` has claimed. A claim counts
|
|
18
|
+
* because Metro takes a few seconds to bind, and two starts close together would pick the same port.
|
|
19
|
+
*/
|
|
20
|
+
export async function freePort([from, to], claimed = new Set()) {
|
|
21
|
+
for (let port = from; port <= to; port++) {
|
|
22
|
+
if (claimed.has(port))
|
|
23
|
+
continue;
|
|
24
|
+
if (await portIsFree(port))
|
|
25
|
+
return port;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`No free port between ${from} and ${to}; pass --ports <from>-<to>.`);
|
|
28
|
+
}
|
|
29
|
+
/** What the registry is told; the keys are made the way `publish` makes them. */
|
|
30
|
+
export function registration(input) {
|
|
31
|
+
const { config } = input;
|
|
32
|
+
const compatibility = compatibilityOf(config);
|
|
33
|
+
const keys = {};
|
|
34
|
+
for (const platform of PLATFORMS) {
|
|
35
|
+
const value = compatibility === "appVersion"
|
|
36
|
+
? appVersionOf(config, platform)
|
|
37
|
+
: input.fingerprints?.[platform];
|
|
38
|
+
if (value)
|
|
39
|
+
keys[platform] = compatibilityKey(compatibility, value);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id: `${input.port}-${randomUUID().slice(0, 8)}`,
|
|
43
|
+
port: input.port,
|
|
44
|
+
title: input.title ||
|
|
45
|
+
(input.branch ? previewTitle(input.branch) : (config.name ?? "")),
|
|
46
|
+
branch: input.branch,
|
|
47
|
+
worktree: input.worktree,
|
|
48
|
+
slug: config.slug ?? "",
|
|
49
|
+
name: config.name ?? "",
|
|
50
|
+
iosBundleIdentifier: config.ios?.bundleIdentifier,
|
|
51
|
+
androidPackage: config.android?.package,
|
|
52
|
+
compatibility,
|
|
53
|
+
keys,
|
|
54
|
+
startedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const registryUp = async (registry) => {
|
|
58
|
+
try {
|
|
59
|
+
return (await fetch(`${registry}/servers`, { signal: AbortSignal.timeout(500) })).ok;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/** Starts the registry in the background when none answers, and waits for it. */
|
|
66
|
+
async function ensureRegistry(registryPort, bin) {
|
|
67
|
+
const registry = `http://127.0.0.1:${registryPort}`;
|
|
68
|
+
if (await registryUp(registry))
|
|
69
|
+
return registry;
|
|
70
|
+
spawn(process.execPath, [bin, "registry", "--port", String(registryPort)], {
|
|
71
|
+
detached: true,
|
|
72
|
+
stdio: "ignore",
|
|
73
|
+
}).unref();
|
|
74
|
+
for (let i = 0; i < 30; i++) {
|
|
75
|
+
if (await registryUp(registry))
|
|
76
|
+
return registry;
|
|
77
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`The registry did not start on port ${registryPort}.`);
|
|
80
|
+
}
|
|
81
|
+
/** Forwards the ports to every attached Android device, when adb is there; quiet otherwise. */
|
|
82
|
+
function adbReverse(ports) {
|
|
83
|
+
let devices;
|
|
84
|
+
try {
|
|
85
|
+
devices = execFileSync("adb", ["devices"], {
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
88
|
+
})
|
|
89
|
+
.split("\n")
|
|
90
|
+
.slice(1)
|
|
91
|
+
.filter((line) => line.endsWith("\tdevice"))
|
|
92
|
+
.map((line) => line.split("\t")[0]);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const serial of devices) {
|
|
98
|
+
for (const port of ports) {
|
|
99
|
+
try {
|
|
100
|
+
execFileSync("adb", ["-s", serial, "reverse", `tcp:${port}`, `tcp:${port}`], { stdio: "ignore" });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A device going away mid-loop is not an error worth stopping Metro for.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Runs `expo start` on a free port and announces it, so the app's list shows it. The announcement
|
|
110
|
+
* goes when Metro stops: on exit, on Ctrl+C, and through the registry's heartbeat timeout when the
|
|
111
|
+
* process is killed outright.
|
|
112
|
+
*/
|
|
113
|
+
export async function start(options) {
|
|
114
|
+
const registry = await ensureRegistry(options.registryPort, options.bin);
|
|
115
|
+
const claimed = await fetch(`${registry}/servers`)
|
|
116
|
+
.then((res) => res.json())
|
|
117
|
+
.then(({ servers }) => new Set(servers.map((s) => s.port)))
|
|
118
|
+
.catch(() => new Set());
|
|
119
|
+
const port = await freePort(options.ports, claimed);
|
|
120
|
+
const fingerprints = {};
|
|
121
|
+
if (compatibilityOf(options.config) === "fingerprint") {
|
|
122
|
+
for (const platform of PLATFORMS) {
|
|
123
|
+
fingerprints[platform] = await fingerprint(options.projectRoot, platform);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const server = registration({ ...options, port, fingerprints });
|
|
127
|
+
adbReverse([options.registryPort, port]);
|
|
128
|
+
const post = () => fetch(`${registry}/servers`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: JSON.stringify(server),
|
|
131
|
+
});
|
|
132
|
+
// The registry exits once it has been empty for a while, which a slow start (adb, fingerprint)
|
|
133
|
+
// can outlast, and it may be killed: start it again and register in that case.
|
|
134
|
+
const register = () => post()
|
|
135
|
+
.catch(() => ensureRegistry(options.registryPort, options.bin).then(post))
|
|
136
|
+
.catch(() => {
|
|
137
|
+
// Still down; the next heartbeat tries again.
|
|
138
|
+
});
|
|
139
|
+
const unregister = () => fetch(`${registry}/servers/${encodeURIComponent(server.id)}`, {
|
|
140
|
+
method: "DELETE",
|
|
141
|
+
}).catch(() => undefined);
|
|
142
|
+
await register();
|
|
143
|
+
log(`${server.title || server.name} on port ${port}, announced on ${registry}`);
|
|
144
|
+
let beats = 0;
|
|
145
|
+
const heartbeat = setInterval(() => {
|
|
146
|
+
register();
|
|
147
|
+
// A restarted emulator forgets its reverse rules; renew them now and then.
|
|
148
|
+
if (++beats % 6 === 0)
|
|
149
|
+
adbReverse([options.registryPort, port]);
|
|
150
|
+
}, HEARTBEAT_MS);
|
|
151
|
+
const child = spawn("npx", ["expo", "start", "--port", String(port), ...options.expoArgs], {
|
|
152
|
+
cwd: options.projectRoot,
|
|
153
|
+
stdio: "inherit",
|
|
154
|
+
});
|
|
155
|
+
let stopping = false;
|
|
156
|
+
const stop = async (code) => {
|
|
157
|
+
if (stopping)
|
|
158
|
+
return;
|
|
159
|
+
stopping = true;
|
|
160
|
+
clearInterval(heartbeat);
|
|
161
|
+
await unregister();
|
|
162
|
+
process.exit(code);
|
|
163
|
+
};
|
|
164
|
+
child.on("exit", (code) => stop(code ?? 0));
|
|
165
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
166
|
+
process.on(signal, () => {
|
|
167
|
+
child.kill(signal);
|
|
168
|
+
stop(0);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=start.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.service.js","sourceRoot":"","sources":["../../../src/services/start/start.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync, spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { createServer } from \"node:net\";\n\nimport {\n PLATFORMS,\n compatibilityKey,\n type Platform,\n} from \"../../utils/previews/layout.util\";\nimport {\n HEARTBEAT_MS,\n type RegisteredServer,\n} from \"../../constants/dev-preview-servers/dev-preview-servers.const\";\nimport { previewTitle } from \"../../utils/previews/preview-title.util\";\nimport {\n type AppConfig,\n appVersionOf,\n compatibilityOf,\n} from \"../../utils/config/expo-config.util\";\nimport { fingerprint } from \"../cli/project.service\";\n\nconst log = (message: string) => process.stdout.write(`preview: ${message}\\n`);\n\nconst portIsFree = (port: number) =>\n new Promise<boolean>((resolve) => {\n const probe = createServer();\n probe.once(\"error\", () => resolve(false));\n probe.once(\"listening\", () => probe.close(() => resolve(true)));\n probe.listen(port);\n });\n\n/**\n * The first port in the range nothing listens on and no other `start` has claimed. A claim counts\n * because Metro takes a few seconds to bind, and two starts close together would pick the same port.\n */\nexport async function freePort(\n [from, to]: [number, number],\n claimed: Set<number> = new Set(),\n): Promise<number> {\n for (let port = from; port <= to; port++) {\n if (claimed.has(port)) continue;\n if (await portIsFree(port)) return port;\n }\n throw new Error(\n `No free port between ${from} and ${to}; pass --ports <from>-<to>.`,\n );\n}\n\n/** What the registry is told; the keys are made the way `publish` makes them. */\nexport function registration(input: {\n config: AppConfig & {\n slug?: string;\n name?: string;\n ios?: { bundleIdentifier?: string };\n android?: { package?: string };\n };\n port: number;\n branch: string;\n worktree: string;\n /** What the app's list shows. Default: the branch's readable title, or the app's name. */\n title?: string;\n fingerprints?: Partial<Record<Platform, string>>;\n}): RegisteredServer {\n const { config } = input;\n const compatibility = compatibilityOf(config);\n const keys: Partial<Record<Platform, string>> = {};\n for (const platform of PLATFORMS) {\n const value =\n compatibility === \"appVersion\"\n ? appVersionOf(config, platform)\n : input.fingerprints?.[platform];\n if (value) keys[platform] = compatibilityKey(compatibility, value);\n }\n return {\n id: `${input.port}-${randomUUID().slice(0, 8)}`,\n port: input.port,\n title:\n input.title ||\n (input.branch ? previewTitle(input.branch) : (config.name ?? \"\")),\n branch: input.branch,\n worktree: input.worktree,\n slug: config.slug ?? \"\",\n name: config.name ?? \"\",\n iosBundleIdentifier: config.ios?.bundleIdentifier,\n androidPackage: config.android?.package,\n compatibility,\n keys,\n startedAt: new Date().toISOString(),\n };\n}\n\nconst registryUp = async (registry: string) => {\n try {\n return (\n await fetch(`${registry}/servers`, { signal: AbortSignal.timeout(500) })\n ).ok;\n } catch {\n return false;\n }\n};\n\n/** Starts the registry in the background when none answers, and waits for it. */\nasync function ensureRegistry(registryPort: number, bin: string) {\n const registry = `http://127.0.0.1:${registryPort}`;\n if (await registryUp(registry)) return registry;\n spawn(process.execPath, [bin, \"registry\", \"--port\", String(registryPort)], {\n detached: true,\n stdio: \"ignore\",\n }).unref();\n for (let i = 0; i < 30; i++) {\n if (await registryUp(registry)) return registry;\n await new Promise((r) => setTimeout(r, 100));\n }\n throw new Error(`The registry did not start on port ${registryPort}.`);\n}\n\n/** Forwards the ports to every attached Android device, when adb is there; quiet otherwise. */\nfunction adbReverse(ports: number[]) {\n let devices: string[];\n try {\n devices = execFileSync(\"adb\", [\"devices\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n })\n .split(\"\\n\")\n .slice(1)\n .filter((line) => line.endsWith(\"\\tdevice\"))\n .map((line) => line.split(\"\\t\")[0]);\n } catch {\n return;\n }\n for (const serial of devices) {\n for (const port of ports) {\n try {\n execFileSync(\n \"adb\",\n [\"-s\", serial, \"reverse\", `tcp:${port}`, `tcp:${port}`],\n { stdio: \"ignore\" },\n );\n } catch {\n // A device going away mid-loop is not an error worth stopping Metro for.\n }\n }\n }\n}\n\nexport type StartOptions = {\n projectRoot: string;\n config: AppConfig & { slug?: string; name?: string };\n branch: string;\n worktree: string;\n title?: string;\n ports: [number, number];\n registryPort: number;\n expoArgs: string[];\n bin: string;\n};\n\n/**\n * Runs `expo start` on a free port and announces it, so the app's list shows it. The announcement\n * goes when Metro stops: on exit, on Ctrl+C, and through the registry's heartbeat timeout when the\n * process is killed outright.\n */\nexport async function start(options: StartOptions) {\n const registry = await ensureRegistry(options.registryPort, options.bin);\n const claimed = await fetch(`${registry}/servers`)\n .then((res) => res.json() as Promise<{ servers: RegisteredServer[] }>)\n .then(({ servers }) => new Set(servers.map((s) => s.port)))\n .catch(() => new Set<number>());\n const port = await freePort(options.ports, claimed);\n const fingerprints: Partial<Record<Platform, string>> = {};\n if (compatibilityOf(options.config) === \"fingerprint\") {\n for (const platform of PLATFORMS) {\n fingerprints[platform] = await fingerprint(options.projectRoot, platform);\n }\n }\n const server = registration({ ...options, port, fingerprints });\n adbReverse([options.registryPort, port]);\n\n const post = () =>\n fetch(`${registry}/servers`, {\n method: \"POST\",\n body: JSON.stringify(server),\n });\n // The registry exits once it has been empty for a while, which a slow start (adb, fingerprint)\n // can outlast, and it may be killed: start it again and register in that case.\n const register = () =>\n post()\n .catch(() => ensureRegistry(options.registryPort, options.bin).then(post))\n .catch(() => {\n // Still down; the next heartbeat tries again.\n });\n const unregister = () =>\n fetch(`${registry}/servers/${encodeURIComponent(server.id)}`, {\n method: \"DELETE\",\n }).catch(() => undefined);\n\n await register();\n log(\n `${server.title || server.name} on port ${port}, announced on ${registry}`,\n );\n let beats = 0;\n const heartbeat = setInterval(() => {\n register();\n // A restarted emulator forgets its reverse rules; renew them now and then.\n if (++beats % 6 === 0) adbReverse([options.registryPort, port]);\n }, HEARTBEAT_MS);\n\n const child = spawn(\n \"npx\",\n [\"expo\", \"start\", \"--port\", String(port), ...options.expoArgs],\n {\n cwd: options.projectRoot,\n stdio: \"inherit\",\n },\n );\n let stopping = false;\n const stop = async (code: number) => {\n if (stopping) return;\n stopping = true;\n clearInterval(heartbeat);\n await unregister();\n process.exit(code);\n };\n child.on(\"exit\", (code) => stop(code ?? 0));\n for (const signal of [\"SIGINT\", \"SIGTERM\", \"SIGHUP\"] as const) {\n process.on(signal, () => {\n child.kill(signal);\n stop(0);\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DevPreviewServer } from "../../utils/dev-preview-servers/merge-servers.util";
|
|
2
|
+
export type DevPreviewServersState = {
|
|
3
|
+
servers: DevPreviewServer[];
|
|
4
|
+
/** The bundle URL of the dev preview server the next start uses, or null. */
|
|
5
|
+
activeUrl: string | null;
|
|
6
|
+
error: string | null;
|
|
7
|
+
};
|
|
8
|
+
export declare const devPreviewServersStore: {
|
|
9
|
+
getState: () => DevPreviewServersState;
|
|
10
|
+
subscribe(listener: () => void): () => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Looks every 2 s while something watches (the list is on screen), and stops when nothing does.
|
|
13
|
+
* Debug builds only; returns a function that stops watching.
|
|
14
|
+
*/
|
|
15
|
+
watch(): () => void;
|
|
16
|
+
refresh: () => Promise<void>;
|
|
17
|
+
/** Restarts the app on a dev preview server, or back on what it ran before with null. */
|
|
18
|
+
switchTo(server: DevPreviewServer | null): void;
|
|
19
|
+
};
|
|
20
|
+
export declare const useDevPreviewServersState: () => DevPreviewServersState;
|
|
21
|
+
//# sourceMappingURL=dev-preview-servers.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-preview-servers.store.d.ts","sourceRoot":"","sources":["../../../src/stores/dev-preview-servers/dev-preview-servers.store.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,oDAAoD,CAAC;AAG5D,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AA+CF,eAAO,MAAM,sBAAsB;;wBAEb,MAAM,IAAI;IAK9B;;;OAGG;aACM,MAAM,IAAI;;IAkBnB,yFAAyF;qBACxE,gBAAgB,GAAG,IAAI;CAUzC,CAAC;AAEF,eAAO,MAAM,yBAAyB,8BAInC,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { buildKey, buildSlug } from "../../services/native/build-info.service";
|
|
3
|
+
import { currentPlatform } from "../../utils/config/config.util";
|
|
4
|
+
import { discover } from "../../services/dev-preview-servers/discover.service";
|
|
5
|
+
import { mergeServers, } from "../../utils/dev-preview-servers/merge-servers.util";
|
|
6
|
+
import { nativeModule } from "../../services/native/native.service";
|
|
7
|
+
let state = {
|
|
8
|
+
servers: [],
|
|
9
|
+
activeUrl: null,
|
|
10
|
+
error: null,
|
|
11
|
+
};
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const set = (patch) => {
|
|
14
|
+
state = { ...state, ...patch };
|
|
15
|
+
listeners.forEach((listener) => listener());
|
|
16
|
+
};
|
|
17
|
+
const POLL_MS = 2000;
|
|
18
|
+
let watchers = 0;
|
|
19
|
+
let timer = null;
|
|
20
|
+
let polling = false;
|
|
21
|
+
async function poll() {
|
|
22
|
+
const platform = currentPlatform();
|
|
23
|
+
const appSlug = buildSlug();
|
|
24
|
+
// A round can outlast the interval on a slow host; overlapping rounds would land out of order.
|
|
25
|
+
if (!platform || !appSlug || polling)
|
|
26
|
+
return;
|
|
27
|
+
polling = true;
|
|
28
|
+
try {
|
|
29
|
+
const found = await discover(platform);
|
|
30
|
+
set({
|
|
31
|
+
servers: mergeServers({
|
|
32
|
+
...found,
|
|
33
|
+
appSlug,
|
|
34
|
+
buildKey: buildKey(),
|
|
35
|
+
platform,
|
|
36
|
+
}),
|
|
37
|
+
activeUrl: nativeModule()?.state().localServer ?? null,
|
|
38
|
+
error: null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
set({ error: error instanceof Error ? error.message : String(error) });
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
polling = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
49
|
+
export const devPreviewServersStore = {
|
|
50
|
+
getState: () => state,
|
|
51
|
+
subscribe(listener) {
|
|
52
|
+
listeners.add(listener);
|
|
53
|
+
return () => listeners.delete(listener);
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Looks every 2 s while something watches (the list is on screen), and stops when nothing does.
|
|
57
|
+
* Debug builds only; returns a function that stops watching.
|
|
58
|
+
*/
|
|
59
|
+
watch() {
|
|
60
|
+
if (!__DEV__)
|
|
61
|
+
return () => undefined;
|
|
62
|
+
watchers++;
|
|
63
|
+
if (!timer) {
|
|
64
|
+
poll();
|
|
65
|
+
timer = setInterval(poll, POLL_MS);
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
watchers--;
|
|
69
|
+
if (watchers === 0 && timer) {
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
timer = null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
refresh: () => poll(),
|
|
76
|
+
/** Restarts the app on a dev preview server, or back on what it ran before with null. */
|
|
77
|
+
switchTo(server) {
|
|
78
|
+
const native = nativeModule();
|
|
79
|
+
if (!native)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
native.selectLocalServer(server?.url ?? null, server?.hostPort ?? null);
|
|
83
|
+
native.reload();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
set({ error: message(error) });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export const useDevPreviewServersState = () => useSyncExternalStore(devPreviewServersStore.subscribe, devPreviewServersStore.getState);
|
|
91
|
+
//# sourceMappingURL=dev-preview-servers.store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-preview-servers.store.js","sourceRoot":"","sources":["../../../src/stores/dev-preview-servers/dev-preview-servers.store.ts"],"names":[],"mappings":"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","sourcesContent":["import { useSyncExternalStore } from \"react\";\n\nimport { buildKey, buildSlug } from \"../../services/native/build-info.service\";\nimport { currentPlatform } from \"../../utils/config/config.util\";\nimport { discover } from \"../../services/dev-preview-servers/discover.service\";\nimport {\n type DevPreviewServer,\n mergeServers,\n} from \"../../utils/dev-preview-servers/merge-servers.util\";\nimport { nativeModule } from \"../../services/native/native.service\";\n\nexport type DevPreviewServersState = {\n servers: DevPreviewServer[];\n /** The bundle URL of the dev preview server the next start uses, or null. */\n activeUrl: string | null;\n error: string | null;\n};\n\nlet state: DevPreviewServersState = {\n servers: [],\n activeUrl: null,\n error: null,\n};\nconst listeners = new Set<() => void>();\nconst set = (patch: Partial<DevPreviewServersState>) => {\n state = { ...state, ...patch };\n listeners.forEach((listener) => listener());\n};\n\nconst POLL_MS = 2000;\nlet watchers = 0;\nlet timer: ReturnType<typeof setInterval> | null = null;\n\nlet polling = false;\n\nasync function poll() {\n const platform = currentPlatform();\n const appSlug = buildSlug();\n // A round can outlast the interval on a slow host; overlapping rounds would land out of order.\n if (!platform || !appSlug || polling) return;\n polling = true;\n try {\n const found = await discover(platform);\n set({\n servers: mergeServers({\n ...found,\n appSlug,\n buildKey: buildKey(),\n platform,\n }),\n activeUrl: nativeModule()?.state().localServer ?? null,\n error: null,\n });\n } catch (error) {\n set({ error: error instanceof Error ? error.message : String(error) });\n } finally {\n polling = false;\n }\n}\n\nconst message = (error: unknown) =>\n error instanceof Error ? error.message : String(error);\n\nexport const devPreviewServersStore = {\n getState: () => state,\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n /**\n * Looks every 2 s while something watches (the list is on screen), and stops when nothing does.\n * Debug builds only; returns a function that stops watching.\n */\n watch(): () => void {\n if (!__DEV__) return () => undefined;\n watchers++;\n if (!timer) {\n poll();\n timer = setInterval(poll, POLL_MS);\n }\n return () => {\n watchers--;\n if (watchers === 0 && timer) {\n clearInterval(timer);\n timer = null;\n }\n };\n },\n\n refresh: () => poll(),\n\n /** Restarts the app on a dev preview server, or back on what it ran before with null. */\n switchTo(server: DevPreviewServer | null) {\n const native = nativeModule();\n if (!native) return;\n try {\n native.selectLocalServer(server?.url ?? null, server?.hostPort ?? null);\n native.reload();\n } catch (error) {\n set({ error: message(error) });\n }\n },\n};\n\nexport const useDevPreviewServersState = () =>\n useSyncExternalStore(\n devPreviewServersStore.subscribe,\n devPreviewServersStore.getState,\n );\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PreviewConfig } from "../../utils/config/config.util";
|
|
2
|
+
import type { IndexEntry } from "../../utils/previews/layout.util";
|
|
3
|
+
import { LocalPreview } from "../../services/previews/local-previews.service";
|
|
4
|
+
export type PreviewsState = {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
/** This build's key; downloads made for any other are incompatible. */
|
|
8
|
+
buildKey: string | null;
|
|
9
|
+
/** What the bucket lists for this build. Null until it has answered once. */
|
|
10
|
+
remote: IndexEntry[] | null;
|
|
11
|
+
local: LocalPreview[];
|
|
12
|
+
/** Download progress by id, from 0 to 1. */
|
|
13
|
+
downloading: Record<string, number>;
|
|
14
|
+
/** The preview the next start loads, or null for the base bundle. */
|
|
15
|
+
activeId: string | null;
|
|
16
|
+
/** Previews that stopped before they finished starting, so the base bundle loaded instead. */
|
|
17
|
+
broken: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare const previewsStore: {
|
|
20
|
+
getState: () => PreviewsState;
|
|
21
|
+
subscribe(listener: () => void): () => boolean;
|
|
22
|
+
/** Reads the downloads on the device, then asks the bucket for this build's list. */
|
|
23
|
+
refresh(config: PreviewConfig): Promise<void>;
|
|
24
|
+
download(config: PreviewConfig, id: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Restarts into a preview, downloading it first when it is not on the device, or into the base
|
|
27
|
+
* bundle with null. A preview with an update keeps its downloaded copy until QA updates it.
|
|
28
|
+
*/
|
|
29
|
+
switchTo(config: PreviewConfig, id: string | null): Promise<void>;
|
|
30
|
+
/** Deletes a download. If it is the one running, the app goes back to the base bundle first. */
|
|
31
|
+
remove(id: string): Promise<void>;
|
|
32
|
+
/** Deletes every download made for another native build. */
|
|
33
|
+
removeIncompatible(): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
export declare const usePreviewsState: () => PreviewsState;
|
|
36
|
+
//# sourceMappingURL=previews.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"previews.store.d.ts","sourceRoot":"","sources":["../../../src/stores/previews/previews.store.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EAGd,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAEnE,OAAO,EACL,YAAY,EAGb,MAAM,gDAAgD,CAAC;AAQxD,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,6EAA6E;IAC7E,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,qEAAqE;IACrE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAgCF,eAAO,MAAM,aAAa;;wBAEJ,MAAM,IAAI;IAK9B,qFAAqF;oBAC/D,aAAa;qBAeZ,aAAa,MAAM,MAAM;IAsBhD;;;OAGG;qBACoB,aAAa,MAAM,MAAM,GAAG,IAAI;IA6BvD,gGAAgG;eAC/E,MAAM;IAgBvB,4DAA4D;;CAO7D,CAAC;AAEF,eAAO,MAAM,gBAAgB,qBAC0C,CAAC"}
|