qlara 0.1.2 → 0.1.3

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/aws.cjs CHANGED
@@ -520,14 +520,15 @@ async function bundleEdgeHandler(config) {
520
520
  define: {
521
521
  __QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
522
522
  __QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
523
- __QLARA_REGION__: JSON.stringify(config.region)
523
+ __QLARA_REGION__: JSON.stringify(config.region),
524
+ __QLARA_CACHE_TTL__: String(config.cacheTtl)
524
525
  },
525
526
  // Bundle everything — Lambda@Edge must be self-contained
526
527
  external: []
527
528
  });
528
529
  return createZip(outfile, "edge-handler.js");
529
530
  }
530
- async function bundleRenderer(routeFile) {
531
+ async function bundleRenderer(routeFile, cacheTtl = 3600) {
531
532
  (0, import_node_fs2.mkdirSync)(BUNDLE_DIR, { recursive: true });
532
533
  const outfile = (0, import_node_path2.join)(BUNDLE_DIR, "renderer.js");
533
534
  const alias = {};
@@ -548,6 +549,9 @@ async function bundleRenderer(routeFile) {
548
549
  outfile,
549
550
  minify: true,
550
551
  alias,
552
+ define: {
553
+ __QLARA_CACHE_TTL__: String(cacheTtl)
554
+ },
551
555
  external: []
552
556
  });
553
557
  return createZip(outfile, "renderer.js");
@@ -764,6 +768,7 @@ function byoiResources(config) {
764
768
  function aws(awsConfig = {}) {
765
769
  const region = "us-east-1";
766
770
  const byoi = isByoi(awsConfig);
771
+ const cacheTtl = awsConfig.cacheTtl ?? 3600;
767
772
  const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
768
773
  return {
769
774
  name: "aws",
@@ -845,7 +850,8 @@ function aws(awsConfig = {}) {
845
850
  const edgeZip = await bundleEdgeHandler({
846
851
  bucketName: res.bucketName,
847
852
  rendererArn: res.rendererFunctionArn,
848
- region: res.region
853
+ region: res.region,
854
+ cacheTtl
849
855
  });
850
856
  const lambda = new import_client_lambda.LambdaClient({ region: res.region });
851
857
  console.log("[qlara/aws] Waiting for edge handler to be ready...");
@@ -922,7 +928,7 @@ function aws(awsConfig = {}) {
922
928
  const cf = new import_client_cloudfront.CloudFrontClient({ region: res.region });
923
929
  await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
924
930
  console.log("[qlara/aws] Bundling renderer...");
925
- const rendererZip = await bundleRenderer(config.routeFile);
931
+ const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
926
932
  await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
927
933
  { client: lambda, maxWaitTime: 120 },
928
934
  { FunctionName: res.rendererFunctionArn }
@@ -997,6 +1003,21 @@ function aws(awsConfig = {}) {
997
1003
  `[qlara/aws] Could not update renderer permissions: ${err.message}`
998
1004
  );
999
1005
  }
1006
+ console.log("[qlara/aws] Warming up renderer...");
1007
+ try {
1008
+ await lambda.send(
1009
+ new import_client_lambda.InvokeCommand({
1010
+ FunctionName: res.rendererFunctionArn,
1011
+ InvocationType: "RequestResponse",
1012
+ Payload: JSON.stringify({ warmup: true })
1013
+ })
1014
+ );
1015
+ console.log("[qlara/aws] Renderer warmed up");
1016
+ } catch (err) {
1017
+ console.warn(
1018
+ `[qlara/aws] Renderer warm-up failed (non-critical): ${err.message}`
1019
+ );
1020
+ }
1000
1021
  console.log("[qlara/aws] Invalidating CloudFront cache...");
1001
1022
  await cf.send(
1002
1023
  new import_client_cloudfront.CreateInvalidationCommand({
package/dist/aws.d.cts CHANGED
@@ -3,6 +3,16 @@ import { P as ProviderResources, Q as QlaraProvider } from './types-gl2xFqEX.cjs
3
3
  interface AwsConfig {
4
4
  stackName?: string;
5
5
  bucketName?: string;
6
+ /**
7
+ * How long (in seconds) CloudFront should cache dynamically rendered pages.
8
+ * This sets the `s-maxage` value on the Cache-Control header.
9
+ *
10
+ * - Browsers always revalidate with CloudFront (`max-age=0`)
11
+ * - CloudFront serves from edge cache for this duration
12
+ *
13
+ * @default 3600 (1 hour)
14
+ */
15
+ cacheTtl?: number;
6
16
  distributionId?: string;
7
17
  distributionDomain?: string;
8
18
  edgeFunctionArn?: string;
package/dist/aws.d.ts CHANGED
@@ -3,6 +3,16 @@ import { P as ProviderResources, Q as QlaraProvider } from './types-gl2xFqEX.js'
3
3
  interface AwsConfig {
4
4
  stackName?: string;
5
5
  bucketName?: string;
6
+ /**
7
+ * How long (in seconds) CloudFront should cache dynamically rendered pages.
8
+ * This sets the `s-maxage` value on the Cache-Control header.
9
+ *
10
+ * - Browsers always revalidate with CloudFront (`max-age=0`)
11
+ * - CloudFront serves from edge cache for this duration
12
+ *
13
+ * @default 3600 (1 hour)
14
+ */
15
+ cacheTtl?: number;
6
16
  distributionId?: string;
7
17
  distributionDomain?: string;
8
18
  edgeFunctionArn?: string;
package/dist/aws.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  UpdateFunctionConfigurationCommand,
16
16
  PublishVersionCommand,
17
17
  AddPermissionCommand,
18
+ InvokeCommand,
18
19
  waitUntilFunctionUpdatedV2
19
20
  } from "@aws-sdk/client-lambda";
20
21
  import {
@@ -515,14 +516,15 @@ async function bundleEdgeHandler(config) {
515
516
  define: {
516
517
  __QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
517
518
  __QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
518
- __QLARA_REGION__: JSON.stringify(config.region)
519
+ __QLARA_REGION__: JSON.stringify(config.region),
520
+ __QLARA_CACHE_TTL__: String(config.cacheTtl)
519
521
  },
520
522
  // Bundle everything — Lambda@Edge must be self-contained
521
523
  external: []
522
524
  });
523
525
  return createZip(outfile, "edge-handler.js");
524
526
  }
525
- async function bundleRenderer(routeFile) {
527
+ async function bundleRenderer(routeFile, cacheTtl = 3600) {
526
528
  mkdirSync(BUNDLE_DIR, { recursive: true });
527
529
  const outfile = join2(BUNDLE_DIR, "renderer.js");
528
530
  const alias = {};
@@ -543,6 +545,9 @@ async function bundleRenderer(routeFile) {
543
545
  outfile,
544
546
  minify: true,
545
547
  alias,
548
+ define: {
549
+ __QLARA_CACHE_TTL__: String(cacheTtl)
550
+ },
546
551
  external: []
547
552
  });
548
553
  return createZip(outfile, "renderer.js");
@@ -759,6 +764,7 @@ function byoiResources(config) {
759
764
  function aws(awsConfig = {}) {
760
765
  const region = "us-east-1";
761
766
  const byoi = isByoi(awsConfig);
767
+ const cacheTtl = awsConfig.cacheTtl ?? 3600;
762
768
  const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
763
769
  return {
764
770
  name: "aws",
@@ -840,7 +846,8 @@ function aws(awsConfig = {}) {
840
846
  const edgeZip = await bundleEdgeHandler({
841
847
  bucketName: res.bucketName,
842
848
  rendererArn: res.rendererFunctionArn,
843
- region: res.region
849
+ region: res.region,
850
+ cacheTtl
844
851
  });
845
852
  const lambda = new LambdaClient({ region: res.region });
846
853
  console.log("[qlara/aws] Waiting for edge handler to be ready...");
@@ -917,7 +924,7 @@ function aws(awsConfig = {}) {
917
924
  const cf = new CloudFrontClient({ region: res.region });
918
925
  await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
919
926
  console.log("[qlara/aws] Bundling renderer...");
920
- const rendererZip = await bundleRenderer(config.routeFile);
927
+ const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
921
928
  await waitUntilFunctionUpdatedV2(
922
929
  { client: lambda, maxWaitTime: 120 },
923
930
  { FunctionName: res.rendererFunctionArn }
@@ -992,6 +999,21 @@ function aws(awsConfig = {}) {
992
999
  `[qlara/aws] Could not update renderer permissions: ${err.message}`
993
1000
  );
994
1001
  }
1002
+ console.log("[qlara/aws] Warming up renderer...");
1003
+ try {
1004
+ await lambda.send(
1005
+ new InvokeCommand({
1006
+ FunctionName: res.rendererFunctionArn,
1007
+ InvocationType: "RequestResponse",
1008
+ Payload: JSON.stringify({ warmup: true })
1009
+ })
1010
+ );
1011
+ console.log("[qlara/aws] Renderer warmed up");
1012
+ } catch (err) {
1013
+ console.warn(
1014
+ `[qlara/aws] Renderer warm-up failed (non-critical): ${err.message}`
1015
+ );
1016
+ }
995
1017
  console.log("[qlara/aws] Invalidating CloudFront cache...");
996
1018
  await cf.send(
997
1019
  new CreateInvalidationCommand({
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  UpdateFunctionConfigurationCommand,
27
27
  PublishVersionCommand,
28
28
  AddPermissionCommand,
29
+ InvokeCommand,
29
30
  waitUntilFunctionUpdatedV2
30
31
  } from "@aws-sdk/client-lambda";
31
32
  import {
@@ -526,14 +527,15 @@ async function bundleEdgeHandler(config) {
526
527
  define: {
527
528
  __QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
528
529
  __QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
529
- __QLARA_REGION__: JSON.stringify(config.region)
530
+ __QLARA_REGION__: JSON.stringify(config.region),
531
+ __QLARA_CACHE_TTL__: String(config.cacheTtl)
530
532
  },
531
533
  // Bundle everything — Lambda@Edge must be self-contained
532
534
  external: []
533
535
  });
534
536
  return createZip(outfile, "edge-handler.js");
535
537
  }
536
- async function bundleRenderer(routeFile) {
538
+ async function bundleRenderer(routeFile, cacheTtl = 3600) {
537
539
  mkdirSync(BUNDLE_DIR, { recursive: true });
538
540
  const outfile = join2(BUNDLE_DIR, "renderer.js");
539
541
  const alias = {};
@@ -554,6 +556,9 @@ async function bundleRenderer(routeFile) {
554
556
  outfile,
555
557
  minify: true,
556
558
  alias,
559
+ define: {
560
+ __QLARA_CACHE_TTL__: String(cacheTtl)
561
+ },
557
562
  external: []
558
563
  });
559
564
  return createZip(outfile, "renderer.js");
@@ -767,6 +772,7 @@ function byoiResources(config) {
767
772
  function aws(awsConfig = {}) {
768
773
  const region = "us-east-1";
769
774
  const byoi = isByoi(awsConfig);
775
+ const cacheTtl = awsConfig.cacheTtl ?? 3600;
770
776
  const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
771
777
  return {
772
778
  name: "aws",
@@ -848,7 +854,8 @@ function aws(awsConfig = {}) {
848
854
  const edgeZip = await bundleEdgeHandler({
849
855
  bucketName: res.bucketName,
850
856
  rendererArn: res.rendererFunctionArn,
851
- region: res.region
857
+ region: res.region,
858
+ cacheTtl
852
859
  });
853
860
  const lambda = new LambdaClient({ region: res.region });
854
861
  console.log("[qlara/aws] Waiting for edge handler to be ready...");
@@ -925,7 +932,7 @@ function aws(awsConfig = {}) {
925
932
  const cf = new CloudFrontClient({ region: res.region });
926
933
  await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
927
934
  console.log("[qlara/aws] Bundling renderer...");
928
- const rendererZip = await bundleRenderer(config.routeFile);
935
+ const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
929
936
  await waitUntilFunctionUpdatedV2(
930
937
  { client: lambda, maxWaitTime: 120 },
931
938
  { FunctionName: res.rendererFunctionArn }
@@ -1000,6 +1007,21 @@ function aws(awsConfig = {}) {
1000
1007
  `[qlara/aws] Could not update renderer permissions: ${err.message}`
1001
1008
  );
1002
1009
  }
1010
+ console.log("[qlara/aws] Warming up renderer...");
1011
+ try {
1012
+ await lambda.send(
1013
+ new InvokeCommand({
1014
+ FunctionName: res.rendererFunctionArn,
1015
+ InvocationType: "RequestResponse",
1016
+ Payload: JSON.stringify({ warmup: true })
1017
+ })
1018
+ );
1019
+ console.log("[qlara/aws] Renderer warmed up");
1020
+ } catch (err) {
1021
+ console.warn(
1022
+ `[qlara/aws] Renderer warm-up failed (non-critical): ${err.message}`
1023
+ );
1024
+ }
1003
1025
  console.log("[qlara/aws] Invalidating CloudFront cache...");
1004
1026
  await cf.send(
1005
1027
  new CreateInvalidationCommand({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qlara",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Runtime ISR for static React apps — dynamic routing and SEO metadata for statically exported Next.js apps on AWS",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -24,6 +24,7 @@ import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
24
24
  declare const __QLARA_BUCKET_NAME__: string;
25
25
  declare const __QLARA_RENDERER_ARN__: string;
26
26
  declare const __QLARA_REGION__: string;
27
+ declare const __QLARA_CACHE_TTL__: number;
27
28
  // ── Types (inlined to keep bundle self-contained) ────────────────
28
29
 
29
30
  interface ManifestRoute {
@@ -238,7 +239,7 @@ function buildHtmlResponse(
238
239
  { key: 'Content-Type', value: 'text/html; charset=utf-8' },
239
240
  ],
240
241
  'cache-control': [
241
- { key: 'Cache-Control', value: 'public, max-age=0, must-revalidate' },
242
+ { key: 'Cache-Control', value: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60` },
242
243
  ],
243
244
  };
244
245
 
@@ -52,6 +52,9 @@ import type {
52
52
  // The routes module is resolved by esbuild at deploy time.
53
53
  // esbuild's `alias` option maps this import to the developer's route file.
54
54
  // At bundle time: '__qlara_routes__' → './qlara.routes.ts' (or wherever the dev put it)
55
+ // Injected at bundle time by esbuild define
56
+ declare const __QLARA_CACHE_TTL__: number;
57
+
55
58
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
56
59
  // @ts-ignore — resolved at bundle time by esbuild alias
57
60
  import routes from '__qlara_routes__';
@@ -752,20 +755,30 @@ function metadataToRscEntries(metadata: QlaraMetadata): string {
752
755
  return entries.join('');
753
756
  }
754
757
 
755
- export async function handler(event: RendererEvent): Promise<RendererResult> {
758
+ export async function handler(event: RendererEvent & { warmup?: boolean }): Promise<RendererResult> {
759
+ // Warmup invocation — just initialize the runtime and return
760
+ if (event.warmup) {
761
+ return { statusCode: 200, body: 'warm' };
762
+ }
763
+
756
764
  const { uri, bucket, routePattern, params } = event;
757
765
  const region = process.env.AWS_REGION || 'us-east-1';
758
766
 
759
767
  const s3 = new S3Client({ region });
760
768
 
761
769
  try {
762
- // 0. Check if the page was already rendered (guards against duplicate concurrent requests)
770
+ // 0. Check if already rendered + read fallback in parallel
763
771
  const s3Key = deriveS3Key(uri);
764
- try {
765
- const existing = await s3.send(
766
- new GetObjectCommand({ Bucket: bucket, Key: s3Key })
767
- );
768
- const existingHtml = await existing.Body?.transformToString('utf-8');
772
+ const fallbackKey = deriveFallbackKey(uri);
773
+
774
+ const [existingResult, fallbackResult] = await Promise.allSettled([
775
+ s3.send(new GetObjectCommand({ Bucket: bucket, Key: s3Key })),
776
+ s3.send(new GetObjectCommand({ Bucket: bucket, Key: fallbackKey })),
777
+ ]);
778
+
779
+ // If page already exists, return it (guards against duplicate concurrent requests)
780
+ if (existingResult.status === 'fulfilled') {
781
+ const existingHtml = await existingResult.value.Body?.transformToString('utf-8');
769
782
  if (existingHtml) {
770
783
  return {
771
784
  statusCode: 200,
@@ -773,26 +786,20 @@ export async function handler(event: RendererEvent): Promise<RendererResult> {
773
786
  html: existingHtml,
774
787
  };
775
788
  }
776
- } catch {
777
- // Page doesn't exist yet — continue with rendering
778
789
  }
779
790
 
780
- // 1. Read the fallback HTML from S3
781
- const fallbackKey = deriveFallbackKey(uri);
791
+ // Extract fallback HTML
782
792
  let fallbackHtml: string;
783
793
 
784
- try {
785
- const response = await s3.send(
786
- new GetObjectCommand({ Bucket: bucket, Key: fallbackKey })
787
- );
788
- fallbackHtml = (await response.Body?.transformToString('utf-8')) || '';
794
+ if (fallbackResult.status === 'fulfilled') {
795
+ fallbackHtml = (await fallbackResult.value.Body?.transformToString('utf-8')) || '';
789
796
  if (!fallbackHtml) {
790
797
  return {
791
798
  statusCode: 500,
792
799
  body: JSON.stringify({ error: `Empty fallback at ${fallbackKey}` }),
793
800
  };
794
801
  }
795
- } catch {
802
+ } else {
796
803
  return {
797
804
  statusCode: 500,
798
805
  body: JSON.stringify({ error: `Fallback not found: ${fallbackKey}` }),
@@ -824,8 +831,7 @@ export async function handler(event: RendererEvent): Promise<RendererResult> {
824
831
  Key: s3Key,
825
832
  Body: html,
826
833
  ContentType: 'text/html; charset=utf-8',
827
- CacheControl:
828
- 'public, max-age=0, s-maxage=31536000, stale-while-revalidate=86400',
834
+ CacheControl: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60`,
829
835
  })
830
836
  );
831
837