@imagina-build/ias-cli 0.2.0 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imagina-build/ias-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "The command line of Imagina Application Services (IAS): ias.json, OTA updates, internal distribution and the checks of a project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,10 +18,14 @@ import { readSecret, readSecretFile, isReference, tokenKind } from '../../secret
18
18
 
19
19
  export const PLATFORMS = ['ios', 'android'];
20
20
 
21
- /** The environment of the app's tools: none of this shell's EXPO_PUBLIC_* (they would go into the bundle) or secrets. */
22
- function appEnv() {
21
+ /**
22
+ * The environment of the app's tools: none of this shell's EXPO_PUBLIC_* (they would go into the bundle) or secrets.
23
+ * With `tmp`, TMPDIR is a folder of this run: Metro keeps its cache in TMPDIR (metro-cache) and a cache from another
24
+ * run brings the paths of its folder into the bundle. The runner does the same for each build.
25
+ */
26
+ function appEnv(tmp) {
23
27
  const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(EXPO_PUBLIC_|OTA_|IAS_)/.test(key) && !['EXPO_TOKEN', 'NODE_ENV'].includes(key)));
24
- return { ...env, CI: '1', EXPO_NO_TELEMETRY: '1' };
28
+ return { ...env, CI: '1', EXPO_NO_TELEMETRY: '1', ...(tmp === undefined ? {} : { TMPDIR: tmp }) };
25
29
  }
26
30
 
