@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.
Files changed (182) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -2
  4. package/android/build.gradle +21 -0
  5. package/android/consumer-rules.pro +3 -0
  6. package/android/src/main/java/expo/modules/axonpackpreview/AxonpackPreviewModule.kt +72 -0
  7. package/android/src/main/java/expo/modules/axonpackpreview/BundleLoaderOverride.kt +41 -0
  8. package/android/src/main/java/expo/modules/axonpackpreview/HermesHeader.kt +24 -0
  9. package/android/src/main/java/expo/modules/axonpackpreview/PreviewPackage.kt +76 -0
  10. package/android/src/main/java/expo/modules/axonpackpreview/PreviewState.kt +295 -0
  11. package/app.plugin.js +112 -0
  12. package/bin/preview.js +2 -0
  13. package/build/cli.cjs +1465 -0
  14. package/build/cli.d.ts +3 -0
  15. package/build/cli.d.ts.map +1 -0
  16. package/build/cli.js +164 -0
  17. package/build/cli.js.map +1 -0
  18. package/build/components/preview-list/preview-list.component.d.ts +13 -0
  19. package/build/components/preview-list/preview-list.component.d.ts.map +1 -0
  20. package/build/components/preview-list/preview-list.component.js +316 -0
  21. package/build/components/preview-list/preview-list.component.js.map +1 -0
  22. package/build/components/preview-list/preview-list.types.d.ts +59 -0
  23. package/build/components/preview-list/preview-list.types.d.ts.map +1 -0
  24. package/build/components/preview-list/preview-list.types.js +2 -0
  25. package/build/components/preview-list/preview-list.types.js.map +1 -0
  26. package/build/components/preview-provider/preview-provider.component.d.ts +12 -0
  27. package/build/components/preview-provider/preview-provider.component.d.ts.map +1 -0
  28. package/build/components/preview-provider/preview-provider.component.js +19 -0
  29. package/build/components/preview-provider/preview-provider.component.js.map +1 -0
  30. package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts +26 -0
  31. package/build/constants/dev-preview-servers/dev-preview-servers.const.d.ts.map +1 -0
  32. package/build/constants/dev-preview-servers/dev-preview-servers.const.js +9 -0
  33. package/build/constants/dev-preview-servers/dev-preview-servers.const.js.map +1 -0
  34. package/build/constants/preview-list/theme.const.d.ts +5 -0
  35. package/build/constants/preview-list/theme.const.d.ts.map +1 -0
  36. package/build/constants/preview-list/theme.const.js +65 -0
  37. package/build/constants/preview-list/theme.const.js.map +1 -0
  38. package/build/index.d.ts +24 -0
  39. package/build/index.d.ts.map +1 -0
  40. package/build/index.js +16 -0
  41. package/build/index.js.map +1 -0
  42. package/build/services/cli/config.service.d.ts +13 -0
  43. package/build/services/cli/config.service.d.ts.map +1 -0
  44. package/build/services/cli/config.service.js +39 -0
  45. package/build/services/cli/config.service.js.map +1 -0
  46. package/build/services/cli/project.service.d.ts +25 -0
  47. package/build/services/cli/project.service.d.ts.map +1 -0
  48. package/build/services/cli/project.service.js +81 -0
  49. package/build/services/cli/project.service.js.map +1 -0
  50. package/build/services/cli/title.service.d.ts +15 -0
  51. package/build/services/cli/title.service.d.ts.map +1 -0
  52. package/build/services/cli/title.service.js +24 -0
  53. package/build/services/cli/title.service.js.map +1 -0
  54. package/build/services/dev-preview-servers/discover.service.d.ts +14 -0
  55. package/build/services/dev-preview-servers/discover.service.d.ts.map +1 -0
  56. package/build/services/dev-preview-servers/discover.service.js +106 -0
  57. package/build/services/dev-preview-servers/discover.service.js.map +1 -0
  58. package/build/services/native/build-info.service.d.ts +11 -0
  59. package/build/services/native/build-info.service.d.ts.map +1 -0
  60. package/build/services/native/build-info.service.js +18 -0
  61. package/build/services/native/build-info.service.js.map +1 -0
  62. package/build/services/native/native.service.d.ts +24 -0
  63. package/build/services/native/native.service.d.ts.map +1 -0
  64. package/build/services/native/native.service.js +13 -0
  65. package/build/services/native/native.service.js.map +1 -0
  66. package/build/services/previews/download.service.d.ts +14 -0
  67. package/build/services/previews/download.service.d.ts.map +1 -0
  68. package/build/services/previews/download.service.js +66 -0
  69. package/build/services/previews/download.service.js.map +1 -0
  70. package/build/services/previews/local-previews.service.d.ts +25 -0
  71. package/build/services/previews/local-previews.service.d.ts.map +1 -0
  72. package/build/services/previews/local-previews.service.js +50 -0
  73. package/build/services/previews/local-previews.service.js.map +1 -0
  74. package/build/services/previews/reload.service.d.ts +6 -0
  75. package/build/services/previews/reload.service.d.ts.map +1 -0
  76. package/build/services/previews/reload.service.js +13 -0
  77. package/build/services/previews/reload.service.js.map +1 -0
  78. package/build/services/previews/remote.service.d.ts +11 -0
  79. package/build/services/previews/remote.service.d.ts.map +1 -0
  80. package/build/services/previews/remote.service.js +34 -0
  81. package/build/services/previews/remote.service.js.map +1 -0
  82. package/build/services/previews/select.service.d.ts +11 -0
  83. package/build/services/previews/select.service.d.ts.map +1 -0
  84. package/build/services/previews/select.service.js +25 -0
  85. package/build/services/previews/select.service.js.map +1 -0
  86. package/build/services/previews/use-preview.service.d.ts +21 -0
  87. package/build/services/previews/use-preview.service.d.ts.map +1 -0
  88. package/build/services/previews/use-preview.service.js +24 -0
  89. package/build/services/previews/use-preview.service.js.map +1 -0
  90. package/build/services/previews/use-running.service.d.ts +4 -0
  91. package/build/services/previews/use-running.service.d.ts.map +1 -0
  92. package/build/services/previews/use-running.service.js +21 -0
  93. package/build/services/previews/use-running.service.js.map +1 -0
  94. package/build/services/publish/bundle.service.d.ts +11 -0
  95. package/build/services/publish/bundle.service.d.ts.map +1 -0
  96. package/build/services/publish/bundle.service.js +46 -0
  97. package/build/services/publish/bundle.service.js.map +1 -0
  98. package/build/services/publish/commands.service.d.ts +23 -0
  99. package/build/services/publish/commands.service.d.ts.map +1 -0
  100. package/build/services/publish/commands.service.js +130 -0
  101. package/build/services/publish/commands.service.js.map +1 -0
  102. package/build/services/publish/eas-env.service.d.ts +9 -0
  103. package/build/services/publish/eas-env.service.d.ts.map +1 -0
  104. package/build/services/publish/eas-env.service.js +49 -0
  105. package/build/services/publish/eas-env.service.js.map +1 -0
  106. package/build/services/registry/registry.service.d.ts +27 -0
  107. package/build/services/registry/registry.service.d.ts.map +1 -0
  108. package/build/services/registry/registry.service.js +89 -0
  109. package/build/services/registry/registry.service.js.map +1 -0
  110. package/build/services/s3/s3.service.d.ts +59 -0
  111. package/build/services/s3/s3.service.d.ts.map +1 -0
  112. package/build/services/s3/s3.service.js +154 -0
  113. package/build/services/s3/s3.service.js.map +1 -0
  114. package/build/services/start/start.service.d.ts +48 -0
  115. package/build/services/start/start.service.d.ts.map +1 -0
  116. package/build/services/start/start.service.js +172 -0
  117. package/build/services/start/start.service.js.map +1 -0
  118. package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts +21 -0
  119. package/build/stores/dev-preview-servers/dev-preview-servers.store.d.ts.map +1 -0
  120. package/build/stores/dev-preview-servers/dev-preview-servers.store.js +91 -0
  121. package/build/stores/dev-preview-servers/dev-preview-servers.store.js.map +1 -0
  122. package/build/stores/previews/previews.store.d.ts +36 -0
  123. package/build/stores/previews/previews.store.d.ts.map +1 -0
  124. package/build/stores/previews/previews.store.js +134 -0
  125. package/build/stores/previews/previews.store.js.map +1 -0
  126. package/build/utils/config/config.util.d.ts +39 -0
  127. package/build/utils/config/config.util.d.ts.map +1 -0
  128. package/build/utils/config/config.util.js +24 -0
  129. package/build/utils/config/config.util.js.map +1 -0
  130. package/build/utils/config/expo-config.util.d.ts +30 -0
  131. package/build/utils/config/expo-config.util.d.ts.map +1 -0
  132. package/build/utils/config/expo-config.util.js +43 -0
  133. package/build/utils/config/expo-config.util.js.map +1 -0
  134. package/build/utils/dev-preview-servers/merge-servers.util.d.ts +47 -0
  135. package/build/utils/dev-preview-servers/merge-servers.util.d.ts.map +1 -0
  136. package/build/utils/dev-preview-servers/merge-servers.util.js +37 -0
  137. package/build/utils/dev-preview-servers/merge-servers.util.js.map +1 -0
  138. package/build/utils/preview-list/theme.util.d.ts +5 -0
  139. package/build/utils/preview-list/theme.util.d.ts.map +1 -0
  140. package/build/utils/preview-list/theme.util.js +12 -0
  141. package/build/utils/preview-list/theme.util.js.map +1 -0
  142. package/build/utils/previews/layout.util.d.ts +89 -0
  143. package/build/utils/previews/layout.util.d.ts.map +1 -0
  144. package/build/utils/previews/layout.util.js +30 -0
  145. package/build/utils/previews/layout.util.js.map +1 -0
  146. package/build/utils/previews/preview-items.util.d.ts +23 -0
  147. package/build/utils/previews/preview-items.util.d.ts.map +1 -0
  148. package/build/utils/previews/preview-items.util.js +39 -0
  149. package/build/utils/previews/preview-items.util.js.map +1 -0
  150. package/build/utils/previews/preview-title.util.d.ts +2 -0
  151. package/build/utils/previews/preview-title.util.d.ts.map +1 -0
  152. package/build/utils/previews/preview-title.util.js +6 -0
  153. package/build/utils/previews/preview-title.util.js.map +1 -0
  154. package/build/utils/previews/running.util.d.ts +23 -0
  155. package/build/utils/previews/running.util.d.ts.map +1 -0
  156. package/build/utils/previews/running.util.js +23 -0
  157. package/build/utils/previews/running.util.js.map +1 -0
  158. package/build/utils/publish/changelog.util.d.ts +5 -0
  159. package/build/utils/publish/changelog.util.d.ts.map +1 -0
  160. package/build/utils/publish/changelog.util.js +19 -0
  161. package/build/utils/publish/changelog.util.js.map +1 -0
  162. package/build/utils/publish/indexes.util.d.ts +12 -0
  163. package/build/utils/publish/indexes.util.d.ts.map +1 -0
  164. package/build/utils/publish/indexes.util.js +52 -0
  165. package/build/utils/publish/indexes.util.js.map +1 -0
  166. package/build/utils/s3/sigv4.util.d.ts +24 -0
  167. package/build/utils/s3/sigv4.util.d.ts.map +1 -0
  168. package/build/utils/s3/sigv4.util.js +67 -0
  169. package/build/utils/s3/sigv4.util.js.map +1 -0
  170. package/expo-module.config.json +10 -0
  171. package/ios/AxonpackPreview.podspec +30 -0
  172. package/ios/AxonpackPreviewModule.swift +55 -0
  173. package/ios/PreviewReactDelegateHandler.swift +147 -0
  174. package/ios/PreviewState.swift +236 -0
  175. package/notes/README.md +36 -0
  176. package/notes/bundles.md +35 -0
  177. package/notes/dev-preview-servers.md +67 -0
  178. package/notes/distribution.md +138 -0
  179. package/notes/native-loader.md +56 -0
  180. package/notes/storage.md +145 -0
  181. package/notes/testing.md +97 -0
  182. package/package.json +107 -4
