sst 2.5.5 → 2.5.7

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.
@@ -8,6 +8,7 @@ export const bind = (program) => program
8
8
  .example(`sst bind "vitest run"`, "Bind your resources to your tests")
9
9
  .example(`sst bind "tsx scripts/myscript.ts"`, "Bind your resources to a script"), async (args) => {
10
10
  const { spawn } = await import("child_process");
11
+ const kill = await import("tree-kill");
11
12
  const { useProject } = await import("../../project.js");
12
13
  const { useBus } = await import("../../bus.js");
13
14
  const { useIOT } = await import("../../iot.js");
@@ -112,7 +113,7 @@ export const bind = (program) => program
112
113
  Colors.line(`\n`, `Your AWS session is about to expire. Creating a new session and restarting \`${command}\`...`);
113
114
  bindSite("iam_expired");
114
115
  }, expireAt - Date.now());
115
- runCommand({
116
+ await runCommand({
116
117
  ...siteConfig.envs,
117
118
  AWS_ACCESS_KEY_ID: credentials.AccessKeyId,
118
119
  AWS_SECRET_ACCESS_KEY: credentials.SecretAccessKey,
@@ -122,14 +123,14 @@ export const bind = (program) => program
122
123
  }
123
124
  }
124
125
  // Fallback to use local IAM credentials
125
- runCommand({
126
+ await runCommand({
126
127
  ...siteConfig.envs,
127
128
  ...(await localIamCredentials()),
128
129
  });
129
130
  }
130
131
  async function bindScript() {
131
132
  const { Config } = await import("../../config.js");
132
- runCommand({
133
+ await runCommand({
133
134
  ...(await Config.env()),
134
135
  ...(await localIamCredentials()),
135
136
  });
@@ -234,10 +235,21 @@ export const bind = (program) => program
234
235
  AWS_SESSION_TOKEN: credentials.sessionToken,
235
236
  };
236
237
  }
237
- function runCommand(envs) {
238
+ async function runCommand(envs) {
238
239
  Colors.gap();
239
240
  if (p) {
240
- p.kill();
241
+ p.removeAllListeners("exit");
242
+ // Note: calling p.kill() does not kill child processes. And in the
243
+ // cases of Next.js and CRA, servers are child processes. Need to
244
+ // kill the entire process tree to free up port ie. 3000.
245
+ await new Promise((resolve, reject) => {
246
+ kill.default(p?.pid, (error) => {
247
+ if (error) {
248
+ return reject(error);
249
+ }
250
+ resolve(true);
251
+ });
252
+ });
241
253
  }
242
254
  p = spawn(command, {
243
255
  env: {
package/config.js CHANGED
@@ -10,7 +10,7 @@ export var Config;
10
10
  async function parameters() {
11
11
  const result = [];
12
12
  for await (const p of scan(PREFIX.FALLBACK)) {
13
- const parsed = parse(p.Name);
13
+ const parsed = parse(p.Name, PREFIX.FALLBACK);
14
14
  if (parsed.type === "secrets")
15
15
  continue;
16
16
  result.push({
@@ -19,7 +19,7 @@ export var Config;
19
19
  });
20
20
  }
21
21
  for await (const p of scan(PREFIX.STAGE)) {
22
- const parsed = parse(p.Name);
22
+ const parsed = parse(p.Name, PREFIX.STAGE);
23
23
  if (parsed.type === "secrets")
24
24
  continue;
25
25
  result.push({
@@ -45,13 +45,13 @@ export var Config;
45
45
  async function secrets() {
46
46
  const result = {};
47
47
  for await (const p of scan(PREFIX.STAGE + "Secret")) {
48
- const parsed = parse(p.Name);
48
+ const parsed = parse(p.Name, PREFIX.STAGE);
49
49
  if (!result[parsed.id])
50
50
  result[parsed.id] = {};
51
51
  result[parsed.id].value = p.Value;
52
52
  }
53
53
  for await (const p of scan(PREFIX.FALLBACK + "Secret")) {
54
- const parsed = parse(p.Name);
54
+ const parsed = parse(p.Name, PREFIX.FALLBACK);
55
55
  if (!result[parsed.id])
56
56
  result[parsed.id] = {};
57
57
  result[parsed.id].fallback = p.Value;
@@ -178,19 +178,19 @@ const SECRET_UPDATED_AT_ENV = "SST_ADMIN_SECRET_UPDATED_AT";
178
178
  const PREFIX = {
179
179
  get STAGE() {
180
180
  const project = useProject();
181
- return `/sst/${project.config.name}/${project.config.stage}/`;
181
+ return project.config.ssmPrefix;
182
182
  },
183
183
  get FALLBACK() {
184
184
  const project = useProject();
185
185
  return `/sst/${project.config.name}/${FALLBACK_STAGE}/`;
186
186
  },
187
187
  };
188
- function parse(ssmName) {
189
- const parts = ssmName.split("/");
188
+ function parse(ssmName, prefix) {
189
+ const parts = ssmName.substring(prefix.length).split("/");
190
190
  return {
191
- type: parts[4],
192
- id: parts[5],
193
- prop: parts.slice(6).join("/"),
191
+ type: parts[0],
192
+ id: parts[1],
193
+ prop: parts.slice(2).join("/"),
194
194
  };
195
195
  }
196
196
  async function restartFunction(arn) {
@@ -165,10 +165,11 @@ export class SsrSite extends Construct {
165
165
  * ```
166
166
  */
167
167
  attachPermissions(permissions) {
168
+ this.serverLambdaForDev?.attachPermissions(permissions);
169
+ this.serverLambdaForEdge?.attachPermissions(permissions);
168
170
  if (this.serverLambdaForRegional) {
169
171
  attachPermissionsToRole(this.serverLambdaForRegional.role, permissions);
170
172
  }
171
- this.serverLambdaForEdge?.attachPermissions(permissions);
172
173
  }
173
174
  /** @internal */
174
175
  getConstructMetadata() {
@@ -432,6 +433,9 @@ export class SsrSite extends Construct {
432
433
  environment,
433
434
  permissions,
434
435
  role,
436
+ // Force enable live dev to prevent the function handler to be built
437
+ // in the case user set "enableLiveDev: false" on the app or stack.
438
+ enableLiveDev: true,
435
439
  });
436
440
  fn._doNotAllowOthersToBind = true;
437
441
  return fn;
package/credentials.js CHANGED
@@ -70,6 +70,7 @@ export function useAWSClient(client, force = false) {
70
70
  // Handle no internet connection => retry
71
71
  if (e.code === "ENOTFOUND") {
72
72
  printNoInternet();
73
+ return true;
73
74
  }
74
75
  // Handle throttling errors => retry
75
76
  if ([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sst",
3
- "version": "2.5.5",
3
+ "version": "2.5.7",
4
4
  "bin": {
5
5
  "sst": "cli/sst.js"
6
6
  },
@@ -80,6 +80,7 @@
80
80
  "react": "18.2.0",
81
81
  "remeda": "^1.3.0",
82
82
  "sst-aws-cdk": "2.62.2-3",
83
+ "tree-kill": "^1.2.2",
83
84
  "undici": "^5.12.0",
84
85
  "uuid": "^9.0.0",
85
86
  "ws": "^8.11.0",
package/project.d.ts CHANGED
@@ -56,4 +56,10 @@ interface GlobalOptions {
56
56
  region?: string;
57
57
  }
58
58
  export declare function initProject(globals: GlobalOptions): Promise<void>;
59
+ declare function sanitizeStageName(stage: string): string;
60
+ declare function isValidStageName(stage: string): boolean;
61
+ export declare const exportedForTesting: {
62
+ sanitizeStageName: typeof sanitizeStageName;
63
+ isValidStageName: typeof isValidStageName;
64
+ };
59
65
  export {};
package/project.js CHANGED
@@ -117,21 +117,29 @@ async function usePersonalStage(out) {
117
117
  return;
118
118
  }
119
119
  }
120
- async function promptPersonalStage(out) {
120
+ async function promptPersonalStage(out, isRetry) {
121
121
  const readline = await import("readline");
122
122
  const rl = readline.createInterface({
123
123
  input: process.stdin,
124
124
  output: process.stdout,
125
125
  });
126
- return new Promise((resolve) => {
127
- const suggested = os.userInfo().username;
128
- rl.question(`Please enter a name you’d like to use for your personal stage. Or hit enter to use ${blue(suggested)}: `, async (input) => {
126
+ const stage = await new Promise((resolve) => {
127
+ const suggested = sanitizeStageName(os.userInfo().username) || "local";
128
+ const instruction = !isRetry
129
+ ? `Please enter a name you’d like to use for your personal stage.`
130
+ : `Please enter a name that starts with a letter, followed by letters, numbers, or hyphens.`;
131
+ rl.question(`${instruction} Or hit enter to use ${blue(suggested)}: `, async (input) => {
129
132
  rl.close();
130
- const result = input || suggested;
131
- await fs.writeFile(path.join(out, "stage"), result);
133
+ const result = input === "" ? suggested : input;
132
134
  resolve(result);
133
135
  });
134
136
  });
137
+ // Validate stage name
138
+ if (isValidStageName(stage)) {
139
+ await fs.writeFile(path.join(out, "stage"), stage);
140
+ return stage;
141
+ }
142
+ return await promptPersonalStage(out, true);
135
143
  }
136
144
  async function findRoot() {
137
145
  async function find(dir) {
@@ -148,3 +156,17 @@ async function findRoot() {
148
156
  const result = await find(process.cwd());
149
157
  return result;
150
158
  }
159
+ function sanitizeStageName(stage) {
160
+ return (stage
161
+ .replace(/[^A-Za-z0-9-]/g, "-")
162
+ .replace(/--+/g, "-")
163
+ .replace(/^[^A-Za-z]/, "")
164
+ .replace(/-$/, "") || "local");
165
+ }
166
+ function isValidStageName(stage) {
167
+ return Boolean(stage.match(/^[A-Za-z][A-Za-z0-9-]*$/));
168
+ }
169
+ export const exportedForTesting = {
170
+ sanitizeStageName,
171
+ isValidStageName,
172
+ };
package/sst.mjs CHANGED
@@ -317,6 +317,7 @@ var init_build = __esm({
317
317
  var project_exports = {};
318
318
  __export(project_exports, {
319
319
  ProjectContext: () => ProjectContext,
320
+ exportedForTesting: () => exportedForTesting,
320
321
  initProject: () => initProject,
321
322
  useProject: () => useProject
322
323
  });
@@ -420,26 +421,29 @@ async function usePersonalStage(out) {
420
421
  return;
421
422
  }
422
423
  }
423
- async function promptPersonalStage(out) {
424
+ async function promptPersonalStage(out, isRetry) {
424
425
  const readline = await import("readline");
425
426
  const rl = readline.createInterface({
426
427
  input: process.stdin,
427
428
  output: process.stdout
428
429
  });
429
- return new Promise((resolve) => {
430
- const suggested = os.userInfo().username;
430
+ const stage = await new Promise((resolve) => {
431
+ const suggested = sanitizeStageName(os.userInfo().username) || "local";
432
+ const instruction = !isRetry ? `Please enter a name you\u2019d like to use for your personal stage.` : `Please enter a name that starts with a letter, followed by letters, numbers, or hyphens.`;
431
433
  rl.question(
432
- `Please enter a name you\u2019d like to use for your personal stage. Or hit enter to use ${blue(
433
- suggested
434
- )}: `,
434
+ `${instruction} Or hit enter to use ${blue(suggested)}: `,
435
435
  async (input) => {
436
436
  rl.close();
437
- const result = input || suggested;
438
- await fs4.writeFile(path4.join(out, "stage"), result);
437
+ const result = input === "" ? suggested : input;
439
438
  resolve(result);
440
439
  }
441
440
  );
442
441
  });
442
+ if (isValidStageName(stage)) {
443
+ await fs4.writeFile(path4.join(out, "stage"), stage);
444
+ return stage;
445
+ }
446
+ return await promptPersonalStage(out, true);
443
447
  }
444
448
  async function findRoot() {
445
449
  async function find2(dir) {
@@ -460,7 +464,13 @@ async function findRoot() {
460
464
  const result = await find2(process.cwd());
461
465
  return result;
462
466
  }
463
- var ProjectContext, CONFIG_EXTENSIONS;
467
+ function sanitizeStageName(stage) {
468
+ return stage.replace(/[^A-Za-z0-9-]/g, "-").replace(/--+/g, "-").replace(/^[^A-Za-z]/, "").replace(/-$/, "") || "local";
469
+ }
470
+ function isValidStageName(stage) {
471
+ return Boolean(stage.match(/^[A-Za-z][A-Za-z0-9-]*$/));
472
+ }
473
+ var ProjectContext, CONFIG_EXTENSIONS, exportedForTesting;
464
474
  var init_project = __esm({
465
475
  "src/project.ts"() {
466
476
  "use strict";
@@ -477,6 +487,10 @@ var init_project = __esm({
477
487
  ".config.mjs",
478
488
  ".config.js"
479
489
  ];
490
+ exportedForTesting = {
491
+ sanitizeStageName,
492
+ isValidStageName
493
+ };
480
494
  }
481
495
  });
482
496
 
@@ -1587,6 +1601,7 @@ function useAWSClient(client, force = false) {
1587
1601
  retryDecider: (e) => {
1588
1602
  if (e.code === "ENOTFOUND") {
1589
1603
  printNoInternet();
1604
+ return true;
1590
1605
  }
1591
1606
  if ([
1592
1607
  "ThrottlingException",
@@ -6523,12 +6538,12 @@ async function* scan(prefix) {
6523
6538
  token = results.NextToken;
6524
6539
  }
6525
6540
  }
6526
- function parse(ssmName) {
6527
- const parts = ssmName.split("/");
6541
+ function parse(ssmName, prefix) {
6542
+ const parts = ssmName.substring(prefix.length).split("/");
6528
6543
  return {
6529
- type: parts[4],
6530
- id: parts[5],
6531
- prop: parts.slice(6).join("/")
6544
+ type: parts[0],
6545
+ id: parts[1],
6546
+ prop: parts.slice(2).join("/")
6532
6547
  };
6533
6548
  }
6534
6549
  async function restartFunction(arn) {
@@ -6569,7 +6584,7 @@ var init_config = __esm({
6569
6584
  async function parameters() {
6570
6585
  const result = [];
6571
6586
  for await (const p of scan(PREFIX.FALLBACK)) {
6572
- const parsed = parse(p.Name);
6587
+ const parsed = parse(p.Name, PREFIX.FALLBACK);
6573
6588
  if (parsed.type === "secrets")
6574
6589
  continue;
6575
6590
  result.push({
@@ -6578,7 +6593,7 @@ var init_config = __esm({
6578
6593
  });
6579
6594
  }
6580
6595
  for await (const p of scan(PREFIX.STAGE)) {
6581
- const parsed = parse(p.Name);
6596
+ const parsed = parse(p.Name, PREFIX.STAGE);
6582
6597
  if (parsed.type === "secrets")
6583
6598
  continue;
6584
6599
  result.push({
@@ -6604,13 +6619,13 @@ var init_config = __esm({
6604
6619
  async function secrets2() {
6605
6620
  const result = {};
6606
6621
  for await (const p of scan(PREFIX.STAGE + "Secret")) {
6607
- const parsed = parse(p.Name);
6622
+ const parsed = parse(p.Name, PREFIX.STAGE);
6608
6623
  if (!result[parsed.id])
6609
6624
  result[parsed.id] = {};
6610
6625
  result[parsed.id].value = p.Value;
6611
6626
  }
6612
6627
  for await (const p of scan(PREFIX.FALLBACK + "Secret")) {
6613
- const parsed = parse(p.Name);
6628
+ const parsed = parse(p.Name, PREFIX.FALLBACK);
6614
6629
  if (!result[parsed.id])
6615
6630
  result[parsed.id] = {};
6616
6631
  result[parsed.id].fallback = p.Value;
@@ -6721,7 +6736,7 @@ var init_config = __esm({
6721
6736
  PREFIX = {
6722
6737
  get STAGE() {
6723
6738
  const project = useProject();
6724
- return `/sst/${project.config.name}/${project.config.stage}/`;
6739
+ return project.config.ssmPrefix;
6725
6740
  },
6726
6741
  get FALLBACK() {
6727
6742
  const project = useProject();
@@ -7129,6 +7144,7 @@ var bind = (program2) => program2.command(
7129
7144
  ),
7130
7145
  async (args) => {
7131
7146
  const { spawn: spawn7 } = await import("child_process");
7147
+ const kill = await import("tree-kill");
7132
7148
  const { useProject: useProject2 } = await Promise.resolve().then(() => (init_project(), project_exports));
7133
7149
  const { useBus: useBus2 } = await Promise.resolve().then(() => (init_bus(), bus_exports));
7134
7150
  const { useIOT: useIOT2 } = await Promise.resolve().then(() => (init_iot(), iot_exports));
@@ -7256,7 +7272,7 @@ var bind = (program2) => program2.command(
7256
7272
  );
7257
7273
  bindSite("iam_expired");
7258
7274
  }, expireAt - Date.now());
7259
- runCommand({
7275
+ await runCommand({
7260
7276
  ...siteConfig.envs,
7261
7277
  AWS_ACCESS_KEY_ID: credentials.AccessKeyId,
7262
7278
  AWS_SECRET_ACCESS_KEY: credentials.SecretAccessKey,
@@ -7265,14 +7281,14 @@ var bind = (program2) => program2.command(
7265
7281
  return;
7266
7282
  }
7267
7283
  }
7268
- runCommand({
7284
+ await runCommand({
7269
7285
  ...siteConfig.envs,
7270
7286
  ...await localIamCredentials()
7271
7287
  });
7272
7288
  }
7273
7289
  async function bindScript() {
7274
7290
  const { Config: Config2 } = await Promise.resolve().then(() => (init_config(), config_exports));
7275
- runCommand({
7291
+ await runCommand({
7276
7292
  ...await Config2.env(),
7277
7293
  ...await localIamCredentials()
7278
7294
  });
@@ -7371,10 +7387,18 @@ var bind = (program2) => program2.command(
7371
7387
  AWS_SESSION_TOKEN: credentials.sessionToken
7372
7388
  };
7373
7389
  }
7374
- function runCommand(envs) {
7390
+ async function runCommand(envs) {
7375
7391
  Colors2.gap();
7376
7392
  if (p) {
7377
- p.kill();
7393
+ p.removeAllListeners("exit");
7394
+ await new Promise((resolve, reject) => {
7395
+ kill.default(p?.pid, (error2) => {
7396
+ if (error2) {
7397
+ return reject(error2);
7398
+ }
7399
+ resolve(true);
7400
+ });
7401
+ });
7378
7402
  }
7379
7403
  p = spawn7(command, {
7380
7404
  env: {