27
31
  /**
@@ -134,11 +138,13 @@ function copyOf(settings, ref, output) {
134
138
  run('git', ['archive', '--format=tar', '-o', join(dir, 'source.tar'), commit], { cwd: settings.projectDir });
135
139
  const app = join(dir, 'app');
136
140
  run('mkdir', [app]);
141
+ const tmp = join(dir, 'tmp');
142
+ run('mkdir', [tmp]);
137
143
  run('tar', ['-xf', join(dir, 'source.tar'), '-C', app]);
138
144
  const install = INSTALLS.find(([lockfile]) => existsSync(join(app, lockfile)));
139
145
  if (!install) throw new Error('the commit has no lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml or bun.lock): the copy could not install the same dependencies');
140
146
  output.progress(`${install[1]} ${install[2][0]} in a copy of ${commit.slice(0, 10)}…`);
141
- gently(install[1], install[2], { cwd: app, env: appEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
147
+ gently(install[1], install[2], { cwd: app, env: appEnv(tmp), stdio: ['ignore', 'pipe', 'pipe'] });
142
148
  const mustContain = [];
143
149
  if (settings.envFile) {
144
150
  const envFile = join(settings.projectDir, settings.envFile);
@@ -152,11 +158,11 @@ function copyOf(settings, ref, output) {
152
158
  } else if (settings.bundleEnv.length > 0) {
153
159
  throw new Error(`${settings.names.config} lists bundleEnv but no envFile to take them from`);
154
160
  }
155
- return { app, commit, mustContain };
161
+ return { app, tmp, commit, mustContain };
156
162
  }
157
163
 
158
- function runtimeVersion(cwd, platform) {
159
- const output = run('npx', ['expo-updates', 'runtimeversion:resolve', '--platform', platform], { cwd, env: appEnv(), label: 'expo-updates runtimeversion:resolve' });
164
+ function runtimeVersion(cwd, platform, tmp) {
165
+ const output = run('npx', ['expo-updates', 'runtimeversion:resolve', '--platform', platform], { cwd, env: appEnv(tmp), label: 'expo-updates runtimeversion:resolve' });
160
166
  return JSON.parse(output.slice(output.indexOf('{'))).runtimeVersion;
161
167
  }
162
168
 
@@ -174,13 +180,13 @@ export async function publish(settings, { ref = 'HEAD', message, platforms = PLA
174
180
  const copy = copyOf(settings, ref, output);
175
181
  const dist = join(dirname(copy.app), 'dist');
176
182
  output.progress(`exporting ${copy.commit.slice(0, 10)} (${platforms.join(', ')})…`);
177
- gently('npx', ['expo', 'export', ...platforms.flatMap((platform) => ['--platform', platform]), '--output-dir', dist], { cwd: copy.app, env: appEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
183
+ gently('npx', ['expo', 'export', ...platforms.flatMap((platform) => ['--platform', platform]), '--output-dir', dist], { cwd: copy.app, env: appEnv(copy.tmp), stdio: ['ignore', 'pipe', 'pipe'] });
178
184
  const { manifests, files } = buildManifests({
179
185
  dist,
180
186
  metadata: JSON.parse(readFileSync(join(dist, 'metadata.json'), 'utf8')),
181
187
  platforms,
182
- runtimes: Object.fromEntries(platforms.map((platform) => [platform, runtimeVersion(copy.app, platform)])),
183
- expoClient: JSON.parse(run('npx', ['expo', 'config', '--type', 'public', '--json'], { cwd: copy.app, env: appEnv(), label: 'expo config' })),
188
+ runtimes: Object.fromEntries(platforms.map((platform) => [platform, runtimeVersion(copy.app, platform, copy.tmp)])),
189
+ expoClient: JSON.parse(run('npx', ['expo', 'config', '--type', 'public', '--json'], { cwd: copy.app, env: appEnv(copy.tmp), label: 'expo config' })),
184
190
  assetPath: settings.assetPath,
185
191
  createdAt: new Date().toISOString(),
186
192
  mustContain: copy.mustContain,
@@ -387,6 +393,12 @@ export async function check(settings, { platforms = PLATFORMS }, output) {
387
393
  return { checks };
388
394
  }
389
395
 
396
+ /**
397
+ * The URL of a file the manifest points to, asked of the server this publication went to: a manifest may name any of
398
+ * the hosts the server answers for (the old domain, the new one), and they all serve the same path.
399
+ */
400
+ const assetOf = (url, settings) => new URL(new URL(url, settings.manifestUrl).pathname, settings.base);
401
+
390
402
  /**
391
403
  * Asks for the manifest as a phone would and checks the signature, the id and every file it points to. `checked`
392
404
  * holds the files already checked in this run (one per settings): the platforms share most of them. `channel` and
@@ -418,7 +430,7 @@ export async function checkServed(settings, platform, runtime, expectedId, outpu
418
430
  checked.add(asset.hash);
419
431
  // Each file once, and the bundle also without gzip.
420
432
  for (const encoding of index === 0 ? [undefined, 'identity'] : [undefined]) {
421
- const answer = await download(new URL(asset.url, settings.manifestUrl), encoding ? { 'accept-encoding': encoding } : {}, output);
433
+ const answer = await download(assetOf(asset.url, settings), encoding ? { 'accept-encoding': encoding } : {}, output);
422
434
  if (!answer.ok) throw new Error(`${platform}: ${asset.url} answered ${answer.status}`);
423
435
  if (sha256(Buffer.from(await answer.arrayBuffer())) !== asset.hash) throw new Error(`${platform}: ${asset.url} does not match its hash`);
424
436
  }
package/src/config.mjs CHANGED
@@ -163,7 +163,7 @@ function iasUpdateSettings({ dir, config }, env) {
163
163
  manifestUrl: manifestUrl.href,
164
164
  base,
165
165
  project: manifestUrl.pathname.match(MANIFEST_PATH)[1],
166
- assetPath: `${new URL(base).pathname}/assets/`,
166
+ assetPath: `${base}/assets/`,
167
167
  keyid,
168
168
  certificate: new X509Certificate(readFileSync(certificateFile)),
169
169
  signingSource: appCertificate ? 'app.json' : 'updates.signing of ias.json',
@@ -203,7 +203,7 @@ function legacyUpdateSettings(projectDir, env) {
203
203
  manifestUrl: manifestUrl.href,
204
204
  base,
205
205
  project: manifestUrl.pathname.match(MANIFEST_PATH)[1],
206
- assetPath: `${new URL(base).pathname}/assets/`,
206
+ assetPath: `${base}/assets/`,
207
207
  keyid: updates.codeSigningMetadata.keyid,
208
208
  certificate,
209
209
  privateKey,
package/src/ota.mjs CHANGED
@@ -30,9 +30,10 @@ export function readEnvFile(path, names) {
30
30
  }
31
31
 
32
32
  /**
33
- * One manifest per platform from an expo export folder: the same createdAt, the runtime of each platform, relative
34
- * asset URLs, metadata {} (filters could make phones load an older update) and the public config as extra.expoClient.
35
- * Returns the manifests and the files to upload, by hash. Throws if a bundle lacks a value it must carry.
33
+ * One manifest per platform from an expo export folder: the same createdAt, the runtime of each platform, absolute
34
+ * asset URLs (the released expo-updates clients do not resolve relative ones), metadata {} (filters could make phones
35
+ * load an older update) and the public config as extra.expoClient. Returns the manifests and the files to upload, by
36
+ * hash. Throws if a bundle lacks a value it must carry.
36
37
  */
37
38
  export function buildManifests({ dist, metadata, platforms, runtimes, expoClient, assetPath, createdAt, mustContain = [] }) {
38
39
  const files = new Map();