@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
package/build/cli.cjs
ADDED
|
@@ -0,0 +1,1465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
var import_node_fs4 = require("node:fs");
|
|
5
|
+
var import_node_path5 = require("node:path");
|
|
6
|
+
var import_node_util2 = require("node:util");
|
|
7
|
+
|
|
8
|
+
// src/utils/previews/layout.util.ts
|
|
9
|
+
var SCHEMA = 1;
|
|
10
|
+
var PLATFORMS = ["android", "ios"];
|
|
11
|
+
var DEFAULT_COMPATIBILITY = "appVersion";
|
|
12
|
+
var isCompatibility = (value) => value === "appVersion" || value === "fingerprint";
|
|
13
|
+
var compatibilityKey = (compatibility, value) => `${compatibility === "appVersion" ? "version" : "fingerprint"}-${value.replace(/[^A-Za-z0-9._-]/g, "_")}`;
|
|
14
|
+
var BUNDLE_FILE = {
|
|
15
|
+
android: "index.android.bundle",
|
|
16
|
+
ios: "main.jsbundle"
|
|
17
|
+
};
|
|
18
|
+
var ROOT_INDEX_KEY = "index.json";
|
|
19
|
+
var fileKey = (sha256) => `files/${sha256}`;
|
|
20
|
+
var platformIndexKey = (platform, key) => `index/${platform}/${key}.json`;
|
|
21
|
+
var manifestKey = (platform, key, id) => `previews/${platform}/${key}/${id}.json`;
|
|
22
|
+
|
|
23
|
+
// src/services/publish/commands.service.ts
|
|
24
|
+
var import_node_crypto2 = require("node:crypto");
|
|
25
|
+
var import_node_fs2 = require("node:fs");
|
|
26
|
+
|
|
27
|
+
// ../../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/utils.js
|
|
28
|
+
function isBytes(a) {
|
|
29
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
30
|
+
}
|
|
31
|
+
var atitle = (title) => title ? `"${title}" ` : "";
|
|
32
|
+
function anumber(n, title = "") {
|
|
33
|
+
if (typeof n !== "number")
|
|
34
|
+
throw new TypeError(atitle(title) + "expected number, got " + typeof n);
|
|
35
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
36
|
+
throw new RangeError(atitle(title) + "expected integer >= 0, got " + n);
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
function abytes(value, length, title = "") {
|
|
40
|
+
if (isBytes(value) && (length === undefined || value.length === length))
|
|
41
|
+
return value;
|
|
42
|
+
if (length !== undefined)
|
|
43
|
+
anumber(length, "length");
|
|
44
|
+
const bytes = isBytes(value);
|
|
45
|
+
const ofLen = length !== undefined ? ` of length ${length}` : "";
|
|
46
|
+
const got = bytes ? `length=${value.length}` : `type=${typeof value}`;
|
|
47
|
+
const message = atitle(title) + "expected Uint8Array" + ofLen + ", got " + got;
|
|
48
|
+
if (!bytes)
|
|
49
|
+
throw new TypeError(message);
|
|
50
|
+
throw new RangeError(message);
|
|
51
|
+
}
|
|
52
|
+
function ahash(h) {
|
|
53
|
+
if (typeof h !== "function" || typeof h.create !== "function")
|
|
54
|
+
throw new TypeError("expected hash wrapped by utils.createHasher");
|
|
55
|
+
anumber(h.outputLen);
|
|
56
|
+
anumber(h.blockLen);
|
|
57
|
+
if (h.outputLen < 1 || h.blockLen < 1)
|
|
58
|
+
throw new Error("hash blockLen / outputLen must be >= 1");
|
|
59
|
+
}
|
|
60
|
+
var aobject = (value, label) => {
|
|
61
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
62
|
+
throw new TypeError((label === "object" ? "" : `"${label}" `) + "expected object, got type=" + typeof value);
|
|
63
|
+
};
|
|
64
|
+
var aopts = (value, label) => {
|
|
65
|
+
aobject(value, label);
|
|
66
|
+
const proto = Object.getPrototypeOf(value);
|
|
67
|
+
if (proto !== Object.prototype && proto !== null)
|
|
68
|
+
throw new TypeError(`"${label}" expected plain object`);
|
|
69
|
+
if (Object.hasOwn(value, "__proto__"))
|
|
70
|
+
throw new TypeError(`"${label}.__proto__" is not allowed`);
|
|
71
|
+
};
|
|
72
|
+
function aexists(instance, checkFinished = true) {
|
|
73
|
+
if (instance.destroyed)
|
|
74
|
+
throw new Error("hash was destroyed");
|
|
75
|
+
if (checkFinished && instance.finished)
|
|
76
|
+
throw new Error("digest() was already called");
|
|
77
|
+
}
|
|
78
|
+
function aoutput(out, instance) {
|
|
79
|
+
abytes(out, undefined, "output");
|
|
80
|
+
const min = instance.outputLen;
|
|
81
|
+
if (!(out.length >= min)) {
|
|
82
|
+
throw new RangeError('"output" expected length >= ' + min);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function clean(...arrays) {
|
|
86
|
+
for (let i = 0;i < arrays.length; i++) {
|
|
87
|
+
arrays[i].fill(0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function createView(arr) {
|
|
91
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
92
|
+
}
|
|
93
|
+
function rotr(word, shift) {
|
|
94
|
+
return word << 32 - shift | word >>> shift;
|
|
95
|
+
}
|
|
96
|
+
var hasHexBuiltin = /* @__PURE__ */ (() => typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function")();
|
|
97
|
+
var hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
98
|
+
function bytesToHex(bytes) {
|
|
99
|
+
abytes(bytes);
|
|
100
|
+
if (hasHexBuiltin)
|
|
101
|
+
return bytes.toHex();
|
|
102
|
+
let hex = "";
|
|
103
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
104
|
+
hex += hexes[bytes[i]];
|
|
105
|
+
}
|
|
106
|
+
return hex;
|
|
107
|
+
}
|
|
108
|
+
function utf8ToBytes(str) {
|
|
109
|
+
if (typeof str !== "string")
|
|
110
|
+
throw new TypeError("string expected");
|
|
111
|
+
const encoded = new TextEncoder().encode(str);
|
|
112
|
+
try {
|
|
113
|
+
return new Uint8Array(encoded);
|
|
114
|
+
} finally {
|
|
115
|
+
clean(encoded);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function checkOpts(defaults, opts, title = "opts") {
|
|
119
|
+
aopts(defaults, "defaults");
|
|
120
|
+
if (opts !== undefined)
|
|
121
|
+
aopts(opts, title);
|
|
122
|
+
const merged = Object.assign(Object.create(null), defaults, opts);
|
|
123
|
+
return merged;
|
|
124
|
+
}
|
|
125
|
+
function createHasher(hashCons, info = {}) {
|
|
126
|
+
if (typeof hashCons !== "function")
|
|
127
|
+
throw new TypeError('"hashCons" expected function, got type=' + typeof hashCons);
|
|
128
|
+
info = checkOpts({}, info, "info");
|
|
129
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
130
|
+
const tmp = hashCons(undefined);
|
|
131
|
+
hashC.outputLen = tmp.outputLen;
|
|
132
|
+
hashC.blockLen = tmp.blockLen;
|
|
133
|
+
hashC.canXOF = tmp.canXOF;
|
|
134
|
+
hashC.create = (opts) => hashCons(opts);
|
|
135
|
+
Object.assign(hashC, info);
|
|
136
|
+
return Object.freeze(hashC);
|
|
137
|
+
}
|
|
138
|
+
var oidNist = (suffix) => ({
|
|
139
|
+
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ../../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/hmac.js
|
|
143
|
+
class _HMAC {
|
|
144
|
+
oHash;
|
|
145
|
+
iHash;
|
|
146
|
+
blockLen;
|
|
147
|
+
outputLen;
|
|
148
|
+
canXOF = false;
|
|
149
|
+
finished = false;
|
|
150
|
+
destroyed = false;
|
|
151
|
+
constructor(hash, key) {
|
|
152
|
+
ahash(hash);
|
|
153
|
+
abytes(key, undefined, "key");
|
|
154
|
+
this.iHash = hash.create();
|
|
155
|
+
if (typeof this.iHash.update !== "function")
|
|
156
|
+
throw new Error("expected Hash instance");
|
|
157
|
+
this.blockLen = this.iHash.blockLen;
|
|
158
|
+
this.outputLen = this.iHash.outputLen;
|
|
159
|
+
const blockLen = this.blockLen;
|
|
160
|
+
const pad = new Uint8Array(blockLen);
|
|
161
|
+
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
|
162
|
+
for (let i = 0;i < pad.length; i++)
|
|
163
|
+
pad[i] ^= 54;
|
|
164
|
+
this.iHash.update(pad);
|
|
165
|
+
this.oHash = hash.create();
|
|
166
|
+
for (let i = 0;i < pad.length; i++)
|
|
167
|
+
pad[i] ^= 54 ^ 92;
|
|
168
|
+
this.oHash.update(pad);
|
|
169
|
+
clean(pad);
|
|
170
|
+
}
|
|
171
|
+
update(buf) {
|
|
172
|
+
aexists(this);
|
|
173
|
+
this.iHash.update(buf);
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
digestInto(out) {
|
|
177
|
+
aexists(this);
|
|
178
|
+
aoutput(out, this);
|
|
179
|
+
this.finished = true;
|
|
180
|
+
const buf = out.subarray(0, this.outputLen);
|
|
181
|
+
this.iHash.digestInto(buf);
|
|
182
|
+
this.oHash.update(buf);
|
|
183
|
+
this.oHash.digestInto(buf);
|
|
184
|
+
this.destroy();
|
|
185
|
+
}
|
|
186
|
+
digest() {
|
|
187
|
+
const out = new Uint8Array(this.oHash.outputLen);
|
|
188
|
+
this.digestInto(out);
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
_cloneInto(to) {
|
|
192
|
+
to ||= Object.create(Object.getPrototypeOf(this), {});
|
|
193
|
+
const { oHash, iHash, finished, destroyed, blockLen, outputLen, canXOF } = this;
|
|
194
|
+
to = to;
|
|
195
|
+
to.finished = finished;
|
|
196
|
+
to.destroyed = destroyed;
|
|
197
|
+
to.blockLen = blockLen;
|
|
198
|
+
to.outputLen = outputLen;
|
|
199
|
+
to.canXOF = canXOF;
|
|
200
|
+
to.oHash = oHash._cloneInto(to.oHash);
|
|
201
|
+
to.iHash = iHash._cloneInto(to.iHash);
|
|
202
|
+
return to;
|
|
203
|
+
}
|
|
204
|
+
clone() {
|
|
205
|
+
return this._cloneInto();
|
|
206
|
+
}
|
|
207
|
+
destroy() {
|
|
208
|
+
this.destroyed = true;
|
|
209
|
+
this.oHash.destroy();
|
|
210
|
+
this.iHash.destroy();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
var hmac = /* @__PURE__ */ (() => {
|
|
214
|
+
const hmac_ = (hash, key, message) => new _HMAC(hash, key).update(message).digest();
|
|
215
|
+
hmac_.create = (hash, key) => new _HMAC(hash, key);
|
|
216
|
+
return hmac_;
|
|
217
|
+
})();
|
|
218
|
+
|
|
219
|
+
// ../../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/_u64.js
|
|
220
|
+
var fromNumH = (n) => n / 2 ** 32 | 0;
|
|
221
|
+
var fromNumL = (n) => n >>> 0;
|
|
222
|
+
function setU64FromNum(view, byteOffset, n, isLE) {
|
|
223
|
+
const h = fromNumH(n);
|
|
224
|
+
const l = fromNumL(n);
|
|
225
|
+
view.setUint32(byteOffset, isLE ? l : h, isLE);
|
|
226
|
+
view.setUint32(byteOffset + 4, isLE ? h : l, isLE);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ../../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/_md.js
|
|
230
|
+
function Chi(a, b, c) {
|
|
231
|
+
return a & b ^ ~a & c;
|
|
232
|
+
}
|
|
233
|
+
function Maj(a, b, c) {
|
|
234
|
+
return a & b ^ a & c ^ b & c;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class HashMD {
|
|
238
|
+
blockLen;
|
|
239
|
+
outputLen;
|
|
240
|
+
canXOF = false;
|
|
241
|
+
padOffset;
|
|
242
|
+
isLE;
|
|
243
|
+
buffer;
|
|
244
|
+
view;
|
|
245
|
+
finished = false;
|
|
246
|
+
length = 0;
|
|
247
|
+
pos = 0;
|
|
248
|
+
destroyed = false;
|
|
249
|
+
constructor(blockLen, outputLen, padOffset, isLE) {
|
|
250
|
+
this.blockLen = blockLen;
|
|
251
|
+
this.outputLen = outputLen;
|
|
252
|
+
this.padOffset = padOffset;
|
|
253
|
+
this.isLE = isLE;
|
|
254
|
+
this.buffer = new Uint8Array(blockLen);
|
|
255
|
+
this.view = createView(this.buffer);
|
|
256
|
+
}
|
|
257
|
+
update(data) {
|
|
258
|
+
aexists(this);
|
|
259
|
+
abytes(data);
|
|
260
|
+
const { view, buffer, blockLen } = this;
|
|
261
|
+
const len = data.length;
|
|
262
|
+
let processed = false;
|
|
263
|
+
for (let pos = 0;pos < len; ) {
|
|
264
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
265
|
+
if (take === blockLen) {
|
|
266
|
+
const dataView = createView(data);
|
|
267
|
+
for (;blockLen <= len - pos; pos += blockLen)
|
|
268
|
+
this.process(dataView, pos);
|
|
269
|
+
processed = true;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
buffer.set(pos === 0 && take === len ? data : data.subarray(pos, pos + take), this.pos);
|
|
273
|
+
this.pos += take;
|
|
274
|
+
pos += take;
|
|
275
|
+
if (this.pos === blockLen) {
|
|
276
|
+
this.process(view, 0);
|
|
277
|
+
this.pos = 0;
|
|
278
|
+
processed = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
this.length += data.length;
|
|
282
|
+
if (processed)
|
|
283
|
+
this.roundClean();
|
|
284
|
+
return this;
|
|
285
|
+
}
|
|
286
|
+
digestInto(out) {
|
|
287
|
+
aexists(this);
|
|
288
|
+
aoutput(out, this);
|
|
289
|
+
this.finished = true;
|
|
290
|
+
const { buffer, view, blockLen, isLE } = this;
|
|
291
|
+
let { pos } = this;
|
|
292
|
+
buffer[pos++] = 128;
|
|
293
|
+
buffer.fill(0, pos);
|
|
294
|
+
if (this.padOffset > blockLen - pos) {
|
|
295
|
+
this.process(view, 0);
|
|
296
|
+
buffer.fill(0);
|
|
297
|
+
}
|
|
298
|
+
setU64FromNum(view, blockLen - 8, this.length * 8, isLE);
|
|
299
|
+
this.process(view, 0);
|
|
300
|
+
this.roundClean();
|
|
301
|
+
const oview = out === buffer ? view : createView(out);
|
|
302
|
+
const len = this.outputLen;
|
|
303
|
+
const outLen = len / 4;
|
|
304
|
+
const state = this.get();
|
|
305
|
+
if (len % 4 || outLen > state.length)
|
|
306
|
+
throw new Error("invalid outputLen");
|
|
307
|
+
for (let i = 0;i < outLen; i++)
|
|
308
|
+
oview.setUint32(4 * i, state[i], isLE);
|
|
309
|
+
}
|
|
310
|
+
digest() {
|
|
311
|
+
const { buffer, outputLen } = this;
|
|
312
|
+
this.digestInto(buffer);
|
|
313
|
+
const res = buffer.slice(0, outputLen);
|
|
314
|
+
this.destroy();
|
|
315
|
+
return res;
|
|
316
|
+
}
|
|
317
|
+
_cloneIntoMeta(to) {
|
|
318
|
+
const { buffer, length, finished, destroyed, pos } = this;
|
|
319
|
+
to.destroyed = destroyed;
|
|
320
|
+
to.finished = finished;
|
|
321
|
+
to.length = length;
|
|
322
|
+
to.pos = pos;
|
|
323
|
+
if (pos)
|
|
324
|
+
to.buffer.set(buffer);
|
|
325
|
+
return to;
|
|
326
|
+
}
|
|
327
|
+
clone() {
|
|
328
|
+
return this._cloneInto();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
332
|
+
1779033703,
|
|
333
|
+
3144134277,
|
|
334
|
+
1013904242,
|
|
335
|
+
2773480762,
|
|
336
|
+
1359893119,
|
|
337
|
+
2600822924,
|
|
338
|
+
528734635,
|
|
339
|
+
1541459225
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
// ../../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/sha2.js
|
|
343
|
+
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
344
|
+
1116352408,
|
|
345
|
+
1899447441,
|
|
346
|
+
3049323471,
|
|
347
|
+
3921009573,
|
|
348
|
+
961987163,
|
|
349
|
+
1508970993,
|
|
350
|
+
2453635748,
|
|
351
|
+
2870763221,
|
|
352
|
+
3624381080,
|
|
353
|
+
310598401,
|
|
354
|
+
607225278,
|
|
355
|
+
1426881987,
|
|
356
|
+
1925078388,
|
|
357
|
+
2162078206,
|
|
358
|
+
2614888103,
|
|
359
|
+
3248222580,
|
|
360
|
+
3835390401,
|
|
361
|
+
4022224774,
|
|
362
|
+
264347078,
|
|
363
|
+
604807628,
|
|
364
|
+
770255983,
|
|
365
|
+
1249150122,
|
|
366
|
+
1555081692,
|
|
367
|
+
1996064986,
|
|
368
|
+
2554220882,
|
|
369
|
+
2821834349,
|
|
370
|
+
2952996808,
|
|
371
|
+
3210313671,
|
|
372
|
+
3336571891,
|
|
373
|
+
3584528711,
|
|
374
|
+
113926993,
|
|
375
|
+
338241895,
|
|
376
|
+
666307205,
|
|
377
|
+
773529912,
|
|
378
|
+
1294757372,
|
|
379
|
+
1396182291,
|
|
380
|
+
1695183700,
|
|
381
|
+
1986661051,
|
|
382
|
+
2177026350,
|
|
383
|
+
2456956037,
|
|
384
|
+
2730485921,
|
|
385
|
+
2820302411,
|
|
386
|
+
3259730800,
|
|
387
|
+
3345764771,
|
|
388
|
+
3516065817,
|
|
389
|
+
3600352804,
|
|
390
|
+
4094571909,
|
|
391
|
+
275423344,
|
|
392
|
+
430227734,
|
|
393
|
+
506948616,
|
|
394
|
+
659060556,
|
|
395
|
+
883997877,
|
|
396
|
+
958139571,
|
|
397
|
+
1322822218,
|
|
398
|
+
1537002063,
|
|
399
|
+
1747873779,
|
|
400
|
+
1955562222,
|
|
401
|
+
2024104815,
|
|
402
|
+
2227730452,
|
|
403
|
+
2361852424,
|
|
404
|
+
2428436474,
|
|
405
|
+
2756734187,
|
|
406
|
+
3204031479,
|
|
407
|
+
3329325298
|
|
408
|
+
]);
|
|
409
|
+
var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
410
|
+
|
|
411
|
+
class SHA2_32B extends HashMD {
|
|
412
|
+
A = 0;
|
|
413
|
+
B = 0;
|
|
414
|
+
C = 0;
|
|
415
|
+
D = 0;
|
|
416
|
+
E = 0;
|
|
417
|
+
F = 0;
|
|
418
|
+
G = 0;
|
|
419
|
+
H = 0;
|
|
420
|
+
constructor(outputLen, IV) {
|
|
421
|
+
super(64, outputLen, 8, false);
|
|
422
|
+
this.A = IV[0] | 0;
|
|
423
|
+
this.B = IV[1] | 0;
|
|
424
|
+
this.C = IV[2] | 0;
|
|
425
|
+
this.D = IV[3] | 0;
|
|
426
|
+
this.E = IV[4] | 0;
|
|
427
|
+
this.F = IV[5] | 0;
|
|
428
|
+
this.G = IV[6] | 0;
|
|
429
|
+
this.H = IV[7] | 0;
|
|
430
|
+
}
|
|
431
|
+
get() {
|
|
432
|
+
const { A, B, C, D, E, F, G, H } = this;
|
|
433
|
+
return [A, B, C, D, E, F, G, H];
|
|
434
|
+
}
|
|
435
|
+
set(A, B, C, D, E, F, G, H) {
|
|
436
|
+
this.A = A | 0;
|
|
437
|
+
this.B = B | 0;
|
|
438
|
+
this.C = C | 0;
|
|
439
|
+
this.D = D | 0;
|
|
440
|
+
this.E = E | 0;
|
|
441
|
+
this.F = F | 0;
|
|
442
|
+
this.G = G | 0;
|
|
443
|
+
this.H = H | 0;
|
|
444
|
+
}
|
|
445
|
+
_cloneInto(to) {
|
|
446
|
+
(to ||= new this.constructor).set(...this.get());
|
|
447
|
+
return this._cloneIntoMeta(to);
|
|
448
|
+
}
|
|
449
|
+
process(view, offset) {
|
|
450
|
+
for (let i = 0;i < 16; i++, offset += 4)
|
|
451
|
+
SHA256_W[i] = view.getUint32(offset, false);
|
|
452
|
+
for (let i = 16;i < 64; i++) {
|
|
453
|
+
const W15 = SHA256_W[i - 15];
|
|
454
|
+
const W2 = SHA256_W[i - 2];
|
|
455
|
+
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
456
|
+
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
457
|
+
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
458
|
+
}
|
|
459
|
+
let { A, B, C, D, E, F, G, H } = this;
|
|
460
|
+
for (let i = 0;i < 64; i++) {
|
|
461
|
+
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
462
|
+
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
463
|
+
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
|
464
|
+
const T2 = sigma0 + Maj(A, B, C) | 0;
|
|
465
|
+
H = G;
|
|
466
|
+
G = F;
|
|
467
|
+
F = E;
|
|
468
|
+
E = D + T1 | 0;
|
|
469
|
+
D = C;
|
|
470
|
+
C = B;
|
|
471
|
+
B = A;
|
|
472
|
+
A = T1 + T2 | 0;
|
|
473
|
+
}
|
|
474
|
+
A = A + this.A | 0;
|
|
475
|
+
B = B + this.B | 0;
|
|
476
|
+
C = C + this.C | 0;
|
|
477
|
+
D = D + this.D | 0;
|
|
478
|
+
E = E + this.E | 0;
|
|
479
|
+
F = F + this.F | 0;
|
|
480
|
+
G = G + this.G | 0;
|
|
481
|
+
H = H + this.H | 0;
|
|
482
|
+
this.set(A, B, C, D, E, F, G, H);
|
|
483
|
+
}
|
|
484
|
+
roundClean() {
|
|
485
|
+
clean(SHA256_W);
|
|
486
|
+
}
|
|
487
|
+
destroy() {
|
|
488
|
+
this.destroyed = true;
|
|
489
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
490
|
+
clean(this.buffer);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
class _SHA256 extends SHA2_32B {
|
|
495
|
+
constructor() {
|
|
496
|
+
super(32, SHA256_IV);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
var sha256 = /* @__PURE__ */ createHasher(() => new _SHA256, /* @__PURE__ */ oidNist(1));
|
|
500
|
+
|
|
501
|
+
// src/utils/s3/sigv4.util.ts
|
|
502
|
+
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
503
|
+
var sha256Hex = (data) => bytesToHex(sha256(typeof data === "string" ? utf8ToBytes(data) : data));
|
|
504
|
+
var hmacBytes = (key, data) => hmac(sha256, typeof key === "string" ? utf8ToBytes(key) : key, utf8ToBytes(data));
|
|
505
|
+
var encode = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
506
|
+
var canonicalPath = (pathname) => pathname.split("/").map((segment) => encode(decodeURIComponent(segment))).join("/") || "/";
|
|
507
|
+
var canonicalQuery = (search) => [...search.entries()].map(([key, value]) => [encode(key), encode(value)]).sort(([ak, av], [bk, bv]) => ak === bk ? av < bv ? -1 : 1 : ak < bk ? -1 : 1).map(([key, value]) => `${key}=${value}`).join("&");
|
|
508
|
+
var amzDate = (date) => date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
509
|
+
function signRequest(input) {
|
|
510
|
+
const url = new URL(input.url);
|
|
511
|
+
const service = input.service ?? "s3";
|
|
512
|
+
const payloadHash = input.payloadHash ?? EMPTY_SHA256;
|
|
513
|
+
const timestamp = amzDate(input.date ?? new Date);
|
|
514
|
+
const day = timestamp.slice(0, 8);
|
|
515
|
+
const headers = { ...input.headers };
|
|
516
|
+
headers["x-amz-date"] = timestamp;
|
|
517
|
+
if (input.addContentSha256 ?? true)
|
|
518
|
+
headers["x-amz-content-sha256"] = payloadHash;
|
|
519
|
+
if (input.credentials.sessionToken) {
|
|
520
|
+
headers["x-amz-security-token"] = input.credentials.sessionToken;
|
|
521
|
+
}
|
|
522
|
+
const signed = new Map;
|
|
523
|
+
signed.set("host", url.host);
|
|
524
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
525
|
+
signed.set(name.toLowerCase(), value.trim().replace(/\s+/g, " "));
|
|
526
|
+
}
|
|
527
|
+
const names = [...signed.keys()].sort();
|
|
528
|
+
const signedHeaders = names.join(";");
|
|
529
|
+
const canonicalRequest = [
|
|
530
|
+
input.method.toUpperCase(),
|
|
531
|
+
canonicalPath(url.pathname),
|
|
532
|
+
canonicalQuery(url.searchParams),
|
|
533
|
+
names.map((name) => `${name}:${signed.get(name)}
|
|
534
|
+
`).join(""),
|
|
535
|
+
signedHeaders,
|
|
536
|
+
payloadHash
|
|
537
|
+
].join(`
|
|
538
|
+
`);
|
|
539
|
+
const scope = `${day}/${input.region}/${service}/aws4_request`;
|
|
540
|
+
const stringToSign = [
|
|
541
|
+
"AWS4-HMAC-SHA256",
|
|
542
|
+
timestamp,
|
|
543
|
+
scope,
|
|
544
|
+
sha256Hex(canonicalRequest)
|
|
545
|
+
].join(`
|
|
546
|
+
`);
|
|
547
|
+
let key = hmacBytes(`AWS4${input.credentials.secretAccessKey}`, day);
|
|
548
|
+
key = hmacBytes(key, input.region);
|
|
549
|
+
key = hmacBytes(key, service);
|
|
550
|
+
key = hmacBytes(key, "aws4_request");
|
|
551
|
+
const signature = bytesToHex(hmacBytes(key, stringToSign));
|
|
552
|
+
headers.Authorization = `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
553
|
+
return headers;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/services/s3/s3.service.ts
|
|
557
|
+
var DEFAULT_REGION = "us-east-1";
|
|
558
|
+
var trimSlash = (s) => s.replace(/\/+$/, "");
|
|
559
|
+
var encodeKey = (key) => key.split("/").map(encodeURIComponent).join("/");
|
|
560
|
+
function parseBucketUrl(value) {
|
|
561
|
+
const match = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]+)([^?#]*)/i.exec(value.trim());
|
|
562
|
+
if (!match)
|
|
563
|
+
throw new Error(`${value} is not a URL: use <endpoint>/<bucket>`);
|
|
564
|
+
const [, scheme, host, path] = match;
|
|
565
|
+
const [bucket, ...rest] = path.split("/").filter(Boolean);
|
|
566
|
+
if (!bucket)
|
|
567
|
+
throw new Error(`${value} has no bucket: use <endpoint>/<bucket>`);
|
|
568
|
+
return {
|
|
569
|
+
endpoint: `${scheme.toLowerCase()}://${host}`,
|
|
570
|
+
bucket,
|
|
571
|
+
prefix: rest.length > 0 ? rest.join("/") : undefined
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
var withPrefix = (config, key) => config.prefix ? `${config.prefix}/${key}` : key;
|
|
575
|
+
var bucketUrl = (config) => config.bucket ? `${trimSlash(config.endpoint)}/${config.bucket}` : trimSlash(config.endpoint);
|
|
576
|
+
var objectUrl = (config, key) => `${bucketUrl(config)}/${encodeKey(withPrefix(config, key))}`;
|
|
577
|
+
|
|
578
|
+
class S3Error extends Error {
|
|
579
|
+
status;
|
|
580
|
+
key;
|
|
581
|
+
constructor(status, key, body) {
|
|
582
|
+
super(`S3 ${status} for ${key}: ${body.slice(0, 300)}`);
|
|
583
|
+
this.status = status;
|
|
584
|
+
this.key = key;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function s3Fetch(config, request) {
|
|
588
|
+
const base = request.key === undefined ? bucketUrl(config) : objectUrl(config, request.key);
|
|
589
|
+
const url = new URL(base);
|
|
590
|
+
for (const [k, v] of Object.entries(request.query ?? {}))
|
|
591
|
+
url.searchParams.set(k, v);
|
|
592
|
+
let headers = { ...config.headers, ...request.headers };
|
|
593
|
+
if (config.credentials) {
|
|
594
|
+
headers = signRequest({
|
|
595
|
+
method: request.method,
|
|
596
|
+
url: url.toString(),
|
|
597
|
+
region: config.region,
|
|
598
|
+
credentials: config.credentials,
|
|
599
|
+
headers,
|
|
600
|
+
payloadHash: request.body === undefined ? EMPTY_SHA256 : sha256Hex(request.body)
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return fetch(url, {
|
|
604
|
+
method: request.method,
|
|
605
|
+
headers,
|
|
606
|
+
body: request.body
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async function getJson(config, key) {
|
|
610
|
+
const res = await s3Fetch(config, { method: "GET", key });
|
|
611
|
+
if (res.status === 404 || res.status === 403 && !config.credentials)
|
|
612
|
+
return null;
|
|
613
|
+
const body = await res.text();
|
|
614
|
+
if (!res.ok)
|
|
615
|
+
throw new S3Error(res.status, key, body);
|
|
616
|
+
try {
|
|
617
|
+
return { data: JSON.parse(body), etag: res.headers.get("etag") };
|
|
618
|
+
} catch {
|
|
619
|
+
throw new Error(notJsonMessage(objectUrl(config, key), res, body));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function notJsonMessage(url, res, body) {
|
|
623
|
+
const type = res.headers.get("content-type")?.split(";")[0];
|
|
624
|
+
const got = type || JSON.stringify(body.trim().slice(0, 80));
|
|
625
|
+
const redirected = res.url && res.url !== url ? `, redirected to ${res.url}` : "";
|
|
626
|
+
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?`;
|
|
627
|
+
}
|
|
628
|
+
async function putObject(config, key, body, options = {}) {
|
|
629
|
+
const headers = {
|
|
630
|
+
"content-type": options.contentType ?? "application/octet-stream"
|
|
631
|
+
};
|
|
632
|
+
if (options.etag !== undefined) {
|
|
633
|
+
if (options.etag === null)
|
|
634
|
+
headers["if-none-match"] = "*";
|
|
635
|
+
else
|
|
636
|
+
headers["if-match"] = options.etag;
|
|
637
|
+
}
|
|
638
|
+
const res = await s3Fetch(config, { method: "PUT", key, body, headers });
|
|
639
|
+
if (res.status === 412 || res.status === 409)
|
|
640
|
+
return false;
|
|
641
|
+
if (!res.ok)
|
|
642
|
+
throw new S3Error(res.status, key, await res.text());
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
async function exists(config, key) {
|
|
646
|
+
const res = await s3Fetch(config, { method: "HEAD", key });
|
|
647
|
+
if (res.status === 404)
|
|
648
|
+
return false;
|
|
649
|
+
if (!res.ok)
|
|
650
|
+
throw new S3Error(res.status, key, "");
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
async function deleteObject(config, key) {
|
|
654
|
+
const res = await s3Fetch(config, { method: "DELETE", key });
|
|
655
|
+
if (!res.ok && res.status !== 404)
|
|
656
|
+
throw new S3Error(res.status, key, await res.text());
|
|
657
|
+
}
|
|
658
|
+
var unescapeXml = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
659
|
+
async function listKeys(config, prefix) {
|
|
660
|
+
const keys = [];
|
|
661
|
+
const strip = config.prefix ? config.prefix.length + 1 : 0;
|
|
662
|
+
let token;
|
|
663
|
+
do {
|
|
664
|
+
const query = {
|
|
665
|
+
"list-type": "2",
|
|
666
|
+
prefix: withPrefix(config, prefix)
|
|
667
|
+
};
|
|
668
|
+
if (token)
|
|
669
|
+
query["continuation-token"] = token;
|
|
670
|
+
const res = await s3Fetch(config, { method: "GET", query });
|
|
671
|
+
const xml = await res.text();
|
|
672
|
+
if (!res.ok)
|
|
673
|
+
throw new S3Error(res.status, prefix, xml);
|
|
674
|
+
for (const match of xml.matchAll(/<Key>([^<]*)<\/Key>/g))
|
|
675
|
+
keys.push(unescapeXml(match[1]).slice(strip));
|
|
676
|
+
token = /<IsTruncated>true<\/IsTruncated>/.test(xml) ? unescapeXml(/<NextContinuationToken>([^<]*)</.exec(xml)?.[1] ?? "") : undefined;
|
|
677
|
+
} while (token);
|
|
678
|
+
return keys;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/utils/previews/preview-title.util.ts
|
|
682
|
+
function previewTitle(branch) {
|
|
683
|
+
const words = branch.replace(/[-_/]+/g, " ").trim();
|
|
684
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/services/publish/bundle.service.ts
|
|
688
|
+
var import_node_child_process = require("node:child_process");
|
|
689
|
+
var import_node_crypto = require("node:crypto");
|
|
690
|
+
var import_node_fs = require("node:fs");
|
|
691
|
+
var import_node_os = require("node:os");
|
|
692
|
+
var import_node_path = require("node:path");
|
|
693
|
+
var walk = (dir) => import_node_fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(import_node_path.join(dir, entry.name)) : [import_node_path.join(dir, entry.name)]);
|
|
694
|
+
var HERMES_MAGIC = Buffer.from([
|
|
695
|
+
198,
|
|
696
|
+
31,
|
|
697
|
+
188,
|
|
698
|
+
3,
|
|
699
|
+
193,
|
|
700
|
+
3,
|
|
701
|
+
25,
|
|
702
|
+
31
|
|
703
|
+
]);
|
|
704
|
+
var hermesVersion = (bundle) => bundle.length >= 12 && bundle.subarray(0, 8).equals(HERMES_MAGIC) ? bundle.readUInt32LE(8) : null;
|
|
705
|
+
function buildBundle(projectRoot, platform, entryFile) {
|
|
706
|
+
const out = import_node_fs.mkdtempSync(import_node_path.join(import_node_os.tmpdir(), `preview-${platform}-`));
|
|
707
|
+
const args = [
|
|
708
|
+
"expo",
|
|
709
|
+
"export:embed",
|
|
710
|
+
"--platform",
|
|
711
|
+
platform,
|
|
712
|
+
"--dev",
|
|
713
|
+
"false",
|
|
714
|
+
"--bytecode",
|
|
715
|
+
"--bundle-output",
|
|
716
|
+
import_node_path.join(out, BUNDLE_FILE[platform]),
|
|
717
|
+
"--assets-dest",
|
|
718
|
+
out
|
|
719
|
+
];
|
|
720
|
+
if (entryFile)
|
|
721
|
+
args.push("--entry-file", entryFile);
|
|
722
|
+
import_node_child_process.execFileSync("npx", args, { cwd: projectRoot, stdio: "inherit" });
|
|
723
|
+
const files = walk(out).map((absolutePath) => {
|
|
724
|
+
const data = import_node_fs.readFileSync(absolutePath);
|
|
725
|
+
return {
|
|
726
|
+
absolutePath,
|
|
727
|
+
path: import_node_path.relative(out, absolutePath).split(import_node_path.sep).join("/"),
|
|
728
|
+
sha256: import_node_crypto.createHash("sha256").update(data).digest("hex"),
|
|
729
|
+
size: import_node_fs.statSync(absolutePath).size
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
const bundle = import_node_fs.readFileSync(import_node_path.join(out, BUNDLE_FILE[platform]));
|
|
733
|
+
return { files, hermesVersion: hermesVersion(bundle) };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/utils/publish/changelog.util.ts
|
|
737
|
+
function changelogFromSubjects(subjects) {
|
|
738
|
+
return subjects.map((s) => s.trim()).filter(Boolean).map((s) => `- ${s}`).join(`
|
|
739
|
+
`);
|
|
740
|
+
}
|
|
741
|
+
function shortChangelog(changelog, maxLines = 3, maxChars = 280) {
|
|
742
|
+
const lines = changelog.split(`
|
|
743
|
+
`).filter((l) => l.trim());
|
|
744
|
+
let short = lines.slice(0, maxLines).join(`
|
|
745
|
+
`);
|
|
746
|
+
if (short.length > maxChars)
|
|
747
|
+
short = `${short.slice(0, maxChars - 1).trimEnd()}…`;
|
|
748
|
+
else if (lines.length > maxLines)
|
|
749
|
+
short += `
|
|
750
|
+
… ${lines.length - maxLines} more`;
|
|
751
|
+
return short;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/utils/publish/indexes.util.ts
|
|
755
|
+
var newestFirst = (a, b) => b.publishedAt.localeCompare(a.publishedAt);
|
|
756
|
+
function upsertPreview(index, platform, build, entry, now) {
|
|
757
|
+
const others = (index?.previews ?? []).filter((p) => p.id !== entry.id);
|
|
758
|
+
return {
|
|
759
|
+
schema: SCHEMA,
|
|
760
|
+
platform,
|
|
761
|
+
key: build.key,
|
|
762
|
+
compatibility: build.compatibility,
|
|
763
|
+
updatedAt: now,
|
|
764
|
+
previews: [...others, entry].sort(newestFirst)
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function removePreview(index, id, now) {
|
|
768
|
+
return {
|
|
769
|
+
...index,
|
|
770
|
+
updatedAt: now,
|
|
771
|
+
previews: index.previews.filter((p) => p.id !== id)
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function syncRootVersion(root, index, now) {
|
|
775
|
+
const versions = (root?.versions ?? []).filter((v) => !(v.platform === index.platform && v.key === index.key));
|
|
776
|
+
if (index.previews.length > 0) {
|
|
777
|
+
const latest = index.previews[0];
|
|
778
|
+
const version = {
|
|
779
|
+
platform: index.platform,
|
|
780
|
+
key: index.key,
|
|
781
|
+
compatibility: index.compatibility,
|
|
782
|
+
appVersion: latest.appVersion,
|
|
783
|
+
buildNumber: latest.buildNumber,
|
|
784
|
+
fingerprint: latest.fingerprint,
|
|
785
|
+
count: index.previews.length,
|
|
786
|
+
updatedAt: now,
|
|
787
|
+
index: platformIndexKey(index.platform, index.key),
|
|
788
|
+
previews: index.previews.map(({ id, title, publishedAt }) => ({
|
|
789
|
+
id,
|
|
790
|
+
title,
|
|
791
|
+
publishedAt
|
|
792
|
+
}))
|
|
793
|
+
};
|
|
794
|
+
versions.push(version);
|
|
795
|
+
}
|
|
796
|
+
versions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
797
|
+
return { schema: SCHEMA, updatedAt: now, versions };
|
|
798
|
+
}
|
|
799
|
+
var indexesListing = (root, id) => (root?.versions ?? []).filter((v) => v.previews.some((p) => p.id === id)).map((v) => v.index);
|
|
800
|
+
|
|
801
|
+
// src/utils/config/expo-config.util.ts
|
|
802
|
+
var PLUGIN_NAME = "@axonpack/preview";
|
|
803
|
+
function compatibilityOf(config) {
|
|
804
|
+
for (const plugin of config.plugins ?? []) {
|
|
805
|
+
if (Array.isArray(plugin) && plugin[0] === PLUGIN_NAME) {
|
|
806
|
+
const option = plugin[1]?.compatibility;
|
|
807
|
+
if (option === undefined)
|
|
808
|
+
break;
|
|
809
|
+
if (!isCompatibility(option)) {
|
|
810
|
+
throw new Error(`${PLUGIN_NAME}: compatibility must be "appVersion" or "fingerprint"`);
|
|
811
|
+
}
|
|
812
|
+
return option;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return DEFAULT_COMPATIBILITY;
|
|
816
|
+
}
|
|
817
|
+
var appVersionOf = (config, platform) => String(config[platform]?.version ?? config.version ?? "");
|
|
818
|
+
var buildNumberOf = (config, platform) => String((platform === "ios" ? config.ios?.buildNumber : config.android?.versionCode) ?? "");
|
|
819
|
+
function resolveCompatibility(fromApp, flag) {
|
|
820
|
+
if (flag !== undefined && !isCompatibility(flag)) {
|
|
821
|
+
throw new Error(`--compatibility must be appVersion or fingerprint, not ${flag}`);
|
|
822
|
+
}
|
|
823
|
+
const compatibility = flag ?? fromApp;
|
|
824
|
+
const warnings = [];
|
|
825
|
+
if (compatibility !== fromApp) {
|
|
826
|
+
warnings.push(`--compatibility ${compatibility} differs from the app config (${fromApp}), so a build of this app will not list this preview.`);
|
|
827
|
+
}
|
|
828
|
+
if (compatibility === "appVersion") {
|
|
829
|
+
warnings.push("Matched by app version: a native change without a version bump is not detected. Bump the version after native changes, or use fingerprint.");
|
|
830
|
+
}
|
|
831
|
+
return { compatibility, warnings };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/services/cli/project.service.ts
|
|
835
|
+
var import_node_child_process2 = require("node:child_process");
|
|
836
|
+
var import_node_module = require("node:module");
|
|
837
|
+
var import_node_path2 = require("node:path");
|
|
838
|
+
var git = (cwd, args) => {
|
|
839
|
+
try {
|
|
840
|
+
return import_node_child_process2.execFileSync("git", args, {
|
|
841
|
+
cwd,
|
|
842
|
+
encoding: "utf8",
|
|
843
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
844
|
+
}).trim();
|
|
845
|
+
} catch {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
var appRequire = (projectRoot) => {
|
|
850
|
+
const expo = import_node_module.createRequire(import_node_path2.join(projectRoot, "package.json")).resolve("expo/package.json");
|
|
851
|
+
return import_node_module.createRequire(expo);
|
|
852
|
+
};
|
|
853
|
+
function appConfig(projectRoot) {
|
|
854
|
+
const { getConfig } = appRequire(projectRoot)("@expo/config");
|
|
855
|
+
return getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp;
|
|
856
|
+
}
|
|
857
|
+
async function fingerprint(projectRoot, platform) {
|
|
858
|
+
const { createFingerprintAsync } = appRequire(projectRoot)("@expo/fingerprint");
|
|
859
|
+
const result = await createFingerprintAsync(projectRoot, {
|
|
860
|
+
platforms: [platform],
|
|
861
|
+
silent: true
|
|
862
|
+
});
|
|
863
|
+
return result.hash;
|
|
864
|
+
}
|
|
865
|
+
function gitInfo(projectRoot, {
|
|
866
|
+
id,
|
|
867
|
+
env = process.env
|
|
868
|
+
} = {}) {
|
|
869
|
+
const ref = git(projectRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? "";
|
|
870
|
+
return {
|
|
871
|
+
branch: ref === "HEAD" ? env.GITHUB_HEAD_REF || id || "" : ref,
|
|
872
|
+
commit: git(projectRoot, ["rev-parse", "--short", "HEAD"]) ?? "",
|
|
873
|
+
author: git(projectRoot, ["log", "-1", "--format=%an"]) ?? git(projectRoot, ["config", "user.name"]) ?? ""
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
var gitTopLevel = (projectRoot) => git(projectRoot, ["rev-parse", "--show-toplevel"]);
|
|
877
|
+
function defaultBase(projectRoot) {
|
|
878
|
+
const head = git(projectRoot, [
|
|
879
|
+
"symbolic-ref",
|
|
880
|
+
"--short",
|
|
881
|
+
"refs/remotes/origin/HEAD"
|
|
882
|
+
]);
|
|
883
|
+
if (head)
|
|
884
|
+
return head;
|
|
885
|
+
for (const name of ["main", "master"]) {
|
|
886
|
+
if (git(projectRoot, ["rev-parse", "--verify", "--quiet", name]) !== null)
|
|
887
|
+
return name;
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
function changelogSince(projectRoot, base) {
|
|
892
|
+
if (!base)
|
|
893
|
+
return "";
|
|
894
|
+
const forkPoint = git(projectRoot, ["merge-base", "HEAD", base]);
|
|
895
|
+
if (!forkPoint)
|
|
896
|
+
return "";
|
|
897
|
+
const subjects = git(projectRoot, [
|
|
898
|
+
"log",
|
|
899
|
+
"--format=%s",
|
|
900
|
+
`${forkPoint}..HEAD`
|
|
901
|
+
]);
|
|
902
|
+
return changelogFromSubjects(subjects ? subjects.split(`
|
|
903
|
+
`) : []);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/services/publish/commands.service.ts
|
|
907
|
+
var log = (message) => process.stdout.write(`${message}
|
|
908
|
+
`);
|
|
909
|
+
async function updateJson(s3, key, change, attempts = 8) {
|
|
910
|
+
for (let attempt = 1;attempt <= attempts; attempt++) {
|
|
911
|
+
const current = await getJson(s3, key);
|
|
912
|
+
const next = change(current?.data ?? null);
|
|
913
|
+
const written = await putObject(s3, key, JSON.stringify(next, null, 2), {
|
|
914
|
+
contentType: "application/json",
|
|
915
|
+
etag: current?.etag ?? null
|
|
916
|
+
});
|
|
917
|
+
if (written)
|
|
918
|
+
return next;
|
|
919
|
+
await new Promise((r) => setTimeout(r, 100 * attempt + Math.random() * 200));
|
|
920
|
+
}
|
|
921
|
+
throw new Error(`Gave up writing ${key}: it kept changing under us`);
|
|
922
|
+
}
|
|
923
|
+
async function publish(options) {
|
|
924
|
+
const { s3, id } = options;
|
|
925
|
+
const title = options.title || previewTitle(id);
|
|
926
|
+
const publishedAt = new Date().toISOString();
|
|
927
|
+
for (const platform of options.platforms) {
|
|
928
|
+
log(`
|
|
929
|
+
${platform}: building ${id}`);
|
|
930
|
+
const { files, hermesVersion } = buildBundle(options.projectRoot, platform, options.entryFile);
|
|
931
|
+
const fp = await fingerprint(options.projectRoot, platform);
|
|
932
|
+
const version = appVersionOf(options.config, platform);
|
|
933
|
+
const key = compatibilityKey(options.compatibility, options.compatibility === "appVersion" ? version : fp);
|
|
934
|
+
let uploaded = 0;
|
|
935
|
+
for (const file of files) {
|
|
936
|
+
if (await exists(s3, fileKey(file.sha256)))
|
|
937
|
+
continue;
|
|
938
|
+
await putObject(s3, fileKey(file.sha256), import_node_fs2.readFileSync(file.absolutePath));
|
|
939
|
+
uploaded++;
|
|
940
|
+
}
|
|
941
|
+
log(`${platform}: ${files.length} files, ${uploaded} new`);
|
|
942
|
+
const info = {
|
|
943
|
+
id,
|
|
944
|
+
title,
|
|
945
|
+
branch: options.branch,
|
|
946
|
+
commit: options.commit,
|
|
947
|
+
author: options.author,
|
|
948
|
+
publishedAt,
|
|
949
|
+
appVersion: version,
|
|
950
|
+
buildNumber: buildNumberOf(options.config, platform),
|
|
951
|
+
compatibility: options.compatibility,
|
|
952
|
+
fingerprint: fp
|
|
953
|
+
};
|
|
954
|
+
const manifest = {
|
|
955
|
+
schema: SCHEMA,
|
|
956
|
+
...info,
|
|
957
|
+
platform,
|
|
958
|
+
key,
|
|
959
|
+
bundle: BUNDLE_FILE[platform],
|
|
960
|
+
hermesVersion,
|
|
961
|
+
changelog: options.changelog,
|
|
962
|
+
files: files.map(({ path, sha256, size }) => ({ path, sha256, size }))
|
|
963
|
+
};
|
|
964
|
+
const manifestPath = manifestKey(platform, key, id);
|
|
965
|
+
await putObject(s3, manifestPath, JSON.stringify(manifest, null, 2), {
|
|
966
|
+
contentType: "application/json"
|
|
967
|
+
});
|
|
968
|
+
const entry = {
|
|
969
|
+
...info,
|
|
970
|
+
revision: import_node_crypto2.createHash("sha256").update(manifest.files.map((f) => `${f.path}:${f.sha256}`).sort().join(`
|
|
971
|
+
`)).digest("hex").slice(0, 16),
|
|
972
|
+
size: files.reduce((sum, f) => sum + f.size, 0),
|
|
973
|
+
changelog: shortChangelog(options.changelog),
|
|
974
|
+
manifest: manifestPath
|
|
975
|
+
};
|
|
976
|
+
const index = await updateJson(s3, platformIndexKey(platform, key), (current) => upsertPreview(current, platform, { key, compatibility: options.compatibility }, entry, publishedAt));
|
|
977
|
+
await updateJson(s3, ROOT_INDEX_KEY, (root) => syncRootVersion(root, index, publishedAt));
|
|
978
|
+
log(`${platform}: published ${id} for build ${key}`);
|
|
979
|
+
log(`${platform}: ${objectUrl(s3, platformIndexKey(platform, key))}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function unpublish(s3, id) {
|
|
983
|
+
const root = await getJson(s3, ROOT_INDEX_KEY);
|
|
984
|
+
const keys = indexesListing(root?.data ?? null, id);
|
|
985
|
+
if (keys.length === 0) {
|
|
986
|
+
log(`${id} is not published`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const now = new Date().toISOString();
|
|
990
|
+
for (const key of keys) {
|
|
991
|
+
const index = await updateJson(s3, key, (current) => {
|
|
992
|
+
if (!current)
|
|
993
|
+
throw new Error(`${key} is listed in ${ROOT_INDEX_KEY} but missing`);
|
|
994
|
+
return removePreview(current, id, now);
|
|
995
|
+
});
|
|
996
|
+
await deleteObject(s3, manifestKey(index.platform, index.key, id));
|
|
997
|
+
await updateJson(s3, ROOT_INDEX_KEY, (current) => syncRootVersion(current, index, now));
|
|
998
|
+
log(`unpublished ${id} from ${key}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async function prune(s3) {
|
|
1002
|
+
const referenced = new Set;
|
|
1003
|
+
for (const key of await listKeys(s3, "previews/")) {
|
|
1004
|
+
const manifest = await getJson(s3, key);
|
|
1005
|
+
manifest?.data.files.forEach((f) => referenced.add(fileKey(f.sha256)));
|
|
1006
|
+
}
|
|
1007
|
+
let removed = 0;
|
|
1008
|
+
for (const key of await listKeys(s3, "files/")) {
|
|
1009
|
+
if (referenced.has(key))
|
|
1010
|
+
continue;
|
|
1011
|
+
await deleteObject(s3, key);
|
|
1012
|
+
removed++;
|
|
1013
|
+
}
|
|
1014
|
+
log(`pruned ${removed} files, kept ${referenced.size}`);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/services/publish/eas-env.service.ts
|
|
1018
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1019
|
+
var import_node_fs3 = require("node:fs");
|
|
1020
|
+
var import_node_os2 = require("node:os");
|
|
1021
|
+
var import_node_path3 = require("node:path");
|
|
1022
|
+
var import_node_util = require("node:util");
|
|
1023
|
+
function pullEasEnv(projectRoot, environment) {
|
|
1024
|
+
const dir = import_node_fs3.mkdtempSync(import_node_path3.join(import_node_os2.tmpdir(), "preview-eas-env-"));
|
|
1025
|
+
const file = import_node_path3.join(dir, ".env");
|
|
1026
|
+
try {
|
|
1027
|
+
import_node_child_process3.execFileSync("eas", [
|
|
1028
|
+
"env:pull",
|
|
1029
|
+
"--environment",
|
|
1030
|
+
environment,
|
|
1031
|
+
"--non-interactive",
|
|
1032
|
+
"--path",
|
|
1033
|
+
file
|
|
1034
|
+
], { cwd: projectRoot, encoding: "utf8", stdio: "pipe" });
|
|
1035
|
+
return import_node_util.parseEnv(import_node_fs3.readFileSync(file, "utf8"));
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
throw easError(error, environment);
|
|
1038
|
+
} finally {
|
|
1039
|
+
import_node_fs3.rmSync(dir, { recursive: true, force: true });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function easError(error, environment) {
|
|
1043
|
+
const e = error;
|
|
1044
|
+
if (e.code === "ENOENT")
|
|
1045
|
+
return new Error("--environment needs eas-cli: npm install -g eas-cli, then eas login.");
|
|
1046
|
+
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
1047
|
+
if (output.includes("An Expo user account is required"))
|
|
1048
|
+
return new Error("--environment needs an Expo login: run eas login, or set EXPO_TOKEN in CI.");
|
|
1049
|
+
if (output.includes("eas init"))
|
|
1050
|
+
return new Error("--environment needs the project linked to EAS: run eas init in the app's root.");
|
|
1051
|
+
return new Error(`eas env:pull --environment ${environment} failed:
|
|
1052
|
+
${output.trim() || String(error)}`);
|
|
1053
|
+
}
|
|
1054
|
+
function applyEasEnv(projectRoot, environment, env = process.env) {
|
|
1055
|
+
for (const [name, value] of Object.entries(pullEasEnv(projectRoot, environment)))
|
|
1056
|
+
if (env[name] === undefined)
|
|
1057
|
+
env[name] = value;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/constants/dev-preview-servers/dev-preview-servers.const.ts
|
|
1061
|
+
var DEFAULT_REGISTRY_PORT = 19150;
|
|
1062
|
+
var DEFAULT_PORT_RANGE = [8081, 8099];
|
|
1063
|
+
var HEARTBEAT_MS = 5000;
|
|
1064
|
+
var SERVER_TTL_MS = 15000;
|
|
1065
|
+
|
|
1066
|
+
// src/services/cli/config.service.ts
|
|
1067
|
+
var import_node_path4 = require("node:path");
|
|
1068
|
+
var ENV = {
|
|
1069
|
+
url: "PREVIEW_S3_URL",
|
|
1070
|
+
accessKeyId: "PREVIEW_S3_ACCESS_KEY_ID",
|
|
1071
|
+
secretAccessKey: "PREVIEW_S3_SECRET_ACCESS_KEY",
|
|
1072
|
+
region: "PREVIEW_S3_REGION"
|
|
1073
|
+
};
|
|
1074
|
+
var expoEnvFiles = (projectRoot, env) => appRequire(projectRoot)("@expo/env").parseProjectEnv(projectRoot, {
|
|
1075
|
+
mode: "production",
|
|
1076
|
+
silent: true,
|
|
1077
|
+
systemEnv: env
|
|
1078
|
+
}).env;
|
|
1079
|
+
function readConfig(projectRoot, env = process.env) {
|
|
1080
|
+
env = { ...expoEnvFiles(projectRoot, env), ...env };
|
|
1081
|
+
const missing = [ENV.url, ENV.accessKeyId, ENV.secretAccessKey].filter((name) => !env[name]);
|
|
1082
|
+
if (missing.length > 0) {
|
|
1083
|
+
throw new Error(`Set ${missing.join(", ")} in the environment (CI secrets), or locally in ${import_node_path4.join(projectRoot, ".env.local")}. Any env file Expo loads for a release build works, but keep the write key out of a committed .env.`);
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
...parseBucketUrl(env[ENV.url]),
|
|
1087
|
+
region: env[ENV.region] || DEFAULT_REGION,
|
|
1088
|
+
credentials: {
|
|
1089
|
+
accessKeyId: env[ENV.accessKeyId],
|
|
1090
|
+
secretAccessKey: env[ENV.secretAccessKey]
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/services/registry/registry.service.ts
|
|
1096
|
+
var import_node_http = require("node:http");
|
|
1097
|
+
class ServerList {
|
|
1098
|
+
ttlMs;
|
|
1099
|
+
entries = new Map;
|
|
1100
|
+
constructor(ttlMs = SERVER_TTL_MS) {
|
|
1101
|
+
this.ttlMs = ttlMs;
|
|
1102
|
+
}
|
|
1103
|
+
upsert(server, now) {
|
|
1104
|
+
this.entries.set(server.id, { server, lastSeen: now });
|
|
1105
|
+
}
|
|
1106
|
+
remove(id) {
|
|
1107
|
+
this.entries.delete(id);
|
|
1108
|
+
}
|
|
1109
|
+
alive(now) {
|
|
1110
|
+
for (const [id, entry] of this.entries) {
|
|
1111
|
+
if (now - entry.lastSeen > this.ttlMs)
|
|
1112
|
+
this.entries.delete(id);
|
|
1113
|
+
}
|
|
1114
|
+
return [...this.entries.values()].map((e) => e.server).sort((a, b) => a.port - b.port);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
var readBody = (req) => new Promise((resolve, reject) => {
|
|
1118
|
+
let body = "";
|
|
1119
|
+
req.on("data", (chunk) => body += chunk);
|
|
1120
|
+
req.on("end", () => resolve(body));
|
|
1121
|
+
req.on("error", reject);
|
|
1122
|
+
});
|
|
1123
|
+
function createRegistry(options = {}) {
|
|
1124
|
+
const now = options.now ?? Date.now;
|
|
1125
|
+
const list = new ServerList(options.ttlMs);
|
|
1126
|
+
let emptySince = now();
|
|
1127
|
+
const server = import_node_http.createServer(async (req, res) => {
|
|
1128
|
+
const send = (status, body) => {
|
|
1129
|
+
res.writeHead(status, {
|
|
1130
|
+
"content-type": "application/json",
|
|
1131
|
+
"cache-control": "no-store"
|
|
1132
|
+
});
|
|
1133
|
+
res.end(body === undefined ? "" : JSON.stringify(body));
|
|
1134
|
+
};
|
|
1135
|
+
try {
|
|
1136
|
+
if (req.method === "GET" && req.url === "/servers") {
|
|
1137
|
+
send(200, { servers: list.alive(now()) });
|
|
1138
|
+
} else if (req.method === "POST" && req.url === "/servers") {
|
|
1139
|
+
const entry = JSON.parse(await readBody(req));
|
|
1140
|
+
if (typeof entry.id !== "string" || typeof entry.port !== "number") {
|
|
1141
|
+
send(400, { error: "id and port are required" });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
list.upsert(entry, now());
|
|
1145
|
+
send(204);
|
|
1146
|
+
} else if (req.method === "DELETE" && req.url?.startsWith("/servers/")) {
|
|
1147
|
+
list.remove(decodeURIComponent(req.url.slice("/servers/".length)));
|
|
1148
|
+
send(204);
|
|
1149
|
+
} else {
|
|
1150
|
+
send(404, { error: "not found" });
|
|
1151
|
+
}
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
send(400, {
|
|
1154
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
if (options.onIdle) {
|
|
1159
|
+
const timer = setInterval(() => {
|
|
1160
|
+
if (list.alive(now()).length > 0)
|
|
1161
|
+
emptySince = now();
|
|
1162
|
+
else if (now() - emptySince > (options.idleExitMs ?? 30000))
|
|
1163
|
+
options.onIdle?.();
|
|
1164
|
+
}, 1000);
|
|
1165
|
+
timer.unref();
|
|
1166
|
+
server.on("close", () => clearInterval(timer));
|
|
1167
|
+
}
|
|
1168
|
+
return { server, list };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/services/start/start.service.ts
|
|
1172
|
+
var import_node_child_process4 = require("node:child_process");
|
|
1173
|
+
var import_node_crypto3 = require("node:crypto");
|
|
1174
|
+
var import_node_net = require("node:net");
|
|
1175
|
+
var log2 = (message) => process.stdout.write(`preview: ${message}
|
|
1176
|
+
`);
|
|
1177
|
+
var portIsFree = (port) => new Promise((resolve) => {
|
|
1178
|
+
const probe = import_node_net.createServer();
|
|
1179
|
+
probe.once("error", () => resolve(false));
|
|
1180
|
+
probe.once("listening", () => probe.close(() => resolve(true)));
|
|
1181
|
+
probe.listen(port);
|
|
1182
|
+
});
|
|
1183
|
+
async function freePort([from, to], claimed = new Set) {
|
|
1184
|
+
for (let port = from;port <= to; port++) {
|
|
1185
|
+
if (claimed.has(port))
|
|
1186
|
+
continue;
|
|
1187
|
+
if (await portIsFree(port))
|
|
1188
|
+
return port;
|
|
1189
|
+
}
|
|
1190
|
+
throw new Error(`No free port between ${from} and ${to}; pass --ports <from>-<to>.`);
|
|
1191
|
+
}
|
|
1192
|
+
function registration(input) {
|
|
1193
|
+
const { config } = input;
|
|
1194
|
+
const compatibility = compatibilityOf(config);
|
|
1195
|
+
const keys = {};
|
|
1196
|
+
for (const platform of PLATFORMS) {
|
|
1197
|
+
const value = compatibility === "appVersion" ? appVersionOf(config, platform) : input.fingerprints?.[platform];
|
|
1198
|
+
if (value)
|
|
1199
|
+
keys[platform] = compatibilityKey(compatibility, value);
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
id: `${input.port}-${import_node_crypto3.randomUUID().slice(0, 8)}`,
|
|
1203
|
+
port: input.port,
|
|
1204
|
+
title: input.title || (input.branch ? previewTitle(input.branch) : config.name ?? ""),
|
|
1205
|
+
branch: input.branch,
|
|
1206
|
+
worktree: input.worktree,
|
|
1207
|
+
slug: config.slug ?? "",
|
|
1208
|
+
name: config.name ?? "",
|
|
1209
|
+
iosBundleIdentifier: config.ios?.bundleIdentifier,
|
|
1210
|
+
androidPackage: config.android?.package,
|
|
1211
|
+
compatibility,
|
|
1212
|
+
keys,
|
|
1213
|
+
startedAt: new Date().toISOString()
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
var registryUp = async (registry) => {
|
|
1217
|
+
try {
|
|
1218
|
+
return (await fetch(`${registry}/servers`, { signal: AbortSignal.timeout(500) })).ok;
|
|
1219
|
+
} catch {
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
async function ensureRegistry(registryPort, bin) {
|
|
1224
|
+
const registry = `http://127.0.0.1:${registryPort}`;
|
|
1225
|
+
if (await registryUp(registry))
|
|
1226
|
+
return registry;
|
|
1227
|
+
import_node_child_process4.spawn(process.execPath, [bin, "registry", "--port", String(registryPort)], {
|
|
1228
|
+
detached: true,
|
|
1229
|
+
stdio: "ignore"
|
|
1230
|
+
}).unref();
|
|
1231
|
+
for (let i = 0;i < 30; i++) {
|
|
1232
|
+
if (await registryUp(registry))
|
|
1233
|
+
return registry;
|
|
1234
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1235
|
+
}
|
|
1236
|
+
throw new Error(`The registry did not start on port ${registryPort}.`);
|
|
1237
|
+
}
|
|
1238
|
+
function adbReverse(ports) {
|
|
1239
|
+
let devices;
|
|
1240
|
+
try {
|
|
1241
|
+
devices = import_node_child_process4.execFileSync("adb", ["devices"], {
|
|
1242
|
+
encoding: "utf8",
|
|
1243
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1244
|
+
}).split(`
|
|
1245
|
+
`).slice(1).filter((line) => line.endsWith("\tdevice")).map((line) => line.split("\t")[0]);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
for (const serial of devices) {
|
|
1250
|
+
for (const port of ports) {
|
|
1251
|
+
try {
|
|
1252
|
+
import_node_child_process4.execFileSync("adb", ["-s", serial, "reverse", `tcp:${port}`, `tcp:${port}`], { stdio: "ignore" });
|
|
1253
|
+
} catch {}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async function start(options) {
|
|
1258
|
+
const registry = await ensureRegistry(options.registryPort, options.bin);
|
|
1259
|
+
const claimed = await fetch(`${registry}/servers`).then((res) => res.json()).then(({ servers }) => new Set(servers.map((s) => s.port))).catch(() => new Set);
|
|
1260
|
+
const port = await freePort(options.ports, claimed);
|
|
1261
|
+
const fingerprints = {};
|
|
1262
|
+
if (compatibilityOf(options.config) === "fingerprint") {
|
|
1263
|
+
for (const platform of PLATFORMS) {
|
|
1264
|
+
fingerprints[platform] = await fingerprint(options.projectRoot, platform);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const server = registration({ ...options, port, fingerprints });
|
|
1268
|
+
adbReverse([options.registryPort, port]);
|
|
1269
|
+
const post = () => fetch(`${registry}/servers`, {
|
|
1270
|
+
method: "POST",
|
|
1271
|
+
body: JSON.stringify(server)
|
|
1272
|
+
});
|
|
1273
|
+
const register = () => post().catch(() => ensureRegistry(options.registryPort, options.bin).then(post)).catch(() => {});
|
|
1274
|
+
const unregister = () => fetch(`${registry}/servers/${encodeURIComponent(server.id)}`, {
|
|
1275
|
+
method: "DELETE"
|
|
1276
|
+
}).catch(() => {
|
|
1277
|
+
return;
|
|
1278
|
+
});
|
|
1279
|
+
await register();
|
|
1280
|
+
log2(`${server.title || server.name} on port ${port}, announced on ${registry}`);
|
|
1281
|
+
let beats = 0;
|
|
1282
|
+
const heartbeat = setInterval(() => {
|
|
1283
|
+
register();
|
|
1284
|
+
if (++beats % 6 === 0)
|
|
1285
|
+
adbReverse([options.registryPort, port]);
|
|
1286
|
+
}, HEARTBEAT_MS);
|
|
1287
|
+
const child = import_node_child_process4.spawn("npx", ["expo", "start", "--port", String(port), ...options.expoArgs], {
|
|
1288
|
+
cwd: options.projectRoot,
|
|
1289
|
+
stdio: "inherit"
|
|
1290
|
+
});
|
|
1291
|
+
let stopping = false;
|
|
1292
|
+
const stop = async (code) => {
|
|
1293
|
+
if (stopping)
|
|
1294
|
+
return;
|
|
1295
|
+
stopping = true;
|
|
1296
|
+
clearInterval(heartbeat);
|
|
1297
|
+
await unregister();
|
|
1298
|
+
process.exit(code);
|
|
1299
|
+
};
|
|
1300
|
+
child.on("exit", (code) => stop(code ?? 0));
|
|
1301
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
1302
|
+
process.on(signal, () => {
|
|
1303
|
+
child.kill(signal);
|
|
1304
|
+
stop(0);
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/services/cli/title.service.ts
|
|
1310
|
+
var import_node_readline = require("node:readline");
|
|
1311
|
+
async function askTitle(options) {
|
|
1312
|
+
const input = options.input ?? process.stdin;
|
|
1313
|
+
const output = options.output ?? process.stdout;
|
|
1314
|
+
if (options.given)
|
|
1315
|
+
return options.given;
|
|
1316
|
+
if (!input.isTTY || !output.isTTY)
|
|
1317
|
+
return options.fallback;
|
|
1318
|
+
const rl = import_node_readline.createInterface({ input, output });
|
|
1319
|
+
try {
|
|
1320
|
+
const answer = new Promise((resolve) => rl.question("Name in the app: ", resolve));
|
|
1321
|
+
rl.write(options.fallback);
|
|
1322
|
+
return (await answer).trim() || options.fallback;
|
|
1323
|
+
} finally {
|
|
1324
|
+
rl.close();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/cli.ts
|
|
1329
|
+
var HELP = `preview <command> [options]
|
|
1330
|
+
|
|
1331
|
+
publish build this app's JS bundle and upload it as a preview
|
|
1332
|
+
--id <id> preview id (default: the git branch)
|
|
1333
|
+
--title <title> name QA sees; without it a terminal asks, prefilled with a readable
|
|
1334
|
+
form of the id (Enter keeps it). No prompt without a terminal (CI)
|
|
1335
|
+
--platform <p> android, ios or all (default: all)
|
|
1336
|
+
--base <branch> branch the changelog counts from (default: the repo's default branch)
|
|
1337
|
+
--changelog=<text> changelog text instead of the commit subjects (use = when it starts
|
|
1338
|
+
with a dash, as a "- item" list does)
|
|
1339
|
+
--changelog-file <path>
|
|
1340
|
+
--entry-file <path>
|
|
1341
|
+
--compatibility <c> appVersion or fingerprint; overrides the app config's plugin option
|
|
1342
|
+
--environment <name> build with this EAS environment's variables, like eas update
|
|
1343
|
+
(needs eas-cli and a project linked to EAS; shell variables win)
|
|
1344
|
+
unpublish <id> remove a preview from every build's list
|
|
1345
|
+
prune delete files no preview uses any more
|
|
1346
|
+
start [-- <expo start args>]
|
|
1347
|
+
run Metro on a free port and announce it to the app's Dev preview servers
|
|
1348
|
+
--title <title> name the app's list shows; asked like publish, prefilled with a
|
|
1349
|
+
readable form of the branch
|
|
1350
|
+
--ports <from>-<to> ports to pick from (default 8081-8099)
|
|
1351
|
+
|
|
1352
|
+
--project <dir> the app's root (default: the current folder)
|
|
1353
|
+
|
|
1354
|
+
Where to publish comes from the environment (the shell, or CI secrets). For local use they can
|
|
1355
|
+
also go in the app's .env.local, or any env file Expo loads for a release build; the environment
|
|
1356
|
+
wins over them. Keep the write key out of a committed .env:
|
|
1357
|
+
PREVIEW_S3_URL <endpoint>/<bucket>[/<folder>], e.g. http://localhost:9000/previews-public
|
|
1358
|
+
PREVIEW_S3_ACCESS_KEY_ID the write key
|
|
1359
|
+
PREVIEW_S3_SECRET_ACCESS_KEY
|
|
1360
|
+
PREVIEW_S3_REGION optional, default us-east-1`;
|
|
1361
|
+
async function main() {
|
|
1362
|
+
const argv = process.argv.slice(2);
|
|
1363
|
+
const dash = argv.indexOf("--");
|
|
1364
|
+
const passThrough = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
1365
|
+
const { values, positionals } = import_node_util2.parseArgs({
|
|
1366
|
+
args: dash >= 0 ? argv.slice(0, dash) : argv,
|
|
1367
|
+
allowPositionals: true,
|
|
1368
|
+
options: {
|
|
1369
|
+
id: { type: "string" },
|
|
1370
|
+
title: { type: "string" },
|
|
1371
|
+
platform: { type: "string", default: "all" },
|
|
1372
|
+
base: { type: "string" },
|
|
1373
|
+
changelog: { type: "string" },
|
|
1374
|
+
"changelog-file": { type: "string" },
|
|
1375
|
+
"entry-file": { type: "string" },
|
|
1376
|
+
compatibility: { type: "string" },
|
|
1377
|
+
environment: { type: "string" },
|
|
1378
|
+
project: { type: "string" },
|
|
1379
|
+
port: { type: "string" },
|
|
1380
|
+
ports: { type: "string" },
|
|
1381
|
+
help: { type: "boolean", short: "h" }
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
const [command, arg] = positionals;
|
|
1385
|
+
if (values.help || !command) {
|
|
1386
|
+
process.stdout.write(`${HELP}
|
|
1387
|
+
`);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const projectRoot = import_node_path5.resolve(values.project ?? process.cwd());
|
|
1391
|
+
const s3 = () => readConfig(projectRoot);
|
|
1392
|
+
if (command === "publish") {
|
|
1393
|
+
if (values.environment)
|
|
1394
|
+
applyEasEnv(projectRoot, values.environment);
|
|
1395
|
+
const git = gitInfo(projectRoot, { id: values.id });
|
|
1396
|
+
const id = values.id ?? git.branch;
|
|
1397
|
+
if (!id)
|
|
1398
|
+
throw new Error("No git branch to name the preview after; pass --id.");
|
|
1399
|
+
const platforms = values.platform === "all" ? PLATFORMS : [values.platform];
|
|
1400
|
+
if (!platforms.every((p) => PLATFORMS.includes(p))) {
|
|
1401
|
+
throw new Error(`--platform must be android, ios or all`);
|
|
1402
|
+
}
|
|
1403
|
+
const changelog = values.changelog ?? (values["changelog-file"] ? import_node_fs4.readFileSync(import_node_path5.resolve(values["changelog-file"]), "utf8").trim() : changelogSince(projectRoot, values.base ?? defaultBase(projectRoot)));
|
|
1404
|
+
const config = appConfig(projectRoot);
|
|
1405
|
+
const { compatibility, warnings } = resolveCompatibility(compatibilityOf(config), values.compatibility);
|
|
1406
|
+
for (const warning of warnings)
|
|
1407
|
+
process.stderr.write(`warning: ${warning}
|
|
1408
|
+
`);
|
|
1409
|
+
await publish({
|
|
1410
|
+
projectRoot,
|
|
1411
|
+
config,
|
|
1412
|
+
compatibility,
|
|
1413
|
+
s3: s3(),
|
|
1414
|
+
platforms,
|
|
1415
|
+
id,
|
|
1416
|
+
title: await askTitle({
|
|
1417
|
+
given: values.title,
|
|
1418
|
+
fallback: previewTitle(id)
|
|
1419
|
+
}),
|
|
1420
|
+
...git,
|
|
1421
|
+
changelog,
|
|
1422
|
+
entryFile: values["entry-file"]
|
|
1423
|
+
});
|
|
1424
|
+
} else if (command === "unpublish") {
|
|
1425
|
+
if (!arg)
|
|
1426
|
+
throw new Error("Which preview? preview unpublish <id>");
|
|
1427
|
+
await unpublish(s3(), arg);
|
|
1428
|
+
} else if (command === "prune") {
|
|
1429
|
+
await prune(s3());
|
|
1430
|
+
} else if (command === "start") {
|
|
1431
|
+
const [from, to] = (values.ports ?? `${DEFAULT_PORT_RANGE[0]}-${DEFAULT_PORT_RANGE[1]}`).split("-").map(Number);
|
|
1432
|
+
if (!from || !to || from > to)
|
|
1433
|
+
throw new Error("--ports must look like 8081-8099");
|
|
1434
|
+
const config = appConfig(projectRoot);
|
|
1435
|
+
const branch = gitInfo(projectRoot).branch;
|
|
1436
|
+
const title = await askTitle({
|
|
1437
|
+
given: values.title,
|
|
1438
|
+
fallback: branch ? previewTitle(branch) : ""
|
|
1439
|
+
});
|
|
1440
|
+
await start({
|
|
1441
|
+
projectRoot,
|
|
1442
|
+
config,
|
|
1443
|
+
branch,
|
|
1444
|
+
title,
|
|
1445
|
+
worktree: gitTopLevel(projectRoot) ?? projectRoot,
|
|
1446
|
+
ports: [from, to],
|
|
1447
|
+
registryPort: DEFAULT_REGISTRY_PORT,
|
|
1448
|
+
expoArgs: passThrough,
|
|
1449
|
+
bin: process.argv[1]
|
|
1450
|
+
});
|
|
1451
|
+
} else if (command === "registry") {
|
|
1452
|
+
const port = Number(values.port ?? DEFAULT_REGISTRY_PORT);
|
|
1453
|
+
const { server } = createRegistry({ onIdle: () => process.exit(0) });
|
|
1454
|
+
server.listen(port);
|
|
1455
|
+
} else {
|
|
1456
|
+
throw new Error(`Unknown command ${command}.
|
|
1457
|
+
|
|
1458
|
+
${HELP}`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
main().catch((error) => {
|
|
1462
|
+
process.stderr.write(`preview: ${error instanceof Error ? error.message : String(error)}
|
|
1463
|
+
`);
|
|
1464
|
+
process.exit(1);
|
|
1465
|
+
});
|