@awsless/cli 0.0.42 → 0.0.43

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,18 +9249,49 @@ 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
- const key = keys[i];
9175
-
9176
9289
  try {
9177
- const route = await store.get(key, { format: 'json' });
9290
+ const value = await store.get(keys[i], { format: 'json' });
9291
+ const result = matchRoute(value, path, method);
9178
9292
 
9179
- if(isValidRoute(route, method)) {
9180
- return route;
9293
+ if(result) {
9294
+ return result;
9181
9295
  }
9182
9296
  } catch (e) {}
9183
9297
  }
@@ -9284,11 +9398,19 @@ async function handler(event) {
9284
9398
 
9285
9399
  ${injection.join("\n")}
9286
9400
 
9287
- const route = await findRoute(path, request.method);
9401
+ const result = await findRoute(path, request.method);
9402
+
9403
+ if(result) {
9404
+ const route = result.route;
9288
9405
 
9289
- if(route) {
9290
9406
  setRouteOrigin(route);
9291
9407
 
9408
+ if(result.params) {
9409
+ for(const name in result.params) {
9410
+ headers['x-param-' + name.toLowerCase()] = { value: encodeURIComponent(result.params[name]) };
9411
+ }
9412
+ }
9413
+
9292
9414
  if(route.forwardHost && headers.host && headers.host.value) {
9293
9415
  headers['x-forwarded-host'] = { value: headers.host.value };
9294
9416
  }
@@ -9717,6 +9839,67 @@ var routerFeature = defineFeature({
9717
9839
  ctx.bind(`ROUTER_${constantCase16(id)}_ENDPOINT`, domainName);
9718
9840
  }
9719
9841
  }
9842
+ },
9843
+ onStack(ctx) {
9844
+ for (const [id, routes] of Object.entries(ctx.stackConfig.routes ?? {})) {
9845
+ if (!ctx.appConfig.defaults.router?.[id]) {
9846
+ throw new FileError(ctx.stackConfig.file, `Router "${id}" is not defined on the app level.`);
9847
+ }
9848
+ const group = new Group31(ctx.stack, "routes", id);
9849
+ const addRoutes = ctx.shared.entry("router", "addRoutes", id);
9850
+ const addInvalidation = ctx.shared.entry("router", "addInvalidation", id);
9851
+ const grouped = {};
9852
+ const invalidationPaths = /* @__PURE__ */ new Set();
9853
+ const versions = [];
9854
+ for (const [pattern, props] of Object.entries(routes)) {
9855
+ const compiled = compileRoutePattern(pattern);
9856
+ const slug = pattern.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20);
9857
+ const entryId = kebabCase9(`${slug || "root"}-${shortId(pattern)}`);
9858
+ const routeGroup = new Group31(group, "route", entryId);
9859
+ const result = createLambdaFunction(routeGroup, ctx, "route", entryId, props);
9860
+ versions.push(result.code.sourceHash);
9861
+ ctx.onBind((name, value) => {
9862
+ result.setEnvironment(name, value);
9863
+ });
9864
+ new aws32.lambda.Permission(routeGroup, "permission", {
9865
+ principal: "cloudfront.amazonaws.com",
9866
+ action: "lambda:InvokeFunctionUrl",
9867
+ functionName: result.lambda.functionName,
9868
+ functionUrlAuthType: "AWS_IAM",
9869
+ sourceArn: `arn:aws:cloudfront::${ctx.accountId}:*`
9870
+ });
9871
+ new aws32.lambda.Permission(routeGroup, "invoke-permission", {
9872
+ principal: "cloudfront.amazonaws.com",
9873
+ action: "lambda:InvokeFunction",
9874
+ functionName: result.lambda.functionName,
9875
+ sourceArn: `arn:aws:cloudfront::${ctx.accountId}:*`
9876
+ });
9877
+ const url = new aws32.lambda.FunctionUrl(routeGroup, "url", {
9878
+ functionName: result.lambda.functionName,
9879
+ authorizationType: "AWS_IAM"
9880
+ });
9881
+ grouped[compiled.key] ??= [];
9882
+ grouped[compiled.key].push({
9883
+ type: "lambda",
9884
+ domainName: url.functionUrl.pipe((url2) => url2.split("/")[2]),
9885
+ forwardHost: true,
9886
+ urlEncodedQueryString: true,
9887
+ match: compiled.match,
9888
+ params: compiled.params
9889
+ });
9890
+ invalidationPaths.add(compiled.key);
9891
+ }
9892
+ const merged = {};
9893
+ for (const [key, list3] of Object.entries(grouped)) {
9894
+ if (list3.length === 1) {
9895
+ merged[key] = list3[0];
9896
+ } else {
9897
+ merged[key] = [...list3.filter((route) => route.match), ...list3.filter((route) => !route.match)];
9898
+ }
9899
+ }
9900
+ addRoutes(group, "routes", merged);
9901
+ addInvalidation(group, "invalidate", [...invalidationPaths], versions);
9902
+ }
9720
9903
  }
9721
9904
  });
9722
9905
 
@@ -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