@awsless/cli 0.0.46-next.7 → 0.0.46-next.9

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/dist/bin.js CHANGED
@@ -2234,6 +2234,66 @@ var InstanceDefaultSchema = z19.object({
2234
2234
  // src/feature/router/schema.ts
2235
2235
  import { days as days3, minutes as minutes2, parse as parse3 } from "@awsless/duration";
2236
2236
  import { z as z20 } from "zod";
2237
+
2238
+ // src/feature/router/pattern.ts
2239
+ var PARAM_TOKEN = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\*/g;
2240
+ var escapeRegex = (value) => {
2241
+ return value.replace(/[|\\{}()[\]^$+*?.\-]/g, "\\$&");
2242
+ };
2243
+ var compileRoutePattern = (pattern) => {
2244
+ if (!pattern.startsWith("/")) {
2245
+ throw new ExpectedError(`Route pattern "${pattern}" must start with a slash (/)`);
2246
+ }
2247
+ if (pattern === "/*") {
2248
+ return { key: pattern };
2249
+ }
2250
+ const params = [];
2251
+ let regex = "";
2252
+ let stars = 0;
2253
+ let last = 0;
2254
+ let token;
2255
+ PARAM_TOKEN.lastIndex = 0;
2256
+ while (token = PARAM_TOKEN.exec(pattern)) {
2257
+ regex += escapeRegex(pattern.slice(last, token.index));
2258
+ const param = token[1];
2259
+ if (param) {
2260
+ if (params.includes(param)) {
2261
+ throw new ExpectedError(`Duplicate param "${param}" in route pattern "${pattern}"`);
2262
+ }
2263
+ params.push(param);
2264
+ regex += "([^/]+)";
2265
+ } else {
2266
+ stars++;
2267
+ regex += ".*";
2268
+ }
2269
+ last = PARAM_TOKEN.lastIndex;
2270
+ }
2271
+ if (params.length === 0 && stars === 0) {
2272
+ return { key: pattern };
2273
+ }
2274
+ regex += escapeRegex(pattern.slice(last));
2275
+ const root2 = pattern.split("/")[1] ?? "";
2276
+ if (root2 === "" || root2.includes("*") || root2.includes("{")) {
2277
+ throw new ExpectedError(
2278
+ `The first path segment of route pattern "${pattern}" must be static when the pattern contains params or wildcards.`
2279
+ );
2280
+ }
2281
+ if (root2.includes(".")) {
2282
+ throw new ExpectedError(
2283
+ `The first path segment of route pattern "${pattern}" can't contain a dot when the pattern contains params or wildcards.`
2284
+ );
2285
+ }
2286
+ if (params.length === 0 && pattern === `/${root2}/*`) {
2287
+ return { key: pattern };
2288
+ }
2289
+ return {
2290
+ key: `/${root2}/*`,
2291
+ match: `^${regex}$`,
2292
+ params: params.length > 0 ? params : void 0
2293
+ };
2294
+ };
2295
+
2296
+ // src/feature/router/schema.ts
2237
2297
  var ErrorResponsePathSchema = z20.string().describe(
2238
2298
  [
2239
2299
  "The path to the custom error page that you want to return to the viewer when your origin returns the HTTP status code specified.",
@@ -2263,6 +2323,28 @@ var ErrorResponseSchema = z20.union([
2263
2323
  })
2264
2324
  ]).optional();
2265
2325
  var RouteSchema = z20.string().regex(/^\//, "Route must start with a slash (/)").regex(/^\/([^/*.]+)?$/, 'Router paths mount a single segment without dots, like "/api".');
2326
+ var RoutesSchema = z20.record(
2327
+ ResourceIdSchema.describe("The router id to add your routes to."),
2328
+ z20.record(z20.string().regex(/^\//, "Route must start with a slash (/)"), FunctionSchema).superRefine((routes, ctx) => {
2329
+ for (const pattern of Object.keys(routes)) {
2330
+ try {
2331
+ compileRoutePattern(pattern);
2332
+ } catch (error) {
2333
+ ctx.addIssue({
2334
+ code: z20.ZodIssueCode.custom,
2335
+ path: [pattern],
2336
+ message: error instanceof Error ? error.message : `Invalid route pattern: ${pattern}`
2337
+ });
2338
+ }
2339
+ }
2340
+ }).describe(
2341
+ [
2342
+ "Define the routes and the lambda function that should handle them.",
2343
+ 'Routes can be an exact path like "/sitemap.xml", a wildcard like "/sitemap/*", or contain params like "/sitemap/{locale}/{page}.xml".',
2344
+ 'Param values are passed to the function as "x-param-[NAME]" request headers.'
2345
+ ].join("\n")
2346
+ )
2347
+ ).optional().describe("Add routes to your global Router that link a path pattern to a lambda function.");
2266
2348
  var VisibilitySchema = z20.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
2267
2349
  var WafSettingsSchema = z20.object({
2268
2350
  rateLimiter: z20.object({
@@ -3371,14 +3453,13 @@ var TestsSchema = z44.union([
3371
3453
  ]).describe("Define the location of your tests for your stack.").optional();
3372
3454
 
3373
3455
  // src/config/stack.ts
3374
- var DependsSchema = ResourceIdSchema.array().optional().describe("Define the stacks that this stack is depended on.");
3375
3456
  var NameSchema = ResourceIdSchema.refine((name) => !["base", "hostedzones"].includes(name), {
3376
3457
  message: `Stack name can't be a reserved name.`
3377
3458
  }).describe("Stack name.");
3378
3459
  var StackSchema = z45.object({
3379
3460
  $schema: z45.string().optional(),
3380
3461
  name: NameSchema,
3381
- depends: DependsSchema,
3462
+ routes: RoutesSchema,
3382
3463
  commands: CommandsSchema,
3383
3464
  // auth: AuthSchema,
3384
3465
  // http: HttpSchema,
@@ -4480,11 +4561,6 @@ var bundleFeature = defineFeature({
4480
4561
  addLayer,
4481
4562
  addPermission
4482
4563
  });
4483
- },
4484
- onStack(ctx) {
4485
- const bundle = ctx.shared.get("bundle", "main");
4486
- ctx.onEnv(bundle.addEnv);
4487
- ctx.onPermission(bundle.addPermission);
4488
4564
  }
4489
4565
  });
4490
4566
 
@@ -4795,7 +4871,7 @@ var configFeature = defineFeature({
4795
4871
  ctx.addEnv(`CONFIG_${constantCase4(name)}`, name);
4796
4872
  }
4797
4873
  if (configs.length) {
4798
- ctx.addStackPermission({
4874
+ ctx.addGlobalPermission({
4799
4875
  actions: [
4800
4876
  "ssm:GetParameter",
4801
4877
  "ssm:GetParameters",
@@ -6397,7 +6473,7 @@ var queueFeature = defineFeature({
6397
6473
  });
6398
6474
  }
6399
6475
  ctx.addEnv(`QUEUE_${constantCase6(ctx.stack.name)}_${constantCase6(id)}_URL`, queue2.url);
6400
- ctx.addStackPermission({
6476
+ ctx.addGlobalPermission({
6401
6477
  actions: [
6402
6478
  "sqs:SendMessage",
6403
6479
  "sqs:ReceiveMessage",
@@ -6757,7 +6833,7 @@ var searchFeature = defineFeature({
6757
6833
  }
6758
6834
  );
6759
6835
  ctx.addEnv(`SEARCH_${constantCase8(ctx.stack.name)}_${constantCase8(id)}_DOMAIN`, openSearch.endpointV2);
6760
- ctx.addStackPermission({
6836
+ ctx.addGlobalPermission({
6761
6837
  actions: ["es:ESHttp*"],
6762
6838
  resources: [
6763
6839
  //
@@ -8677,13 +8753,13 @@ var jobFeature = defineFeature({
8677
8753
  const group = new Group25(ctx.stack, "job", id);
8678
8754
  createFargateJob(group, ctx, "job", id, props);
8679
8755
  }
8680
- ctx.addStackPermission({
8756
+ ctx.addGlobalPermission({
8681
8757
  actions: ["ecs:RunTask"],
8682
8758
  resources: [
8683
8759
  `arn:aws:ecs:${ctx.appConfig.region}:*:task-definition/${ctx.app.name}--${ctx.stackConfig.name}--*`
8684
8760
  ]
8685
8761
  });
8686
- ctx.addStackPermission({
8762
+ ctx.addGlobalPermission({
8687
8763
  actions: ["iam:PassRole"],
8688
8764
  resources: ["*"],
8689
8765
  conditions: {
@@ -9220,7 +9296,7 @@ var metricFeature = defineFeature({
9220
9296
  onStack(ctx) {
9221
9297
  const bundle = ctx.shared.get("bundle", "main");
9222
9298
  const namespace = `awsless/${kebabCase11(ctx.app.name)}/${kebabCase11(ctx.stack.name)}`;
9223
- ctx.addStackPermission({
9299
+ ctx.addGlobalPermission({
9224
9300
  actions: ["cloudwatch:PutMetricData"],
9225
9301
  resources: ["*"],
9226
9302
  conditions: {
@@ -9299,7 +9375,7 @@ var metricFeature = defineFeature({
9299
9375
  import { days as days10, seconds as seconds7, toSeconds as toSeconds13, years } from "@awsless/duration";
9300
9376
  import { Group as Group29 } from "@terraforge/core";
9301
9377
  import { aws as aws30 } from "@terraforge/aws";
9302
- import { camelCase as camelCase9, constantCase as constantCase15 } from "change-case";
9378
+ import { camelCase as camelCase9, constantCase as constantCase15, kebabCase as kebabCase12 } from "change-case";
9303
9379
 
9304
9380
  // src/feature/router/router-code.ts
9305
9381
  import { minutes as minutes8, seconds as seconds6, toSeconds as toSeconds12 } from "@awsless/duration";
@@ -9498,6 +9574,38 @@ function isValidRoute(route, method) {
9498
9574
  return true;
9499
9575
  }
9500
9576
 
9577
+ function matchRoute(value, path, method) {
9578
+ const list = Array.isArray(value) ? value : [value];
9579
+
9580
+ for(const i in list) {
9581
+ const route = list[i];
9582
+
9583
+ if(!isValidRoute(route, method)) {
9584
+ continue;
9585
+ }
9586
+
9587
+ if(route.match) {
9588
+ const found = path.match(new RegExp(route.match));
9589
+
9590
+ if(!found) {
9591
+ continue;
9592
+ }
9593
+
9594
+ const params = {};
9595
+
9596
+ if(route.params) {
9597
+ for(const p in route.params) {
9598
+ params[route.params[p]] = found[Number(p) + 1];
9599
+ }
9600
+ }
9601
+
9602
+ return { route: route, params: params };
9603
+ }
9604
+
9605
+ return { route: route };
9606
+ }
9607
+ }
9608
+
9501
9609
  async function findRoute(path, method, prefix) {
9502
9610
  // only route selection is normalized, the forwarded uri stays untouched
9503
9611
  if (path.length > 1 && path.slice(-1) === '/') {
@@ -9509,14 +9617,34 @@ async function findRoute(path, method, prefix) {
9509
9617
 
9510
9618
  for(const i in keys) {
9511
9619
  const key = keys[i];
9620
+ let value;
9512
9621
 
9513
9622
  try {
9514
- const route = await store.get(prefix + key, { format: 'json' });
9623
+ value = await store.get(prefix + key, { format: 'json' });
9624
+ } catch (e) {
9625
+ continue;
9626
+ }
9627
+
9628
+ // Route lists that are too big for a single key value pair
9629
+ // are sharded over multiple entries behind a route index.
9630
+ if(value && value.list) {
9631
+ for(let n = 0; n < value.list; n++) {
9632
+ try {
9633
+ const route = await store.get(prefix + key + '#' + n, { format: 'json' });
9634
+ const result = matchRoute(route, path, method);
9635
+
9636
+ if(result) {
9637
+ return result;
9638
+ }
9639
+ } catch (e) {}
9640
+ }
9641
+ } else {
9642
+ const result = matchRoute(value, path, method);
9515
9643
 
9516
- if(isValidRoute(route, method)) {
9517
- return route;
9644
+ if(result) {
9645
+ return result;
9518
9646
  }
9519
- } catch (e) {}
9647
+ }
9520
9648
  }
9521
9649
  }
9522
9650
 
@@ -9639,15 +9767,36 @@ async function handler(event) {
9639
9767
 
9640
9768
  ${prefixCode}
9641
9769
 
9642
- const route = await findRoute(path, request.method, prefix);
9770
+ const result = await findRoute(path, request.method, prefix);
9643
9771
 
9644
- if(!route) {
9772
+ if(!result) {
9645
9773
  return {
9646
9774
  statusCode: 404,
9647
9775
  statusDescription: 'Not Found'
9648
9776
  };
9649
9777
  }
9650
9778
 
9779
+ const route = result.route;
9780
+
9781
+ // A client provided param header can never reach the origin.
9782
+ const spoofed = [];
9783
+
9784
+ for(const name in headers) {
9785
+ if(name.indexOf('x-param-') === 0) {
9786
+ spoofed.push(name);
9787
+ }
9788
+ }
9789
+
9790
+ for(const i in spoofed) {
9791
+ delete headers[spoofed[i]];
9792
+ }
9793
+
9794
+ if(result.params) {
9795
+ for(const name in result.params) {
9796
+ headers['x-param-' + name.toLowerCase()] = { value: encodeURIComponent(result.params[name]) };
9797
+ }
9798
+ }
9799
+
9651
9800
  if(route.requestHeaders) {
9652
9801
  for(const name in route.requestHeaders) {
9653
9802
  headers[name] = { value: route.requestHeaders[name] };
@@ -9700,6 +9849,31 @@ async function handler(event) {
9700
9849
  `;
9701
9850
 
9702
9851
  // src/feature/router/index.ts
9852
+ var MAX_VALUE_SIZE = 1e3;
9853
+ var ORIGIN_PLACEHOLDER = "x".repeat(64);
9854
+ var assertRouteValueSize = (key, route) => {
9855
+ const withOrigin = (entry) => {
9856
+ return entry.type === "lambda" ? { ...entry, domainName: ORIGIN_PLACEHOLDER } : entry;
9857
+ };
9858
+ for (const entry of Array.isArray(route) ? route : [route]) {
9859
+ if (Buffer.byteLength(JSON.stringify(withOrigin(entry)), "utf8") > MAX_VALUE_SIZE) {
9860
+ throw new ExpectedError(`The route value of the "${key}" route key is too large.`);
9861
+ }
9862
+ }
9863
+ };
9864
+ var createRouteStoreEntries = (key, route) => {
9865
+ const value = JSON.stringify(route);
9866
+ if (!Array.isArray(route) || Buffer.byteLength(value, "utf8") <= MAX_VALUE_SIZE) {
9867
+ return [{ key, value }];
9868
+ }
9869
+ return [
9870
+ { key, value: JSON.stringify({ list: route.length }) },
9871
+ ...route.map((entry, index) => ({
9872
+ key: `${key}#${index}`,
9873
+ value: JSON.stringify(entry)
9874
+ }))
9875
+ ];
9876
+ };
9703
9877
  var routerFeature = defineFeature({
9704
9878
  name: "router",
9705
9879
  onApp(ctx) {
@@ -9745,12 +9919,13 @@ var routerFeature = defineFeature({
9745
9919
  if (Object.hasOwn(routes, `${id}:${key}`)) {
9746
9920
  throw new ExpectedError(`Duplicate route key: ${key} in the "${id}" router`);
9747
9921
  }
9922
+ assertRouteValueSize(`${id}:${key}`, route);
9748
9923
  routes[`${id}:${key}`] = route;
9749
9924
  }
9750
9925
  for (const dependency of options?.dependsOn ?? []) {
9751
9926
  routeDependencies.add(dependency);
9752
9927
  }
9753
- if (Object.values(newRoutes).some((route) => route.type === "lambda")) {
9928
+ if (Object.values(newRoutes).flat().some((route) => route.type === "lambda")) {
9754
9929
  hasLambdaRoutes = true;
9755
9930
  }
9756
9931
  });
@@ -10152,12 +10327,15 @@ var routerFeature = defineFeature({
10152
10327
  storeArn: routeStore.arn,
10153
10328
  functionVersion: bundle.lambda.version,
10154
10329
  routes: $resolve([routes, lambdaUrlHost], (routes2, lambdaUrlHost2) => {
10155
- return Object.entries(routes2).map(([key, route]) => ({
10156
- key,
10157
- value: JSON.stringify(
10158
- route.type === "lambda" ? { ...route, domainName: lambdaUrlHost2 } : route
10330
+ const withOrigin = (route) => {
10331
+ return route.type === "lambda" ? { ...route, domainName: lambdaUrlHost2 } : route;
10332
+ };
10333
+ return Object.entries(routes2).flatMap(
10334
+ ([key, route]) => createRouteStoreEntries(
10335
+ key,
10336
+ Array.isArray(route) ? route.map(withOrigin) : withOrigin(route)
10159
10337
  )
10160
- }));
10338
+ );
10161
10339
  })
10162
10340
  },
10163
10341
  {
@@ -10217,6 +10395,41 @@ var routerFeature = defineFeature({
10217
10395
  ctx.bind(`ROUTER_${constantCase15(id)}_ENDPOINT`, domainName);
10218
10396
  }
10219
10397
  }
10398
+ },
10399
+ onStack(ctx) {
10400
+ for (const [id, patterns] of Object.entries(ctx.stackConfig.routes ?? {})) {
10401
+ if (!ctx.appConfig.defaults.router?.[id]) {
10402
+ throw new FileError(ctx.stackConfig.file, `Router "${id}" is not defined on the app level.`);
10403
+ }
10404
+ const addRoutes = ctx.shared.entry("router", "addRoutes", id);
10405
+ const grouped = {};
10406
+ for (const [pattern, props] of Object.entries(patterns)) {
10407
+ const compiled = compileRoutePattern(pattern);
10408
+ const slug = kebabCase12(pattern).slice(0, 20);
10409
+ const routeKey = formatRouteKey(ctx.stack.name, "route", `${slug || "root"}-${shortId(pattern)}`);
10410
+ registerBundleFunction(ctx, routeKey, props);
10411
+ grouped[compiled.key] ??= [];
10412
+ grouped[compiled.key].push({
10413
+ type: "lambda",
10414
+ forwardHost: true,
10415
+ urlEncodedQueryString: true,
10416
+ match: compiled.match,
10417
+ params: compiled.params,
10418
+ requestHeaders: {
10419
+ [ROUTE_HEADER]: routeKey
10420
+ }
10421
+ });
10422
+ }
10423
+ const merged = {};
10424
+ for (const [key, list3] of Object.entries(grouped)) {
10425
+ if (list3.length === 1) {
10426
+ merged[key] = list3[0];
10427
+ } else {
10428
+ merged[key] = [...list3.filter((route) => route.match), ...list3.filter((route) => !route.match)];
10429
+ }
10430
+ }
10431
+ addRoutes(merged);
10432
+ }
10220
10433
  }
10221
10434
  });
10222
10435
 
@@ -10602,14 +10815,6 @@ var SharedData = class {
10602
10815
  };
10603
10816
 
10604
10817
  // src/app.ts
10605
- var assertDepsExists = (stack, stacks) => {
10606
- for (const dep of stack.depends ?? []) {
10607
- const found = stacks.find((i) => i.name === dep);
10608
- if (!found) {
10609
- throw new FileError(stack.file, `Stack "${stack.name}" depends on a stack "${dep}" that doesn't exist.`);
10610
- }
10611
- }
10612
- };
10613
10818
  var createApp = (props) => {
10614
10819
  const app = new App2(props.appConfig.name);
10615
10820
  const zones = new Stack(app, "zones");
@@ -10633,17 +10838,10 @@ var createApp = (props) => {
10633
10838
  const bindListeners = [];
10634
10839
  const globalEnv = [];
10635
10840
  const globalEnvListeners = [];
10636
- const allLocalEnv = {};
10637
- const allLocalEnvListeners = {};
10638
10841
  const globalPermissions = [];
10639
10842
  const globalPermissionCallbacks = [];
10640
10843
  const appPermissions = [];
10641
10844
  const appPermissionCallbacks = [];
10642
- const allStackPermissions = {};
10643
- const allStackPermissionCallbacks = {};
10644
- for (const stackConfig of props.stackConfigs) {
10645
- assertDepsExists(stackConfig, props.stackConfigs);
10646
- }
10647
10845
  for (const feature of features) {
10648
10846
  feature.onBefore?.({
10649
10847
  ...props,
@@ -10716,14 +10914,6 @@ var createApp = (props) => {
10716
10914
  }
10717
10915
  for (const stackConfig of props.stackConfigs) {
10718
10916
  const stack = new Stack(app, stackConfig.name);
10719
- const localEnvListeners = [];
10720
- const localEnv = [];
10721
- const stackPermissions = [];
10722
- const stackPermissionCallbacks = [];
10723
- allStackPermissions[stack.name] = stackPermissions;
10724
- allStackPermissionCallbacks[stack.name] = stackPermissionCallbacks;
10725
- allLocalEnvListeners[stack.name] = localEnvListeners;
10726
- allLocalEnv[stack.name] = localEnv;
10727
10917
  for (const feature of features) {
10728
10918
  feature.onStack?.({
10729
10919
  ...props,
@@ -10737,7 +10927,6 @@ var createApp = (props) => {
10737
10927
  shared,
10738
10928
  onPermission(callback) {
10739
10929
  globalPermissionCallbacks.push(callback);
10740
- stackPermissionCallbacks.push(callback);
10741
10930
  },
10742
10931
  addGlobalPermission(permission) {
10743
10932
  globalPermissions.push(permission);
@@ -10745,9 +10934,6 @@ var createApp = (props) => {
10745
10934
  addAppPermission(permission) {
10746
10935
  appPermissions.push(permission);
10747
10936
  },
10748
- addStackPermission(permission) {
10749
- stackPermissions.push(permission);
10750
- },
10751
10937
  addWarning(props2) {
10752
10938
  warnings.push(props2);
10753
10939
  },
@@ -10811,10 +10997,10 @@ var createApp = (props) => {
10811
10997
  bindListeners.push(cb);
10812
10998
  },
10813
10999
  addEnv(name, value) {
10814
- localEnv.push({ name, value });
11000
+ globalEnv.push({ name, value });
10815
11001
  },
10816
11002
  onEnv(cb) {
10817
- localEnvListeners.push(cb);
11003
+ globalEnvListeners.push(cb);
10818
11004
  },
10819
11005
  onReady(cb) {
10820
11006
  readyListeners.push(cb);
@@ -10824,16 +11010,6 @@ var createApp = (props) => {
10824
11010
  }
10825
11011
  });
10826
11012
  }
10827
- for (const callback of stackPermissionCallbacks) {
10828
- for (const permission of stackPermissions) {
10829
- callback(permission);
10830
- }
10831
- }
10832
- for (const listener of localEnvListeners) {
10833
- for (const env of localEnv) {
10834
- listener(env.name, env.value);
10835
- }
10836
- }
10837
11013
  }
10838
11014
  for (const callback of appPermissionCallbacks) {
10839
11015
  for (const permission of appPermissions) {
@@ -10855,24 +11031,6 @@ var createApp = (props) => {
10855
11031
  listener(name, value);
10856
11032
  }
10857
11033
  }
10858
- for (const stackConfig of props.stackConfigs) {
10859
- const envListeners = allLocalEnvListeners[stackConfig.name];
10860
- const permissionCallbacks = allStackPermissionCallbacks[stackConfig.name];
10861
- for (const dependency of stackConfig.depends ?? []) {
10862
- const permissions = allStackPermissions[dependency];
10863
- const env = allLocalEnv[dependency];
10864
- for (const permission of permissions) {
10865
- for (const callback of permissionCallbacks) {
10866
- callback(permission);
10867
- }
10868
- }
10869
- for (const entry of env) {
10870
- for (const listener of envListeners) {
10871
- listener(entry.name, entry.value);
10872
- }
10873
- }
10874
- }
10875
- }
10876
11034
  const ready = () => {
10877
11035
  for (const listener of readyListeners) {
10878
11036
  listener();
@@ -13507,10 +13665,13 @@ program.on("option:no-cache", () => {
13507
13665
  commands10.forEach((fn) => fn(program));
13508
13666
 
13509
13667
  // src/bin.ts
13510
- var interrupt = (code) => () => {
13668
+ var interrupt = (signal, code) => () => {
13511
13669
  process.stdout.write("\x1B[?25h");
13670
+ if (signal === "SIGINT" && process.listenerCount(signal) > 1) {
13671
+ return;
13672
+ }
13512
13673
  process.exit(code);
13513
13674
  };
13514
- process.on("SIGINT", interrupt(130));
13515
- process.on("SIGTERM", interrupt(143));
13675
+ process.on("SIGINT", interrupt("SIGINT", 130));
13676
+ process.on("SIGTERM", interrupt("SIGTERM", 143));
13516
13677
  program.parse(process.argv);
@@ -483,6 +483,70 @@ var InstanceDefaultSchema = z15.object({
483
483
  // src/feature/router/schema.ts
484
484
  import { days as days3, minutes as minutes2, parse as parse3 } from "@awsless/duration";
485
485
  import { z as z16 } from "zod";
486
+
487
+ // src/error.ts
488
+ var ExpectedError = class extends Error {
489
+ };
490
+
491
+ // src/feature/router/pattern.ts
492
+ var PARAM_TOKEN = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\*/g;
493
+ var escapeRegex = (value) => {
494
+ return value.replace(/[|\\{}()[\]^$+*?.\-]/g, "\\$&");
495
+ };
496
+ var compileRoutePattern = (pattern) => {
497
+ if (!pattern.startsWith("/")) {
498
+ throw new ExpectedError(`Route pattern "${pattern}" must start with a slash (/)`);
499
+ }
500
+ if (pattern === "/*") {
501
+ return { key: pattern };
502
+ }
503
+ const params = [];
504
+ let regex = "";
505
+ let stars = 0;
506
+ let last = 0;
507
+ let token;
508
+ PARAM_TOKEN.lastIndex = 0;
509
+ while (token = PARAM_TOKEN.exec(pattern)) {
510
+ regex += escapeRegex(pattern.slice(last, token.index));
511
+ const param = token[1];
512
+ if (param) {
513
+ if (params.includes(param)) {
514
+ throw new ExpectedError(`Duplicate param "${param}" in route pattern "${pattern}"`);
515
+ }
516
+ params.push(param);
517
+ regex += "([^/]+)";
518
+ } else {
519
+ stars++;
520
+ regex += ".*";
521
+ }
522
+ last = PARAM_TOKEN.lastIndex;
523
+ }
524
+ if (params.length === 0 && stars === 0) {
525
+ return { key: pattern };
526
+ }
527
+ regex += escapeRegex(pattern.slice(last));
528
+ const root = pattern.split("/")[1] ?? "";
529
+ if (root === "" || root.includes("*") || root.includes("{")) {
530
+ throw new ExpectedError(
531
+ `The first path segment of route pattern "${pattern}" must be static when the pattern contains params or wildcards.`
532
+ );
533
+ }
534
+ if (root.includes(".")) {
535
+ throw new ExpectedError(
536
+ `The first path segment of route pattern "${pattern}" can't contain a dot when the pattern contains params or wildcards.`
537
+ );
538
+ }
539
+ if (params.length === 0 && pattern === `/${root}/*`) {
540
+ return { key: pattern };
541
+ }
542
+ return {
543
+ key: `/${root}/*`,
544
+ match: `^${regex}$`,
545
+ params: params.length > 0 ? params : void 0
546
+ };
547
+ };
548
+
549
+ // src/feature/router/schema.ts
486
550
  var ErrorResponsePathSchema = z16.string().describe(
487
551
  [
488
552
  "The path to the custom error page that you want to return to the viewer when your origin returns the HTTP status code specified.",
@@ -512,6 +576,28 @@ var ErrorResponseSchema = z16.union([
512
576
  })
513
577
  ]).optional();
514
578
  var RouteSchema = z16.string().regex(/^\//, "Route must start with a slash (/)").regex(/^\/([^/*.]+)?$/, 'Router paths mount a single segment without dots, like "/api".');
579
+ var RoutesSchema = z16.record(
580
+ ResourceIdSchema.describe("The router id to add your routes to."),
581
+ z16.record(z16.string().regex(/^\//, "Route must start with a slash (/)"), FunctionSchema).superRefine((routes, ctx) => {
582
+ for (const pattern of Object.keys(routes)) {
583
+ try {
584
+ compileRoutePattern(pattern);
585
+ } catch (error) {
586
+ ctx.addIssue({
587
+ code: z16.ZodIssueCode.custom,
588
+ path: [pattern],
589
+ message: error instanceof Error ? error.message : `Invalid route pattern: ${pattern}`
590
+ });
591
+ }
592
+ }
593
+ }).describe(
594
+ [
595
+ "Define the routes and the lambda function that should handle them.",
596
+ 'Routes can be an exact path like "/sitemap.xml", a wildcard like "/sitemap/*", or contain params like "/sitemap/{locale}/{page}.xml".',
597
+ 'Param values are passed to the function as "x-param-[NAME]" request headers.'
598
+ ].join("\n")
599
+ )
600
+ ).optional().describe("Add routes to your global Router that link a path pattern to a lambda function.");
515
601
  var VisibilitySchema = z16.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
516
602
  var WafSettingsSchema = z16.object({
517
603
  rateLimiter: z16.object({
@@ -1619,14 +1705,13 @@ var TestsSchema = z40.union([
1619
1705
  ]).describe("Define the location of your tests for your stack.").optional();
1620
1706
 
1621
1707
  // src/config/stack.ts
1622
- var DependsSchema = ResourceIdSchema.array().optional().describe("Define the stacks that this stack is depended on.");
1623
1708
  var NameSchema = ResourceIdSchema.refine((name) => !["base", "hostedzones"].includes(name), {
1624
1709
  message: `Stack name can't be a reserved name.`
1625
1710
  }).describe("Stack name.");
1626
1711
  var StackSchema = z41.object({
1627
1712
  $schema: z41.string().optional(),
1628
1713
  name: NameSchema,
1629
- depends: DependsSchema,
1714
+ routes: RoutesSchema,
1630
1715
  commands: CommandsSchema,
1631
1716
  // auth: AuthSchema,
1632
1717
  // http: HttpSchema,