@awsless/cli 0.0.42 → 0.0.44

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
@@ -1507,6 +1507,66 @@ var InstanceDefaultSchema = z20.object({
1507
1507
  // src/feature/router/schema.ts
1508
1508
  import { days as days3, minutes as minutes2, parse as parse3 } from "@awsless/duration";
1509
1509
  import { z as z21 } from "zod";
1510
+
1511
+ // src/feature/router/pattern.ts
1512
+ var PARAM_TOKEN = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\*/g;
1513
+ var escapeRegex = (value) => {
1514
+ return value.replace(/[|\\{}()[\]^$+*?.\-]/g, "\\$&");
1515
+ };
1516
+ var compileRoutePattern = (pattern) => {
1517
+ if (!pattern.startsWith("/")) {
1518
+ throw new ExpectedError(`Route pattern "${pattern}" must start with a slash (/)`);
1519
+ }
1520
+ if (pattern === "/*") {
1521
+ return { key: pattern };
1522
+ }
1523
+ const params = [];
1524
+ let regex = "";
1525
+ let stars = 0;
1526
+ let last = 0;
1527
+ let token;
1528
+ PARAM_TOKEN.lastIndex = 0;
1529
+ while (token = PARAM_TOKEN.exec(pattern)) {
1530
+ regex += escapeRegex(pattern.slice(last, token.index));
1531
+ const param = token[1];
1532
+ if (param) {
1533
+ if (params.includes(param)) {
1534
+ throw new ExpectedError(`Duplicate param "${param}" in route pattern "${pattern}"`);
1535
+ }
1536
+ params.push(param);
1537
+ regex += "([^/]+)";
1538
+ } else {
1539
+ stars++;
1540
+ regex += ".*";
1541
+ }
1542
+ last = PARAM_TOKEN.lastIndex;
1543
+ }
1544
+ if (params.length === 0 && stars === 0) {
1545
+ return { key: pattern };
1546
+ }
1547
+ regex += escapeRegex(pattern.slice(last));
1548
+ const root2 = pattern.split("/")[1] ?? "";
1549
+ if (root2 === "" || root2.includes("*") || root2.includes("{")) {
1550
+ throw new ExpectedError(
1551
+ `The first path segment of route pattern "${pattern}" must be static when the pattern contains params or wildcards.`
1552
+ );
1553
+ }
1554
+ if (root2.includes(".")) {
1555
+ throw new ExpectedError(
1556
+ `The first path segment of route pattern "${pattern}" can't contain a dot when the pattern contains params or wildcards.`
1557
+ );
1558
+ }
1559
+ if (params.length === 0 && pattern === `/${root2}/*`) {
1560
+ return { key: pattern };
1561
+ }
1562
+ return {
1563
+ key: `/${root2}/*`,
1564
+ match: `^${regex}$`,
1565
+ params: params.length > 0 ? params : void 0
1566
+ };
1567
+ };
1568
+
1569
+ // src/feature/router/schema.ts
1510
1570
  var ErrorResponsePathSchema = z21.string().describe(
1511
1571
  [
1512
1572
  "The path to the custom error page that you want to return to the viewer when your origin returns the HTTP status code specified.",
@@ -1536,6 +1596,28 @@ var ErrorResponseSchema = z21.union([
1536
1596
  })
1537
1597
  ]).optional();
1538
1598
  var RouteSchema = z21.string().regex(/^\//, "Route must start with a slash (/)");
1599
+ var RoutesSchema = z21.record(
1600
+ ResourceIdSchema.describe("The router id to add your routes to."),
1601
+ z21.record(RouteSchema, FunctionSchema).superRefine((routes, ctx) => {
1602
+ for (const pattern of Object.keys(routes)) {
1603
+ try {
1604
+ compileRoutePattern(pattern);
1605
+ } catch (error) {
1606
+ ctx.addIssue({
1607
+ code: z21.ZodIssueCode.custom,
1608
+ path: [pattern],
1609
+ message: error instanceof Error ? error.message : `Invalid route pattern: ${pattern}`
1610
+ });
1611
+ }
1612
+ }
1613
+ }).describe(
1614
+ [
1615
+ "Define the routes and the lambda function that should handle them.",
1616
+ 'Routes can be an exact path like "/sitemap.xml", a wildcard like "/sitemap/*", or contain params like "/sitemap/{locale}/{page}.xml".',
1617
+ 'Param values are passed to the function as "x-param-[NAME]" request headers.'
1618
+ ].join("\n")
1619
+ )
1620
+ ).optional().describe("Add routes to your global Router that link a path pattern to a lambda function.");
1539
1621
  var VisibilitySchema = z21.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
1540
1622
  var WafSettingsSchema = z21.object({
1541
1623
  rateLimiter: z21.object({
@@ -2649,6 +2731,7 @@ var StackSchema = z45.object({
2649
2731
  // auth: AuthSchema,
2650
2732
  // http: HttpSchema,
2651
2733
  rest: RestSchema,
2734
+ routes: RoutesSchema,
2652
2735
  rpc: RpcSchema,
2653
2736
  configs: ConfigsSchema,
2654
2737
  crons: CronsSchema,
@@ -9076,7 +9159,7 @@ var metricFeature = defineFeature({
9076
9159
  import { days as days12, seconds as seconds12, toSeconds as toSeconds12, years } from "@awsless/duration";
9077
9160
  import { Future, Group as Group31 } from "@terraforge/core";
9078
9161
  import { aws as aws32 } from "@terraforge/aws";
9079
- import { camelCase as camelCase9, constantCase as constantCase16 } from "change-case";
9162
+ import { camelCase as camelCase9, constantCase as constantCase16, kebabCase as kebabCase9 } from "change-case";
9080
9163
 
9081
9164
  // src/feature/router/router-code.ts
9082
9165
  var getViewerRequestFunctionCode = (props) => {
@@ -9166,20 +9249,74 @@ function isValidRoute(route, method) {
9166
9249
  return true;
9167
9250
  }
9168
9251
 
9252
+ function matchRoute(value, path, method) {
9253
+ const list = Array.isArray(value) ? value : [value];
9254
+
9255
+ for(const i in list) {
9256
+ const route = list[i];
9257
+
9258
+ if(!isValidRoute(route, method)) {
9259
+ continue;
9260
+ }
9261
+
9262
+ if(route.match) {
9263
+ const found = path.match(new RegExp(route.match));
9264
+
9265
+ if(!found) {
9266
+ continue;
9267
+ }
9268
+
9269
+ const params = {};
9270
+
9271
+ if(route.params) {
9272
+ for(const p in route.params) {
9273
+ params[route.params[p]] = found[Number(p) + 1];
9274
+ }
9275
+ }
9276
+
9277
+ return { route, params };
9278
+ }
9279
+
9280
+ return { route };
9281
+ }
9282
+ }
9283
+
9169
9284
  async function findRoute(path, method) {
9170
9285
  const store = cf.kvs();
9171
9286
  const keys = getPossibleRouteKeys(path);
9172
9287
 
9173
9288
  for(const i in keys) {
9174
9289
  const key = keys[i];
9290
+ let value;
9175
9291
 
9176
9292
  try {
9177
- const route = await store.get(key, { format: 'json' });
9293
+ value = await store.get(key, { format: 'json' });
9294
+ } catch (e) {
9295
+ continue;
9296
+ }
9178
9297
 
9179
- if(isValidRoute(route, method)) {
9180
- return route;
9298
+ // Route lists that are too big for a single key value pair
9299
+ // are sharded over multiple entries behind a route index.
9300
+ if(value && value.list) {
9301
+ for(let n = 0; n < value.list; n++) {
9302
+ try {
9303
+ const route = await store.get(key + '#' + n, { format: 'json' });
9304
+ const result = matchRoute(route, path, method);
9305
+
9306
+ if(result) {
9307
+ return result;
9308
+ }
9309
+ } catch (e) {}
9181
9310
  }
9182
- } catch (e) {}
9311
+
9312
+ continue;
9313
+ }
9314
+
9315
+ const result = matchRoute(value, path, method);
9316
+
9317
+ if(result) {
9318
+ return result;
9319
+ }
9183
9320
  }
9184
9321
  }
9185
9322
 
@@ -9284,11 +9421,19 @@ async function handler(event) {
9284
9421
 
9285
9422
  ${injection.join("\n")}
9286
9423
 
9287
- const route = await findRoute(path, request.method);
9424
+ const result = await findRoute(path, request.method);
9425
+
9426
+ if(result) {
9427
+ const route = result.route;
9288
9428
 
9289
- if(route) {
9290
9429
  setRouteOrigin(route);
9291
9430
 
9431
+ if(result.params) {
9432
+ for(const name in result.params) {
9433
+ headers['x-param-' + name.toLowerCase()] = { value: encodeURIComponent(result.params[name]) };
9434
+ }
9435
+ }
9436
+
9292
9437
  if(route.forwardHost && headers.host && headers.host.value) {
9293
9438
  headers['x-forwarded-host'] = { value: headers.host.value };
9294
9439
  }
@@ -9325,6 +9470,32 @@ async function handler(event) {
9325
9470
 
9326
9471
  // src/feature/router/index.ts
9327
9472
  import { createHash as createHash8 } from "crypto";
9473
+
9474
+ // src/feature/router/route.ts
9475
+ var MAX_VALUE_SIZE = 1e3;
9476
+ var createRouteStoreEntries = (routes) => {
9477
+ const entries = [];
9478
+ for (const [key, value] of Object.entries(routes)) {
9479
+ const json = JSON.stringify(value);
9480
+ if (Array.isArray(value) && Buffer.byteLength(json, "utf8") > MAX_VALUE_SIZE) {
9481
+ entries.push({
9482
+ key,
9483
+ value: JSON.stringify({ list: value.length })
9484
+ });
9485
+ value.forEach((route, index) => {
9486
+ entries.push({
9487
+ key: `${key}#${index}`,
9488
+ value: JSON.stringify(route)
9489
+ });
9490
+ });
9491
+ } else {
9492
+ entries.push({ key, value: json });
9493
+ }
9494
+ }
9495
+ return entries;
9496
+ };
9497
+
9498
+ // src/feature/router/index.ts
9328
9499
  var routerFeature = defineFeature({
9329
9500
  name: "router",
9330
9501
  onApp(ctx) {
@@ -9354,9 +9525,7 @@ var routerFeature = defineFeature({
9354
9525
  {
9355
9526
  kvsArn: routeStore.arn,
9356
9527
  keys: $resolve([routes], (routes2) => {
9357
- return Object.entries(routes2).map(([key, value]) => {
9358
- return { key, value: JSON.stringify(value) };
9359
- });
9528
+ return createRouteStoreEntries(routes2);
9360
9529
  })
9361
9530
  },
9362
9531
  {
@@ -9717,6 +9886,67 @@ var routerFeature = defineFeature({
9717
9886
  ctx.bind(`ROUTER_${constantCase16(id)}_ENDPOINT`, domainName);
9718
9887
  }
9719
9888
  }
9889
+ },
9890
+ onStack(ctx) {
9891
+ for (const [id, routes] of Object.entries(ctx.stackConfig.routes ?? {})) {
9892
+ if (!ctx.appConfig.defaults.router?.[id]) {
9893
+ throw new FileError(ctx.stackConfig.file, `Router "${id}" is not defined on the app level.`);
9894
+ }
9895
+ const group = new Group31(ctx.stack, "routes", id);
9896
+ const addRoutes = ctx.shared.entry("router", "addRoutes", id);
9897
+ const addInvalidation = ctx.shared.entry("router", "addInvalidation", id);
9898
+ const grouped = {};
9899
+ const invalidationPaths = /* @__PURE__ */ new Set();
9900
+ const versions = [];
9901
+ for (const [pattern, props] of Object.entries(routes)) {
9902
+ const compiled = compileRoutePattern(pattern);
9903
+ const slug = pattern.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20);
9904
+ const entryId = kebabCase9(`${slug || "root"}-${shortId(pattern)}`);
9905
+ const routeGroup = new Group31(group, "route", entryId);
9906
+ const result = createLambdaFunction(routeGroup, ctx, "route", entryId, props);
9907
+ versions.push(result.code.sourceHash);
9908
+ ctx.onBind((name, value) => {
9909
+ result.setEnvironment(name, value);
9910
+ });
9911
+ new aws32.lambda.Permission(routeGroup, "permission", {
9912
+ principal: "cloudfront.amazonaws.com",
9913
+ action: "lambda:InvokeFunctionUrl",
9914
+ functionName: result.lambda.functionName,
9915
+ functionUrlAuthType: "AWS_IAM",
9916
+ sourceArn: `arn:aws:cloudfront::${ctx.accountId}:*`
9917
+ });
9918
+ new aws32.lambda.Permission(routeGroup, "invoke-permission", {
9919
+ principal: "cloudfront.amazonaws.com",
9920
+ action: "lambda:InvokeFunction",
9921
+ functionName: result.lambda.functionName,
9922
+ sourceArn: `arn:aws:cloudfront::${ctx.accountId}:*`
9923
+ });
9924
+ const url = new aws32.lambda.FunctionUrl(routeGroup, "url", {
9925
+ functionName: result.lambda.functionName,
9926
+ authorizationType: "AWS_IAM"
9927
+ });
9928
+ grouped[compiled.key] ??= [];
9929
+ grouped[compiled.key].push({
9930
+ type: "lambda",
9931
+ domainName: url.functionUrl.pipe((url2) => url2.split("/")[2]),
9932
+ forwardHost: true,
9933
+ urlEncodedQueryString: true,
9934
+ match: compiled.match,
9935
+ params: compiled.params
9936
+ });
9937
+ invalidationPaths.add(compiled.key);
9938
+ }
9939
+ const merged = {};
9940
+ for (const [key, list3] of Object.entries(grouped)) {
9941
+ if (list3.length === 1) {
9942
+ merged[key] = list3[0];
9943
+ } else {
9944
+ merged[key] = [...list3.filter((route) => route.match), ...list3.filter((route) => !route.match)];
9945
+ }
9946
+ }
9947
+ addRoutes(group, "routes", merged);
9948
+ addInvalidation(group, "invalidate", [...invalidationPaths], versions);
9949
+ }
9720
9950
  }
9721
9951
  });
9722
9952
 
@@ -561,6 +561,70 @@ var InstanceDefaultSchema = z16.object({
561
561
  // src/feature/router/schema.ts
562
562
  import { days as days3, minutes as minutes2, parse as parse3 } from "@awsless/duration";
563
563
  import { z as z17 } from "zod";
564
+
565
+ // src/error.ts
566
+ var ExpectedError = class extends Error {
567
+ };
568
+
569
+ // src/feature/router/pattern.ts
570
+ var PARAM_TOKEN = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\*/g;
571
+ var escapeRegex = (value) => {
572
+ return value.replace(/[|\\{}()[\]^$+*?.\-]/g, "\\$&");
573
+ };
574
+ var compileRoutePattern = (pattern) => {
575
+ if (!pattern.startsWith("/")) {
576
+ throw new ExpectedError(`Route pattern "${pattern}" must start with a slash (/)`);
577
+ }
578
+ if (pattern === "/*") {
579
+ return { key: pattern };
580
+ }
581
+ const params = [];
582
+ let regex = "";
583
+ let stars = 0;
584
+ let last = 0;
585
+ let token;
586
+ PARAM_TOKEN.lastIndex = 0;
587
+ while (token = PARAM_TOKEN.exec(pattern)) {
588
+ regex += escapeRegex(pattern.slice(last, token.index));
589
+ const param = token[1];
590
+ if (param) {
591
+ if (params.includes(param)) {
592
+ throw new ExpectedError(`Duplicate param "${param}" in route pattern "${pattern}"`);
593
+ }
594
+ params.push(param);
595
+ regex += "([^/]+)";
596
+ } else {
597
+ stars++;
598
+ regex += ".*";
599
+ }
600
+ last = PARAM_TOKEN.lastIndex;
601
+ }
602
+ if (params.length === 0 && stars === 0) {
603
+ return { key: pattern };
604
+ }
605
+ regex += escapeRegex(pattern.slice(last));
606
+ const root = pattern.split("/")[1] ?? "";
607
+ if (root === "" || root.includes("*") || root.includes("{")) {
608
+ throw new ExpectedError(
609
+ `The first path segment of route pattern "${pattern}" must be static when the pattern contains params or wildcards.`
610
+ );
611
+ }
612
+ if (root.includes(".")) {
613
+ throw new ExpectedError(
614
+ `The first path segment of route pattern "${pattern}" can't contain a dot when the pattern contains params or wildcards.`
615
+ );
616
+ }
617
+ if (params.length === 0 && pattern === `/${root}/*`) {
618
+ return { key: pattern };
619
+ }
620
+ return {
621
+ key: `/${root}/*`,
622
+ match: `^${regex}$`,
623
+ params: params.length > 0 ? params : void 0
624
+ };
625
+ };
626
+
627
+ // src/feature/router/schema.ts
564
628
  var ErrorResponsePathSchema = z17.string().describe(
565
629
  [
566
630
  "The path to the custom error page that you want to return to the viewer when your origin returns the HTTP status code specified.",
@@ -590,6 +654,28 @@ var ErrorResponseSchema = z17.union([
590
654
  })
591
655
  ]).optional();
592
656
  var RouteSchema = z17.string().regex(/^\//, "Route must start with a slash (/)");
657
+ var RoutesSchema = z17.record(
658
+ ResourceIdSchema.describe("The router id to add your routes to."),
659
+ z17.record(RouteSchema, FunctionSchema).superRefine((routes, ctx) => {
660
+ for (const pattern of Object.keys(routes)) {
661
+ try {
662
+ compileRoutePattern(pattern);
663
+ } catch (error) {
664
+ ctx.addIssue({
665
+ code: z17.ZodIssueCode.custom,
666
+ path: [pattern],
667
+ message: error instanceof Error ? error.message : `Invalid route pattern: ${pattern}`
668
+ });
669
+ }
670
+ }
671
+ }).describe(
672
+ [
673
+ "Define the routes and the lambda function that should handle them.",
674
+ 'Routes can be an exact path like "/sitemap.xml", a wildcard like "/sitemap/*", or contain params like "/sitemap/{locale}/{page}.xml".',
675
+ 'Param values are passed to the function as "x-param-[NAME]" request headers.'
676
+ ].join("\n")
677
+ )
678
+ ).optional().describe("Add routes to your global Router that link a path pattern to a lambda function.");
593
679
  var VisibilitySchema = z17.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
594
680
  var WafSettingsSchema = z17.object({
595
681
  rateLimiter: z17.object({
@@ -1702,6 +1788,7 @@ var StackSchema = z41.object({
1702
1788
  // auth: AuthSchema,
1703
1789
  // http: HttpSchema,
1704
1790
  rest: RestSchema,
1791
+ routes: RoutesSchema,
1705
1792
  rpc: RpcSchema,
1706
1793
  configs: ConfigsSchema,
1707
1794
  crons: CronsSchema,
Binary file
Binary file
Binary file
Binary file