@everystack/cli 0.2.9 → 0.2.11
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 +1 -1
- package/src/cli/aws.ts +5 -11
- package/src/cli/commands/update.ts +108 -32
- package/src/cli/utils/export.ts +3 -2
package/package.json
CHANGED
package/src/cli/aws.ts
CHANGED
|
@@ -94,19 +94,13 @@ export async function invokeAction(
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Resolve the CloudWatch log group for a Lambda function.
|
|
97
|
-
*
|
|
98
|
-
* the conventional `/aws/lambda/{functionName}` pattern if not set.
|
|
97
|
+
* Uses the conventional `/aws/lambda/{functionName}` naming — no IAM call needed.
|
|
99
98
|
*/
|
|
100
|
-
export
|
|
101
|
-
|
|
99
|
+
export function resolveLogGroup(
|
|
100
|
+
_region: string,
|
|
102
101
|
functionName: string,
|
|
103
|
-
):
|
|
104
|
-
|
|
105
|
-
const { GetFunctionConfigurationCommand } = await import('@aws-sdk/client-lambda');
|
|
106
|
-
const config = await client.send(
|
|
107
|
-
new GetFunctionConfigurationCommand({ FunctionName: functionName })
|
|
108
|
-
);
|
|
109
|
-
return config.LoggingConfig?.LogGroup ?? `/aws/lambda/${functionName}`;
|
|
102
|
+
): string {
|
|
103
|
+
return `/aws/lambda/${functionName}`;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
106
|
/**
|
|
@@ -8,7 +8,7 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
import { resolveConfig, type CliConfig } from '../config.js';
|
|
9
9
|
import { uploadToS3, invokeAction } from '../aws.js';
|
|
10
10
|
import { step, success, warn, fail, info } from '../output.js';
|
|
11
|
-
import { exportApp
|
|
11
|
+
import { exportApp } from '../utils/export.js';
|
|
12
12
|
import { walkDirectory } from '../utils/walk.js';
|
|
13
13
|
|
|
14
14
|
export interface UpdateFlags {
|
|
@@ -27,13 +27,19 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
27
27
|
// (e.g. OUTBOUND_HOST=localhost → LAN IP) to leak into the manifest.
|
|
28
28
|
process.env.EVERYSTACK_UPDATE = '1';
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// Resolve ENVIRONMENT before app.config.js evaluation so the manifest
|
|
31
31
|
// snapshots the correct API URLs and feature flags for the target.
|
|
32
|
-
// --environment
|
|
33
|
-
// SST stage names (dev, prod) don't match EAS profile names
|
|
34
|
-
// (development, production) and conflating them causes config failures.
|
|
32
|
+
// Priority: --environment flag > eas.json channel match > existing env.
|
|
35
33
|
if (flags.environment) {
|
|
36
34
|
process.env.ENVIRONMENT ||= flags.environment;
|
|
35
|
+
} else if (!process.env.ENVIRONMENT) {
|
|
36
|
+
// Derive ENVIRONMENT from eas.json by matching --channel to a build profile.
|
|
37
|
+
// EAS profiles define ENVIRONMENT in their env blocks (e.g. production → "production").
|
|
38
|
+
const channel = flags.channel || flags.branch || flags.stage || 'production';
|
|
39
|
+
const easEnv = resolveEasEnvironment(channel);
|
|
40
|
+
if (easEnv) {
|
|
41
|
+
process.env.ENVIRONMENT = easEnv;
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
// When --stage is provided, resolve deployed config early so HOST_URL
|
|
@@ -64,6 +70,21 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
64
70
|
const appConfig = await loadAppConfig();
|
|
65
71
|
const runtimeVersion = appConfig.runtimeVersion;
|
|
66
72
|
|
|
73
|
+
// Validate: SSR flags in the expo-router plugin config will crash native devices.
|
|
74
|
+
// The bundle is compiled with server rendering code paths that have no server at runtime.
|
|
75
|
+
const ssrFlags = getExpoRouterSsrFlags(appConfig);
|
|
76
|
+
if (ssrFlags.useServerRendering || ssrFlags.useServerDataLoaders) {
|
|
77
|
+
fail('Export contains server rendering flags that will crash native devices.');
|
|
78
|
+
info('expo-router plugin has unstable_useServerRendering or unstable_useServerDataLoaders enabled.');
|
|
79
|
+
info('Use SSR_ENABLED to opt in only for web server exports:');
|
|
80
|
+
info('');
|
|
81
|
+
info(" ['expo-router', {");
|
|
82
|
+
info(' unstable_useServerRendering: !!process.env.SSR_ENABLED,');
|
|
83
|
+
info(' unstable_useServerDataLoaders: !!process.env.SSR_ENABLED,');
|
|
84
|
+
info(' }]');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
if (!runtimeVersion) {
|
|
68
89
|
fail('No runtimeVersion found in app.json/app.config.js');
|
|
69
90
|
info('Add "runtimeVersion" to your app.json, e.g.:');
|
|
@@ -73,32 +94,28 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
73
94
|
}
|
|
74
95
|
success(`Runtime version: ${runtimeVersion}`);
|
|
75
96
|
|
|
76
|
-
// Export if needed
|
|
77
97
|
const distDir = path.resolve('dist');
|
|
78
|
-
const
|
|
79
|
-
let shouldExport = flags.export === 'true' || !distExists;
|
|
80
|
-
|
|
81
|
-
// Check if dist/ is stale (source files newer than dist/)
|
|
82
|
-
if (!shouldExport && flags['skip-export'] !== 'true' && distExists) {
|
|
83
|
-
if (await isDistStale(distDir)) {
|
|
84
|
-
step('Source files changed since last export, re-exporting...');
|
|
85
|
-
shouldExport = true;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
98
|
+
const needsMobile = platformFilter !== 'web';
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
// First export: mobile-safe bundles (no SSR_ENABLED).
|
|
101
|
+
// Always re-export — stale dist/ is the #1 cause of broken OTA pushes.
|
|
102
|
+
// Export mobile platforms only — web static pre-rendering can hang when
|
|
103
|
+
// OUTBOUND_HOST points to a real server (open HTTP handles prevent exit).
|
|
104
|
+
if (needsMobile) {
|
|
90
105
|
step('Exporting app...');
|
|
91
|
-
await exportApp();
|
|
106
|
+
await exportApp('ios,android');
|
|
92
107
|
success('Export complete');
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
// Read metadata (mobile exports produce metadata.json; web-only exports don't)
|
|
96
|
-
const metadataPath = path.join(distDir, 'metadata.json');
|
|
97
111
|
let metadata: any = null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
if (needsMobile) {
|
|
113
|
+
const metadataPath = path.join(distDir, 'metadata.json');
|
|
114
|
+
try {
|
|
115
|
+
metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
// No metadata.json
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
// Determine platforms to publish
|
|
@@ -228,6 +245,16 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
228
245
|
|
|
229
246
|
// Web publish — direct S3 upload + Lambda invoke (IAM auth)
|
|
230
247
|
if (hasWeb) {
|
|
248
|
+
// Re-export with SSR_ENABLED for web server rendering.
|
|
249
|
+
// The first export produced mobile-safe bundles (already uploaded above).
|
|
250
|
+
// This second export enables SSR flags so the web bundle includes
|
|
251
|
+
// server rendering code paths and data loaders.
|
|
252
|
+
step('Exporting web bundle (SSR)...');
|
|
253
|
+
process.env.SSR_ENABLED = '1';
|
|
254
|
+
await exportApp('web');
|
|
255
|
+
delete process.env.SSR_ENABLED;
|
|
256
|
+
success('Web export complete');
|
|
257
|
+
|
|
231
258
|
const serverDir = path.join(distDir, 'server');
|
|
232
259
|
const clientDir = path.join(distDir, 'client');
|
|
233
260
|
const routesJson = path.join(serverDir, '_expo', 'routes.json');
|
|
@@ -490,15 +517,6 @@ function mimeFromExt(ext: string): string {
|
|
|
490
517
|
return map[ext] || 'application/octet-stream';
|
|
491
518
|
}
|
|
492
519
|
|
|
493
|
-
async function dirExists(dir: string): Promise<boolean> {
|
|
494
|
-
try {
|
|
495
|
-
const stat = await fs.stat(dir);
|
|
496
|
-
return stat.isDirectory();
|
|
497
|
-
} catch {
|
|
498
|
-
return false;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
520
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
503
521
|
try {
|
|
504
522
|
const stat = await fs.stat(filePath);
|
|
@@ -530,3 +548,61 @@ async function createArchive(sourceDir: string, archivePath: string): Promise<vo
|
|
|
530
548
|
await pipeline(tar.stdout!, brotli, output);
|
|
531
549
|
await tarExit;
|
|
532
550
|
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Read eas.json and find ENVIRONMENT for a given channel.
|
|
554
|
+
* Matches --channel to a build profile's `channel` field, then reads
|
|
555
|
+
* ENVIRONMENT from that profile's env block. Falls back to profile name
|
|
556
|
+
* match (e.g. --channel production matches the "production" profile).
|
|
557
|
+
* @internal Exported for testing.
|
|
558
|
+
*/
|
|
559
|
+
export function resolveEasEnvironment(channel: string, cwd?: string): string | undefined {
|
|
560
|
+
try {
|
|
561
|
+
const easPath = path.resolve(cwd || process.cwd(), 'eas.json');
|
|
562
|
+
// Synchronous read — runs once before export, performance irrelevant.
|
|
563
|
+
const eas = JSON.parse(require('fs').readFileSync(easPath, 'utf8'));
|
|
564
|
+
const profiles = eas?.build;
|
|
565
|
+
if (!profiles || typeof profiles !== 'object') return undefined;
|
|
566
|
+
|
|
567
|
+
// First pass: match on explicit channel field
|
|
568
|
+
for (const [, profile] of Object.entries(profiles)) {
|
|
569
|
+
const p = profile as any;
|
|
570
|
+
if (p?.channel === channel && p?.env?.ENVIRONMENT) {
|
|
571
|
+
return p.env.ENVIRONMENT;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Second pass: match on profile name (e.g. "production" profile for --channel production)
|
|
576
|
+
if (profiles[channel]?.env?.ENVIRONMENT) {
|
|
577
|
+
return profiles[channel].env.ENVIRONMENT;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return undefined;
|
|
581
|
+
} catch {
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Extract expo-router SSR flags from the resolved app config.
|
|
588
|
+
* Checks the plugins array for the expo-router entry.
|
|
589
|
+
* @internal Exported for testing.
|
|
590
|
+
*/
|
|
591
|
+
export function getExpoRouterSsrFlags(appConfig: any): {
|
|
592
|
+
useServerRendering: boolean;
|
|
593
|
+
useServerDataLoaders: boolean;
|
|
594
|
+
} {
|
|
595
|
+
const plugins: any[] = appConfig?.plugins || [];
|
|
596
|
+
for (const plugin of plugins) {
|
|
597
|
+
// Plugin can be a string or [name, options] tuple
|
|
598
|
+
const name = Array.isArray(plugin) ? plugin[0] : plugin;
|
|
599
|
+
const options = Array.isArray(plugin) ? plugin[1] : undefined;
|
|
600
|
+
if (name === 'expo-router' && options && typeof options === 'object') {
|
|
601
|
+
return {
|
|
602
|
+
useServerRendering: !!options.unstable_useServerRendering,
|
|
603
|
+
useServerDataLoaders: !!options.unstable_useServerDataLoaders,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { useServerRendering: false, useServerDataLoaders: false };
|
|
608
|
+
}
|
package/src/cli/utils/export.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { execSync } from 'child_process';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
export async function exportApp(): Promise<void> {
|
|
6
|
-
|
|
5
|
+
export async function exportApp(platform?: string): Promise<void> {
|
|
6
|
+
const platformFlag = platform ? ` --platform ${platform}` : '';
|
|
7
|
+
execSync(`npx expo export --output-dir dist${platformFlag}`, {
|
|
7
8
|
stdio: 'inherit',
|
|
8
9
|
env: { ...process.env },
|
|
9
10
|
});
|