@@ -0,0 +1,4 @@
1
+ import { type Running } from "../../utils/previews/running.util";
2
+ /** What runs now, read from the package's state. Needs a `<PreviewProvider>` above it. */
3
+ export declare function useRunning(): Running;
4
+ //# sourceMappingURL=use-running.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-running.service.d.ts","sourceRoot":"","sources":["../../../src/services/previews/use-running.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAc,MAAM,mCAAmC,CAAC;AAE7E,0FAA0F;AAC1F,wBAAgB,UAAU,IAAI,OAAO,CAcpC"}
@@ -0,0 +1,21 @@
1
+ import { nativeModule } from "../native/native.service";
2
+ import { usePreview } from "./use-preview.service";
3
+ import { useDevPreviewServersState } from "../../stores/dev-preview-servers/dev-preview-servers.store";
4
+ import { runningNow } from "../../utils/previews/running.util";
5
+ /** What runs now, read from the package's state. Needs a `<PreviewProvider>` above it. */
6
+ export function useRunning() {
7
+ const preview = usePreview();
8
+ const local = useDevPreviewServersState();
9
+ const showLocal = __DEV__ && preview.devPreviewServers;
10
+ // The first poll fills activeUrl a moment after the list mounts, but native knows it from the start.
11
+ const localUrl = showLocal
12
+ ? (local.activeUrl ?? nativeModule()?.state().localServer ?? null)
13
+ : null;
14
+ return runningNow({
15
+ items: preview.items,
16
+ activeId: preview.activeId,
17
+ localUrl,
18
+ servers: local.servers,
19
+ });
20
+ }
21
+ //# sourceMappingURL=use-running.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-running.service.js","sourceRoot":"","sources":["../../../src/services/previews/use-running.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAE,MAAM,4DAA4D,CAAC;AACvG,OAAO,EAAgB,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAE7E,0FAA0F;AAC1F,MAAM,UAAU,UAAU;IACxB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAG,yBAAyB,EAAE,CAAC;IAC1C,MAAM,SAAS,GAAG,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC;IACvD,qGAAqG;IACrG,MAAM,QAAQ,GAAG,SAAS;QACxB,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,IAAI,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,WAAW,IAAI,IAAI,CAAC;QAClE,CAAC,CAAC,IAAI,CAAC;IACT,OAAO,UAAU,CAAC;QAChB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ;QACR,OAAO,EAAE,KAAK,CAAC,OAAO;KACvB,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { nativeModule } from \"../native/native.service\";\nimport { usePreview } from \"./use-preview.service\";\nimport { useDevPreviewServersState } from \"../../stores/dev-preview-servers/dev-preview-servers.store\";\nimport { type Running, runningNow } from \"../../utils/previews/running.util\";\n\n/** What runs now, read from the package's state. Needs a `<PreviewProvider>` above it. */\nexport function useRunning(): Running {\n const preview = usePreview();\n const local = useDevPreviewServersState();\n const showLocal = __DEV__ && preview.devPreviewServers;\n // The first poll fills activeUrl a moment after the list mounts, but native knows it from the start.\n const localUrl = showLocal\n ? (local.activeUrl ?? nativeModule()?.state().localServer ?? null)\n : null;\n return runningNow({\n items: preview.items,\n activeId: preview.activeId,\n localUrl,\n servers: local.servers,\n });\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import { ManifestFile, Platform } from "../../utils/previews/layout.util";
2
+ export type BuiltFile = ManifestFile & {
3
+ absolutePath: string;
4
+ };
5
+ export declare const hermesVersion: (bundle: Buffer) => number | null;
6
+ /** The same release bundle and images the native build embeds, as Hermes bytecode. */
7
+ export declare function buildBundle(projectRoot: string, platform: Platform, entryFile?: string): {
8
+ files: BuiltFile[];
9
+ hermesVersion: number | null;
10
+ };
11
+ //# sourceMappingURL=bundle.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.service.d.ts","sourceRoot":"","sources":["../../../src/services/publish/bundle.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,YAAY,EACZ,QAAQ,EACT,MAAM,kCAAkC,CAAC;AAE1C,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAWhE,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,MAAM,GAAG,IAG9C,CAAC;AAEX,sFAAsF;AACtF,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,QAAQ,EAClB,SAAS,CAAC,EAAE,MAAM;;;EA8BnB"}
@@ -0,0 +1,46 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdtempSync, readdirSync, readFileSync, statSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, relative, sep } from "node:path";
6
+ import { BUNDLE_FILE, } from "../../utils/previews/layout.util";
7
+ const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)]);
8
+ /** Hermes bytecode starts with an 8-byte magic number, then its version as a 32-bit int. */
9
+ const HERMES_MAGIC = Buffer.from([
10
+ 0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f,
11
+ ]);
12
+ export const hermesVersion = (bundle) => bundle.length >= 12 && bundle.subarray(0, 8).equals(HERMES_MAGIC)
13
+ ? bundle.readUInt32LE(8)
14
+ : null;
15
+ /** The same release bundle and images the native build embeds, as Hermes bytecode. */
16
+ export function buildBundle(projectRoot, platform, entryFile) {
17
+ const out = mkdtempSync(join(tmpdir(), `preview-${platform}-`));
18
+ const args = [
19
+ "expo",
20
+ "export:embed",
21
+ "--platform",
22
+ platform,
23
+ "--dev",
24
+ "false",
25
+ "--bytecode",
26
+ "--bundle-output",
27
+ join(out, BUNDLE_FILE[platform]),
28
+ "--assets-dest",
29
+ out,
30
+ ];
31
+ if (entryFile)
32
+ args.push("--entry-file", entryFile);
33
+ execFileSync("npx", args, { cwd: projectRoot, stdio: "inherit" });
34
+ const files = walk(out).map((absolutePath) => {
35
+ const data = readFileSync(absolutePath);
36
+ return {
37
+ absolutePath,
38
+ path: relative(out, absolutePath).split(sep).join("/"),
39
+ sha256: createHash("sha256").update(data).digest("hex"),
40
+ size: statSync(absolutePath).size,
41
+ };
42
+ });
43
+ const bundle = readFileSync(join(out, BUNDLE_FILE[platform]));
44
+ return { files, hermesVersion: hermesVersion(bundle) };
45
+ }
46
+ //# sourceMappingURL=bundle.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.service.js","sourceRoot":"","sources":["../../../src/services/publish/bundle.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { mkdtempSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join, relative, sep } from \"node:path\";\n\nimport {\n BUNDLE_FILE,\n ManifestFile,\n Platform,\n} from \"../../utils/previews/layout.util\";\n\nexport type BuiltFile = ManifestFile & { absolutePath: string };\n\nconst walk = (dir: string): string[] =>\n readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>\n entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)],\n );\n\n/** Hermes bytecode starts with an 8-byte magic number, then its version as a 32-bit int. */\nconst HERMES_MAGIC = Buffer.from([\n 0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f,\n]);\nexport const hermesVersion = (bundle: Buffer): number | null =>\n bundle.length >= 12 && bundle.subarray(0, 8).equals(HERMES_MAGIC)\n ? bundle.readUInt32LE(8)\n : null;\n\n/** The same release bundle and images the native build embeds, as Hermes bytecode. */\nexport function buildBundle(\n projectRoot: string,\n platform: Platform,\n entryFile?: string,\n) {\n const out = mkdtempSync(join(tmpdir(), `preview-${platform}-`));\n const args = [\n \"expo\",\n \"export:embed\",\n \"--platform\",\n platform,\n \"--dev\",\n \"false\",\n \"--bytecode\",\n \"--bundle-output\",\n join(out, BUNDLE_FILE[platform]),\n \"--assets-dest\",\n out,\n ];\n if (entryFile) args.push(\"--entry-file\", entryFile);\n execFileSync(\"npx\", args, { cwd: projectRoot, stdio: \"inherit\" });\n\n const files: BuiltFile[] = walk(out).map((absolutePath) => {\n const data = readFileSync(absolutePath);\n return {\n absolutePath,\n path: relative(out, absolutePath).split(sep).join(\"/\"),\n sha256: createHash(\"sha256\").update(data).digest(\"hex\"),\n size: statSync(absolutePath).size,\n };\n });\n const bundle = readFileSync(join(out, BUNDLE_FILE[platform]));\n return { files, hermesVersion: hermesVersion(bundle) };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import { Compatibility, Platform } from "../../utils/previews/layout.util";
2
+ import { S3Config } from "../s3/s3.service";
3
+ import { AppConfig } from "../../utils/config/expo-config.util";
4
+ export type PublishOptions = {
5
+ projectRoot: string;
6
+ config: AppConfig;
7
+ compatibility: Compatibility;
8
+ s3: S3Config;
9
+ platforms: Platform[];
10
+ id: string;
11
+ title?: string;
12
+ branch: string;
13
+ commit: string;
14
+ author: string;
15
+ changelog: string;
16
+ entryFile?: string;
17
+ };
18
+ export declare function publish(options: PublishOptions): Promise<void>;
19
+ /** Removes the preview from every build's list. Its files stay until `prune`. */
20
+ export declare function unpublish(s3: S3Config, id: string): Promise<void>;
21
+ /** Deletes every file that no manifest refers to. */
22
+ export declare function prune(s3: S3Config): Promise<void>;
23
+ //# sourceMappingURL=commands.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.service.d.ts","sourceRoot":"","sources":["../../../src/services/publish/commands.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,aAAa,EAGb,QAAQ,EAST,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,QAAQ,EAOT,MAAM,kBAAkB,CAAC;AAU1B,OAAO,EACL,SAAS,EAGV,MAAM,qCAAqC,CAAC;AA8B7C,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;IAC7B,EAAE,EAAE,QAAQ,CAAC;IACb,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,iBA4FpD;AAED,iFAAiF;AACjF,wBAAsB,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,iBAoBvD;AAED,qDAAqD;AACrD,wBAAsB,KAAK,CAAC,EAAE,EAAE,QAAQ,iBAavC"}
@@ -0,0 +1,130 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { BUNDLE_FILE, ROOT_INDEX_KEY, SCHEMA, compatibilityKey, fileKey, manifestKey, platformIndexKey, } from "../../utils/previews/layout.util";
4
+ import { deleteObject, exists, getJson, listKeys, objectUrl, putObject, } from "../s3/s3.service";
5
+ import { previewTitle } from "../../utils/previews/preview-title.util";
6
+ import { buildBundle } from "./bundle.service";
7
+ import { shortChangelog } from "../../utils/publish/changelog.util";
8
+ import { indexesListing, removePreview, syncRootVersion, upsertPreview, } from "../../utils/publish/indexes.util";
9
+ import { appVersionOf, buildNumberOf, } from "../../utils/config/expo-config.util";
10
+ import { fingerprint } from "../cli/project.service";
11
+ const log = (message) => process.stdout.write(`${message}\n`);
12
+ /**
13
+ * Read, change, write back only if nobody else wrote in between (the ETag still matches), and
14
+ * start over when someone did. This is what keeps two publishes at once from losing an entry.
15
+ */
16
+ async function updateJson(s3, key, change, attempts = 8) {
17
+ for (let attempt = 1; attempt <= attempts; attempt++) {
18
+ const current = await getJson(s3, key);
19
+ const next = change(current?.data ?? null);
20
+ const written = await putObject(s3, key, JSON.stringify(next, null, 2), {
21
+ contentType: "application/json",
22
+ etag: current?.etag ?? null,
23
+ });
24
+ if (written)
25
+ return next;
26
+ await new Promise((r) => setTimeout(r, 100 * attempt + Math.random() * 200));
27
+ }
28
+ throw new Error(`Gave up writing ${key}: it kept changing under us`);
29
+ }
30
+ export async function publish(options) {
31
+ const { s3, id } = options;
32
+ const title = options.title || previewTitle(id);
33
+ const publishedAt = new Date().toISOString();
34
+ for (const platform of options.platforms) {
35
+ log(`\n${platform}: building ${id}`);
36
+ const { files, hermesVersion } = buildBundle(options.projectRoot, platform, options.entryFile);
37
+ const fp = await fingerprint(options.projectRoot, platform);
38
+ const version = appVersionOf(options.config, platform);
39
+ const key = compatibilityKey(options.compatibility, options.compatibility === "appVersion" ? version : fp);
40
+ let uploaded = 0;
41
+ for (const file of files) {
42
+ if (await exists(s3, fileKey(file.sha256)))
43
+ continue;
44
+ await putObject(s3, fileKey(file.sha256), readFileSync(file.absolutePath));
45
+ uploaded++;
46
+ }
47
+ log(`${platform}: ${files.length} files, ${uploaded} new`);
48
+ const info = {
49
+ id,
50
+ title,
51
+ branch: options.branch,
52
+ commit: options.commit,
53
+ author: options.author,
54
+ publishedAt,
55
+ appVersion: version,
56
+ buildNumber: buildNumberOf(options.config, platform),
57
+ compatibility: options.compatibility,
58
+ fingerprint: fp,
59
+ };
60
+ const manifest = {
61
+ schema: SCHEMA,
62
+ ...info,
63
+ platform,
64
+ key,
65
+ bundle: BUNDLE_FILE[platform],
66
+ hermesVersion,
67
+ changelog: options.changelog,
68
+ files: files.map(({ path, sha256, size }) => ({ path, sha256, size })),
69
+ };
70
+ const manifestPath = manifestKey(platform, key, id);
71
+ await putObject(s3, manifestPath, JSON.stringify(manifest, null, 2), {
72
+ contentType: "application/json",
73
+ });
74
+ const entry = {
75
+ ...info,
76
+ revision: createHash("sha256")
77
+ .update(manifest.files
78
+ .map((f) => `${f.path}:${f.sha256}`)
79
+ .sort()
80
+ .join("\n"))
81
+ .digest("hex")
82
+ .slice(0, 16),
83
+ size: files.reduce((sum, f) => sum + f.size, 0),
84
+ changelog: shortChangelog(options.changelog),
85
+ manifest: manifestPath,
86
+ };
87
+ // Last, so an app never sees an entry whose files are not all there yet.
88
+ const index = await updateJson(s3, platformIndexKey(platform, key), (current) => upsertPreview(current, platform, { key, compatibility: options.compatibility }, entry, publishedAt));
89
+ await updateJson(s3, ROOT_INDEX_KEY, (root) => syncRootVersion(root, index, publishedAt));
90
+ log(`${platform}: published ${id} for build ${key}`);
91
+ log(`${platform}: ${objectUrl(s3, platformIndexKey(platform, key))}`);
92
+ }
93
+ }
94
+ /** Removes the preview from every build's list. Its files stay until `prune`. */
95
+ export async function unpublish(s3, id) {
96
+ const root = await getJson(s3, ROOT_INDEX_KEY);
97
+ const keys = indexesListing(root?.data ?? null, id);
98
+ if (keys.length === 0) {
99
+ log(`${id} is not published`);
100
+ return;
101
+ }
102
+ const now = new Date().toISOString();
103
+ for (const key of keys) {
104
+ const index = await updateJson(s3, key, (current) => {
105
+ if (!current)
106
+ throw new Error(`${key} is listed in ${ROOT_INDEX_KEY} but missing`);
107
+ return removePreview(current, id, now);
108
+ });
109
+ await deleteObject(s3, manifestKey(index.platform, index.key, id));
110
+ await updateJson(s3, ROOT_INDEX_KEY, (current) => syncRootVersion(current, index, now));
111
+ log(`unpublished ${id} from ${key}`);
112
+ }
113
+ }
114
+ /** Deletes every file that no manifest refers to. */
115
+ export async function prune(s3) {
116
+ const referenced = new Set();
117
+ for (const key of await listKeys(s3, "previews/")) {
118
+ const manifest = await getJson(s3, key);
119
+ manifest?.data.files.forEach((f) => referenced.add(fileKey(f.sha256)));
120
+ }
121
+ let removed = 0;
122
+ for (const key of await listKeys(s3, "files/")) {
123
+ if (referenced.has(key))
124
+ continue;
125
+ await deleteObject(s3, key);
126
+ removed++;
127
+ }
128
+ log(`pruned ${removed} files, kept ${referenced.size}`);
129
+ }
130
+ //# sourceMappingURL=commands.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.service.js","sourceRoot":"","sources":["../../../src/services/publish/commands.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nimport {\n BUNDLE_FILE,\n Compatibility,\n IndexEntry,\n Manifest,\n Platform,\n PlatformIndex,\n ROOT_INDEX_KEY,\n RootIndex,\n SCHEMA,\n compatibilityKey,\n fileKey,\n manifestKey,\n platformIndexKey,\n} from \"../../utils/previews/layout.util\";\nimport {\n S3Config,\n deleteObject,\n exists,\n getJson,\n listKeys,\n objectUrl,\n putObject,\n} from \"../s3/s3.service\";\nimport { previewTitle } from \"../../utils/previews/preview-title.util\";\nimport { buildBundle } from \"./bundle.service\";\nimport { shortChangelog } from \"../../utils/publish/changelog.util\";\nimport {\n indexesListing,\n removePreview,\n syncRootVersion,\n upsertPreview,\n} from \"../../utils/publish/indexes.util\";\nimport {\n AppConfig,\n appVersionOf,\n buildNumberOf,\n} from \"../../utils/config/expo-config.util\";\nimport { fingerprint } from \"../cli/project.service\";\n\nconst log = (message: string) => process.stdout.write(`${message}\\n`);\n\n/**\n * Read, change, write back only if nobody else wrote in between (the ETag still matches), and\n * start over when someone did. This is what keeps two publishes at once from losing an entry.\n */\nasync function updateJson<T>(\n s3: S3Config,\n key: string,\n change: (current: T | null) => T,\n attempts = 8,\n): Promise<T> {\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const current = await getJson<T>(s3, key);\n const next = change(current?.data ?? null);\n const written = await putObject(s3, key, JSON.stringify(next, null, 2), {\n contentType: \"application/json\",\n etag: current?.etag ?? null,\n });\n if (written) return next;\n await new Promise((r) =>\n setTimeout(r, 100 * attempt + Math.random() * 200),\n );\n }\n throw new Error(`Gave up writing ${key}: it kept changing under us`);\n}\n\nexport type PublishOptions = {\n projectRoot: string;\n config: AppConfig;\n compatibility: Compatibility;\n s3: S3Config;\n platforms: Platform[];\n id: string;\n title?: string;\n branch: string;\n commit: string;\n author: string;\n changelog: string;\n entryFile?: string;\n};\n\nexport async function publish(options: PublishOptions) {\n const { s3, id } = options;\n const title = options.title || previewTitle(id);\n const publishedAt = new Date().toISOString();\n\n for (const platform of options.platforms) {\n log(`\\n${platform}: building ${id}`);\n const { files, hermesVersion } = buildBundle(\n options.projectRoot,\n platform,\n options.entryFile,\n );\n const fp = await fingerprint(options.projectRoot, platform);\n const version = appVersionOf(options.config, platform);\n const key = compatibilityKey(\n options.compatibility,\n options.compatibility === \"appVersion\" ? version : fp,\n );\n\n let uploaded = 0;\n for (const file of files) {\n if (await exists(s3, fileKey(file.sha256))) continue;\n await putObject(\n s3,\n fileKey(file.sha256),\n readFileSync(file.absolutePath),\n );\n uploaded++;\n }\n log(`${platform}: ${files.length} files, ${uploaded} new`);\n\n const info = {\n id,\n title,\n branch: options.branch,\n commit: options.commit,\n author: options.author,\n publishedAt,\n appVersion: version,\n buildNumber: buildNumberOf(options.config, platform),\n compatibility: options.compatibility,\n fingerprint: fp,\n };\n const manifest: Manifest = {\n schema: SCHEMA,\n ...info,\n platform,\n key,\n bundle: BUNDLE_FILE[platform],\n hermesVersion,\n changelog: options.changelog,\n files: files.map(({ path, sha256, size }) => ({ path, sha256, size })),\n };\n const manifestPath = manifestKey(platform, key, id);\n await putObject(s3, manifestPath, JSON.stringify(manifest, null, 2), {\n contentType: \"application/json\",\n });\n\n const entry: IndexEntry = {\n ...info,\n revision: createHash(\"sha256\")\n .update(\n manifest.files\n .map((f) => `${f.path}:${f.sha256}`)\n .sort()\n .join(\"\\n\"),\n )\n .digest(\"hex\")\n .slice(0, 16),\n size: files.reduce((sum, f) => sum + f.size, 0),\n changelog: shortChangelog(options.changelog),\n manifest: manifestPath,\n };\n // Last, so an app never sees an entry whose files are not all there yet.\n const index = await updateJson<PlatformIndex>(\n s3,\n platformIndexKey(platform, key),\n (current) =>\n upsertPreview(\n current,\n platform,\n { key, compatibility: options.compatibility },\n entry,\n publishedAt,\n ),\n );\n await updateJson<RootIndex>(s3, ROOT_INDEX_KEY, (root) =>\n syncRootVersion(root, index, publishedAt),\n );\n log(`${platform}: published ${id} for build ${key}`);\n log(`${platform}: ${objectUrl(s3, platformIndexKey(platform, key))}`);\n }\n}\n\n/** Removes the preview from every build's list. Its files stay until `prune`. */\nexport async function unpublish(s3: S3Config, id: string) {\n const root = await getJson<RootIndex>(s3, ROOT_INDEX_KEY);\n const keys = indexesListing(root?.data ?? null, id);\n if (keys.length === 0) {\n log(`${id} is not published`);\n return;\n }\n const now = new Date().toISOString();\n for (const key of keys) {\n const index = await updateJson<PlatformIndex>(s3, key, (current) => {\n if (!current)\n throw new Error(`${key} is listed in ${ROOT_INDEX_KEY} but missing`);\n return removePreview(current, id, now);\n });\n await deleteObject(s3, manifestKey(index.platform, index.key, id));\n await updateJson<RootIndex>(s3, ROOT_INDEX_KEY, (current) =>\n syncRootVersion(current, index, now),\n );\n log(`unpublished ${id} from ${key}`);\n }\n}\n\n/** Deletes every file that no manifest refers to. */\nexport async function prune(s3: S3Config) {\n const referenced = new Set<string>();\n for (const key of await listKeys(s3, \"previews/\")) {\n const manifest = await getJson<Manifest>(s3, key);\n manifest?.data.files.forEach((f) => referenced.add(fileKey(f.sha256)));\n }\n let removed = 0;\n for (const key of await listKeys(s3, \"files/\")) {\n if (referenced.has(key)) continue;\n await deleteObject(s3, key);\n removed++;\n }\n log(`pruned ${removed} files, kept ${referenced.size}`);\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * An EAS environment's variables, as `eas env:pull` gives them. Pulled into a temp folder outside
3
+ * the project and deleted after. Secret-visibility variables never leave EAS servers: `env:pull`
4
+ * writes them as comments, so they are missing here.
5
+ */
6
+ export declare function pullEasEnv(projectRoot: string, environment: string): Record<string, string>;
7
+ /** Adds the EAS values to `env`; a variable already set there (the shell's) wins. */
8
+ export declare function applyEasEnv(projectRoot: string, environment: string, env?: Record<string, string | undefined>): void;
9
+ //# sourceMappingURL=eas-env.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eas-env.service.d.ts","sourceRoot":"","sources":["../../../src/services/publish/eas-env.service.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBxB;AAsBD,qFAAqF;AACrF,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,QAMtD"}
@@ -0,0 +1,49 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parseEnv } from "node:util";
6
+ /**
7
+ * An EAS environment's variables, as `eas env:pull` gives them. Pulled into a temp folder outside
8
+ * the project and deleted after. Secret-visibility variables never leave EAS servers: `env:pull`
9
+ * writes them as comments, so they are missing here.
10
+ */
11
+ export function pullEasEnv(projectRoot, environment) {
12
+ const dir = mkdtempSync(join(tmpdir(), "preview-eas-env-"));
13
+ const file = join(dir, ".env");
14
+ try {
15
+ execFileSync("eas", [
16
+ "env:pull",
17
+ "--environment",
18
+ environment,
19
+ "--non-interactive",
20
+ "--path",
21
+ file,
22
+ ], { cwd: projectRoot, encoding: "utf8", stdio: "pipe" });
23
+ return parseEnv(readFileSync(file, "utf8"));
24
+ }
25
+ catch (error) {
26
+ throw easError(error, environment);
27
+ }
28
+ finally {
29
+ rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ }
32
+ function easError(error, environment) {
33
+ const e = error;
34
+ if (e.code === "ENOENT")
35
+ return new Error("--environment needs eas-cli: npm install -g eas-cli, then eas login.");
36
+ const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
37
+ if (output.includes("An Expo user account is required"))
38
+ return new Error("--environment needs an Expo login: run eas login, or set EXPO_TOKEN in CI.");
39
+ if (output.includes("eas init"))
40
+ return new Error("--environment needs the project linked to EAS: run eas init in the app's root.");
41
+ return new Error(`eas env:pull --environment ${environment} failed:\n${output.trim() || String(error)}`);
42
+ }
43
+ /** Adds the EAS values to `env`; a variable already set there (the shell's) wins. */
44
+ export function applyEasEnv(projectRoot, environment, env = process.env) {
45
+ for (const [name, value] of Object.entries(pullEasEnv(projectRoot, environment)))
46
+ if (env[name] === undefined)
47
+ env[name] = value;
48
+ }
49
+ //# sourceMappingURL=eas-env.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eas-env.service.js","sourceRoot":"","sources":["../../../src/services/publish/eas-env.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,WAAmB,EACnB,WAAmB;IAEnB,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC;QACH,YAAY,CACV,KAAK,EACL;YACE,UAAU;YACV,eAAe;YACf,WAAW;YACX,mBAAmB;YACnB,QAAQ;YACR,IAAI;SACL,EACD,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CACtD,CAAC;QACF,OAAO,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA2B,CAAC;IACxE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IACrC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc,EAAE,WAAmB;IACnD,MAAM,CAAC,GAAG,KAA4D,CAAC;IACvE,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;QACrB,OAAO,IAAI,KAAK,CACd,sEAAsE,CACvE,CAAC;IACJ,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;IACpD,IAAI,MAAM,CAAC,QAAQ,CAAC,kCAAkC,CAAC;QACrD,OAAO,IAAI,KAAK,CACd,4EAA4E,CAC7E,CAAC;IACJ,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;QAC7B,OAAO,IAAI,KAAK,CACd,gFAAgF,CACjF,CAAC;IACJ,OAAO,IAAI,KAAK,CACd,8BAA8B,WAAW,aAAa,MAAM,CAAC,IAAI,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CACvF,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,WAAW,CACzB,WAAmB,EACnB,WAAmB,EACnB,MAA0C,OAAO,CAAC,GAAG;IAErD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CACxC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CACrC;QACC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;AACnD,CAAC","sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { mkdtempSync, readFileSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { parseEnv } from \"node:util\";\n\n/**\n * An EAS environment's variables, as `eas env:pull` gives them. Pulled into a temp folder outside\n * the project and deleted after. Secret-visibility variables never leave EAS servers: `env:pull`\n * writes them as comments, so they are missing here.\n */\nexport function pullEasEnv(\n projectRoot: string,\n environment: string,\n): Record<string, string> {\n const dir = mkdtempSync(join(tmpdir(), \"preview-eas-env-\"));\n const file = join(dir, \".env\");\n try {\n execFileSync(\n \"eas\",\n [\n \"env:pull\",\n \"--environment\",\n environment,\n \"--non-interactive\",\n \"--path\",\n file,\n ],\n { cwd: projectRoot, encoding: \"utf8\", stdio: \"pipe\" },\n );\n return parseEnv(readFileSync(file, \"utf8\")) as Record<string, string>;\n } catch (error) {\n throw easError(error, environment);\n } finally {\n rmSync(dir, { recursive: true, force: true });\n }\n}\n\nfunction easError(error: unknown, environment: string): Error {\n const e = error as { code?: string; stdout?: string; stderr?: string };\n if (e.code === \"ENOENT\")\n return new Error(\n \"--environment needs eas-cli: npm install -g eas-cli, then eas login.\",\n );\n const output = `${e.stdout ?? \"\"}${e.stderr ?? \"\"}`;\n if (output.includes(\"An Expo user account is required\"))\n return new Error(\n \"--environment needs an Expo login: run eas login, or set EXPO_TOKEN in CI.\",\n );\n if (output.includes(\"eas init\"))\n return new Error(\n \"--environment needs the project linked to EAS: run eas init in the app's root.\",\n );\n return new Error(\n `eas env:pull --environment ${environment} failed:\\n${output.trim() || String(error)}`,\n );\n}\n\n/** Adds the EAS values to `env`; a variable already set there (the shell's) wins. */\nexport function applyEasEnv(\n projectRoot: string,\n environment: string,\n env: Record<string, string | undefined> = process.env,\n) {\n for (const [name, value] of Object.entries(\n pullEasEnv(projectRoot, environment),\n ))\n if (env[name] === undefined) env[name] = value;\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { type Server } from "node:http";
2
+ import { type RegisteredServer } from "../../constants/dev-preview-servers/dev-preview-servers.const";
3
+ /** The registry's memory: servers by id, each dropped once it misses its heartbeats. */
4
+ export declare class ServerList {
5
+ private readonly ttlMs;
6
+ private entries;
7
+ constructor(ttlMs?: number);
8
+ upsert(server: RegisteredServer, now: number): void;
9
+ remove(id: string): void;
10
+ /** The live servers, after dropping the ones whose heartbeat stopped. */
11
+ alive(now: number): RegisteredServer[];
12
+ }
13
+ /**
14
+ * `GET /servers` lists the live servers; `POST /servers` registers one or renews its heartbeat;
15
+ * `DELETE /servers/<id>` removes one. Plain HTTP, so the app reads it with `fetch` and `curl`
16
+ * shows it too. Once it has been empty for `idleExitMs`, `onIdle` is called.
17
+ */
18
+ export declare function createRegistry(options?: {
19
+ ttlMs?: number;
20
+ idleExitMs?: number;
21
+ onIdle?: () => void;
22
+ now?: () => number;
23
+ }): {
24
+ server: Server;
25
+ list: ServerList;
26
+ };
27
+ //# sourceMappingURL=registry.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.service.d.ts","sourceRoot":"","sources":["../../../src/services/registry/registry.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAE5E,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,+DAA+D,CAAC;AAIvE,wFAAwF;AACxF,qBAAa,UAAU;IAGT,OAAO,CAAC,QAAQ,CAAC,KAAK;IAFlC,OAAO,CAAC,OAAO,CAA4B;gBAEd,KAAK,SAAgB;IAElD,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM;IAI5C,MAAM,CAAC,EAAE,EAAE,MAAM;IAIjB,yEAAyE;IACzE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAQvC;AAUD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE;IACP,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACf,GACL;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CA+CtC"}
@@ -0,0 +1,89 @@
1
+ import { createServer } from "node:http";
2
+ import { SERVER_TTL_MS, } from "../../constants/dev-preview-servers/dev-preview-servers.const";
3
+ /** The registry's memory: servers by id, each dropped once it misses its heartbeats. */
4
+ export class ServerList {
5
+ ttlMs;
6
+ entries = new Map();
7
+ constructor(ttlMs = SERVER_TTL_MS) {
8
+ this.ttlMs = ttlMs;
9
+ }
10
+ upsert(server, now) {
11
+ this.entries.set(server.id, { server, lastSeen: now });
12
+ }
13
+ remove(id) {
14
+ this.entries.delete(id);
15
+ }
16
+ /** The live servers, after dropping the ones whose heartbeat stopped. */
17
+ alive(now) {
18
+ for (const [id, entry] of this.entries) {
19
+ if (now - entry.lastSeen > this.ttlMs)
20
+ this.entries.delete(id);
21
+ }
22
+ return [...this.entries.values()]
23
+ .map((e) => e.server)
24
+ .sort((a, b) => a.port - b.port);
25
+ }
26
+ }
27
+ const readBody = (req) => new Promise((resolve, reject) => {
28
+ let body = "";
29
+ req.on("data", (chunk) => (body += chunk));
30
+ req.on("end", () => resolve(body));
31
+ req.on("error", reject);
32
+ });
33
+ /**
34
+ * `GET /servers` lists the live servers; `POST /servers` registers one or renews its heartbeat;
35
+ * `DELETE /servers/<id>` removes one. Plain HTTP, so the app reads it with `fetch` and `curl`
36
+ * shows it too. Once it has been empty for `idleExitMs`, `onIdle` is called.
37
+ */
38
+ export function createRegistry(options = {}) {
39
+ const now = options.now ?? Date.now;
40
+ const list = new ServerList(options.ttlMs);
41
+ let emptySince = now();
42
+ const server = createServer(async (req, res) => {
43
+ const send = (status, body) => {
44
+ res.writeHead(status, {
45
+ "content-type": "application/json",
46
+ "cache-control": "no-store",
47
+ });
48
+ res.end(body === undefined ? "" : JSON.stringify(body));
49
+ };
50
+ try {
51
+ if (req.method === "GET" && req.url === "/servers") {
52
+ send(200, { servers: list.alive(now()) });
53
+ }
54
+ else if (req.method === "POST" && req.url === "/servers") {
55
+ const entry = JSON.parse(await readBody(req));
56
+ if (typeof entry.id !== "string" || typeof entry.port !== "number") {
57
+ send(400, { error: "id and port are required" });
58
+ return;
59
+ }
60
+ list.upsert(entry, now());
61
+ send(204);
62
+ }
63
+ else if (req.method === "DELETE" && req.url?.startsWith("/servers/")) {
64
+ list.remove(decodeURIComponent(req.url.slice("/servers/".length)));
65
+ send(204);
66
+ }
67
+ else {
68
+ send(404, { error: "not found" });
69
+ }
70
+ }
71
+ catch (error) {
72
+ send(400, {
73
+ error: error instanceof Error ? error.message : String(error),
74
+ });
75
+ }
76
+ });
77
+ if (options.onIdle) {
78
+ const timer = setInterval(() => {
79
+ if (list.alive(now()).length > 0)
80
+ emptySince = now();
81
+ else if (now() - emptySince > (options.idleExitMs ?? 30000))
82
+ options.onIdle?.();
83
+ }, 1000);
84
+ timer.unref();
85
+ server.on("close", () => clearInterval(timer));
86
+ }
87
+ return { server, list };
88
+ }
89
+ //# sourceMappingURL=registry.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.service.js","sourceRoot":"","sources":["../../../src/services/registry/registry.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { createServer, type IncomingMessage, type Server } from \"node:http\";\n\nimport {\n type RegisteredServer,\n SERVER_TTL_MS,\n} from \"../../constants/dev-preview-servers/dev-preview-servers.const\";\n\ntype Entry = { server: RegisteredServer; lastSeen: number };\n\n/** The registry's memory: servers by id, each dropped once it misses its heartbeats. */\nexport class ServerList {\n private entries = new Map<string, Entry>();\n\n constructor(private readonly ttlMs = SERVER_TTL_MS) {}\n\n upsert(server: RegisteredServer, now: number) {\n this.entries.set(server.id, { server, lastSeen: now });\n }\n\n remove(id: string) {\n this.entries.delete(id);\n }\n\n /** The live servers, after dropping the ones whose heartbeat stopped. */\n alive(now: number): RegisteredServer[] {\n for (const [id, entry] of this.entries) {\n if (now - entry.lastSeen > this.ttlMs) this.entries.delete(id);\n }\n return [...this.entries.values()]\n .map((e) => e.server)\n .sort((a, b) => a.port - b.port);\n }\n}\n\nconst readBody = (req: IncomingMessage) =>\n new Promise<string>((resolve, reject) => {\n let body = \"\";\n req.on(\"data\", (chunk) => (body += chunk));\n req.on(\"end\", () => resolve(body));\n req.on(\"error\", reject);\n });\n\n/**\n * `GET /servers` lists the live servers; `POST /servers` registers one or renews its heartbeat;\n * `DELETE /servers/<id>` removes one. Plain HTTP, so the app reads it with `fetch` and `curl`\n * shows it too. Once it has been empty for `idleExitMs`, `onIdle` is called.\n */\nexport function createRegistry(\n options: {\n ttlMs?: number;\n idleExitMs?: number;\n onIdle?: () => void;\n now?: () => number;\n } = {},\n): { server: Server; list: ServerList } {\n const now = options.now ?? Date.now;\n const list = new ServerList(options.ttlMs);\n let emptySince = now();\n\n const server = createServer(async (req, res) => {\n const send = (status: number, body?: unknown) => {\n res.writeHead(status, {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n });\n res.end(body === undefined ? \"\" : JSON.stringify(body));\n };\n try {\n if (req.method === \"GET\" && req.url === \"/servers\") {\n send(200, { servers: list.alive(now()) });\n } else if (req.method === \"POST\" && req.url === \"/servers\") {\n const entry = JSON.parse(await readBody(req)) as RegisteredServer;\n if (typeof entry.id !== \"string\" || typeof entry.port !== \"number\") {\n send(400, { error: \"id and port are required\" });\n return;\n }\n list.upsert(entry, now());\n send(204);\n } else if (req.method === \"DELETE\" && req.url?.startsWith(\"/servers/\")) {\n list.remove(decodeURIComponent(req.url.slice(\"/servers/\".length)));\n send(204);\n } else {\n send(404, { error: \"not found\" });\n }\n } catch (error) {\n send(400, {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n });\n\n if (options.onIdle) {\n const timer = setInterval(() => {\n if (list.alive(now()).length > 0) emptySince = now();\n else if (now() - emptySince > (options.idleExitMs ?? 30000))\n options.onIdle?.();\n }, 1000);\n timer.unref();\n server.on(\"close\", () => clearInterval(timer));\n }\n return { server, list };\n}\n"]}
@@ -0,0 +1,59 @@
1
+ import { Credentials } from "../../utils/s3/sigv4.util";
2
+ /** Any S3-compatible bucket: AWS S3, Cloudflare R2, MinIO and others. Path-style URLs. */
3
+ export type S3Config = {
4
+ endpoint: string;
5
+ /** Left out when `endpoint` already is the bucket's base, as for the app's unsigned reads. */
6
+ bucket?: string;
7
+ /** Folder inside the bucket that everything goes under, so several apps can share one bucket. */
8
+ prefix?: string;
9
+ region: string;
10
+ /** Write key in the CLI, read-only key in the app. Without one, requests go unsigned. */
11
+ credentials?: Credentials;
12
+ /** Sent with every request, such as a header a proxy or tunnel in front of the bucket wants. */
13
+ headers?: Record<string, string>;
14
+ };
15
+ /** MinIO and R2 accept it too, so a region is only needed for AWS buckets elsewhere. */
16
+ export declare const DEFAULT_REGION = "us-east-1";
17
+ /**
18
+ * Splits one URL, `<endpoint>/<bucket>[/<prefix>]`, so a single setting says where everything
19
+ * lives: `http://localhost:9000/previews-public`, `https://<account>.r2.cloudflarestorage.com/previews/my-app`.
20
+ */
21
+ export declare function parseBucketUrl(value: string): Pick<S3Config, "endpoint" | "bucket" | "prefix">;
22
+ export declare const objectUrl: (config: S3Config, key: string) => string;
23
+ export declare class S3Error extends Error {
24
+ readonly status: number;
25
+ readonly key: string;
26
+ constructor(status: number, key: string, body: string);
27
+ }
28
+ type Request = {
29
+ method: string;
30
+ key?: string;
31
+ query?: Record<string, string>;
32
+ body?: Uint8Array | string;
33
+ headers?: Record<string, string>;
34
+ };
35
+ export declare function s3Fetch(config: S3Config, request: Request): Promise<Response>;
36
+ /** Null when the object does not exist. The ETag is what a conditional put needs. */
37
+ export declare function getJson<T>(config: S3Config, key: string): Promise<{
38
+ data: T;
39
+ etag: string | null;
40
+ } | null>;
41
+ /**
42
+ * Something in front of the bucket answered instead of it: most often a private tunnel or proxy
43
+ * sending a login page. Say what came back and from where, instead of a bare JSON parse error.
44
+ */
45
+ export declare function notJsonMessage(url: string, res: Response, body: string): string;
46
+ /**
47
+ * False when a condition failed (someone else wrote first), so the caller can read again and
48
+ * retry. `etag: null` means "only if it does not exist yet".
49
+ */
50
+ export declare function putObject(config: S3Config, key: string, body: Uint8Array | string, options?: {
51
+ contentType?: string;
52
+ etag?: string | null;
53
+ }): Promise<boolean>;
54
+ export declare function exists(config: S3Config, key: string): Promise<boolean>;
55
+ export declare function deleteObject(config: S3Config, key: string): Promise<void>;
56
+ /** Every key under the prefix (ListObjectsV2, following continuation tokens). */
57
+ export declare function listKeys(config: S3Config, prefix: string): Promise<string[]>;
58
+ export {};
59
+ //# sourceMappingURL=s3.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.service.d.ts","sourceRoot":"","sources":["../../../src/services/s3/s3.service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAIZ,MAAM,2BAA2B,CAAC;AAEnC,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,cAAc,cAAc,CAAC;AAM1C;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GACZ,IAAI,CAAC,QAAQ,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAiBlD;AAUD,eAAO,MAAM,SAAS,GAAI,QAAQ,QAAQ,EAAE,KAAK,MAAM,WACO,CAAC;AAE/D,qBAAa,OAAQ,SAAQ,KAAK;IAE9B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM;gBADX,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM;CAIf;AAED,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,wBAAsB,OAAO,CAC3B,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAED,qFAAqF;AACrF,wBAAsB,OAAO,CAAC,CAAC,EAC7B,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAAC,CAWlD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,GACX,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,GAAG,MAAM,EACzB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC3D,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAK5E;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAIf;AAUD,iFAAiF;AACjF,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBnB"}