qlara 0.1.3 → 0.1.5

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
@@ -69,12 +69,12 @@ function getContentType(filePath) {
69
69
  const ext = (0, import_node_path.extname)(filePath).toLowerCase();
70
70
  return CONTENT_TYPES[ext] || "application/octet-stream";
71
71
  }
72
- function getCacheControl(key) {
72
+ function getCacheControl(key, cacheTtl) {
73
73
  if (key.includes("_next/static/") || key.includes(".chunk.")) {
74
74
  return "public, max-age=31536000, immutable";
75
75
  }
76
76
  if (key.endsWith(".html")) {
77
- return "public, max-age=0, must-revalidate";
77
+ return `public, max-age=0, s-maxage=${cacheTtl}, stale-while-revalidate=60`;
78
78
  }
79
79
  return "public, max-age=86400";
80
80
  }
@@ -95,8 +95,26 @@ function listFiles(dir) {
95
95
  }
96
96
  return files;
97
97
  }
98
- async function syncToS3(client, bucketName, buildDir) {
98
+ async function listAllKeys(client, bucketName) {
99
+ const keys = /* @__PURE__ */ new Set();
100
+ let continuationToken;
101
+ do {
102
+ const response = await client.send(
103
+ new import_client_s3.ListObjectsV2Command({
104
+ Bucket: bucketName,
105
+ ContinuationToken: continuationToken
106
+ })
107
+ );
108
+ for (const obj of response.Contents || []) {
109
+ if (obj.Key) keys.add(obj.Key);
110
+ }
111
+ continuationToken = response.NextContinuationToken;
112
+ } while (continuationToken);
113
+ return keys;
114
+ }
115
+ async function syncToS3(client, bucketName, buildDir, cacheTtl = 3600) {
99
116
  const files = listFiles(buildDir);
117
+ const newKeys = /* @__PURE__ */ new Set();
100
118
  let uploaded = 0;
101
119
  const batchSize = 10;
102
120
  for (let i = 0; i < files.length; i += batchSize) {
@@ -104,9 +122,10 @@ async function syncToS3(client, bucketName, buildDir) {
104
122
  await Promise.all(
105
123
  batch.map(async (filePath) => {
106
124
  const key = (0, import_node_path.relative)(buildDir, filePath);
125
+ newKeys.add(key);
107
126
  const body = (0, import_node_fs.readFileSync)(filePath);
108
127
  const contentType = getContentType(filePath);
109
- const cacheControl = getCacheControl(key);
128
+ const cacheControl = getCacheControl(key, cacheTtl);
110
129
  await client.send(
111
130
  new import_client_s3.PutObjectCommand({
112
131
  Bucket: bucketName,
@@ -120,7 +139,22 @@ async function syncToS3(client, bucketName, buildDir) {
120
139
  })
121
140
  );
122
141
  }
123
- return uploaded;
142
+ const existingKeys = await listAllKeys(client, bucketName);
143
+ const staleKeys = [...existingKeys].filter((key) => !newKeys.has(key));
144
+ let deleted = 0;
145
+ for (let i = 0; i < staleKeys.length; i += 1e3) {
146
+ const batch = staleKeys.slice(i, i + 1e3);
147
+ await client.send(
148
+ new import_client_s3.DeleteObjectsCommand({
149
+ Bucket: bucketName,
150
+ Delete: {
151
+ Objects: batch.map((key) => ({ Key: key }))
152
+ }
153
+ })
154
+ );
155
+ deleted += batch.length;
156
+ }
157
+ return { uploaded, deleted };
124
158
  }
125
159
  async function emptyBucket(client, bucketName) {
126
160
  let continuationToken;
@@ -844,8 +878,8 @@ function aws(awsConfig = {}) {
844
878
  console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
845
879
  console.log("[qlara/aws] Syncing build output to S3...");
846
880
  const s3 = createS3Client(res.region);
847
- const fileCount = await syncToS3(s3, res.bucketName, buildDir);
848
- console.log(`[qlara/aws] Uploaded ${fileCount} files to S3`);
881
+ const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir, cacheTtl);
882
+ console.log(`[qlara/aws] Uploaded ${uploaded} files, deleted ${deleted} stale files`);
849
883
  console.log("[qlara/aws] Bundling edge handler...");
850
884
  const edgeZip = await bundleEdgeHandler({
851
885
  bucketName: res.bucketName,
@@ -862,7 +896,8 @@ function aws(awsConfig = {}) {
862
896
  await lambda.send(
863
897
  new import_client_lambda.UpdateFunctionConfigurationCommand({
864
898
  FunctionName: res.edgeFunctionArn,
865
- Timeout: 30
899
+ Timeout: 30,
900
+ MemorySize: 512
866
901
  })
867
902
  );
868
903
  await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
@@ -949,7 +984,7 @@ function aws(awsConfig = {}) {
949
984
  new import_client_lambda.UpdateFunctionConfigurationCommand({
950
985
  FunctionName: res.rendererFunctionArn,
951
986
  Layers: [],
952
- MemorySize: 256,
987
+ MemorySize: 512,
953
988
  Timeout: 30,
954
989
  Environment: {
955
990
  Variables: config.env ?? {}
package/dist/aws.js CHANGED
@@ -66,12 +66,12 @@ function getContentType(filePath) {
66
66
  const ext = extname(filePath).toLowerCase();
67
67
  return CONTENT_TYPES[ext] || "application/octet-stream";
68
68
  }
69
- function getCacheControl(key) {
69
+ function getCacheControl(key, cacheTtl) {
70
70
  if (key.includes("_next/static/") || key.includes(".chunk.")) {
71
71
  return "public, max-age=31536000, immutable";
72
72
  }
73
73
  if (key.endsWith(".html")) {
74
- return "public, max-age=0, must-revalidate";
74
+ return `public, max-age=0, s-maxage=${cacheTtl}, stale-while-revalidate=60`;
75
75
  }
76
76
  return "public, max-age=86400";
77
77
  }
@@ -92,8 +92,26 @@ function listFiles(dir) {
92
92
  }
93
93
  return files;
94
94
  }
95
- async function syncToS3(client, bucketName, buildDir) {
95
+ async function listAllKeys(client, bucketName) {
96
+ const keys = /* @__PURE__ */ new Set();
97
+ let continuationToken;
98
+ do {
99
+ const response = await client.send(
100
+ new ListObjectsV2Command({
101
+ Bucket: bucketName,
102
+ ContinuationToken: continuationToken
103
+ })
104
+ );
105
+ for (const obj of response.Contents || []) {
106
+ if (obj.Key) keys.add(obj.Key);
107
+ }
108
+ continuationToken = response.NextContinuationToken;
109
+ } while (continuationToken);
110
+ return keys;
111
+ }
112
+ async function syncToS3(client, bucketName, buildDir, cacheTtl = 3600) {
96
113
  const files = listFiles(buildDir);
114
+ const newKeys = /* @__PURE__ */ new Set();
97
115
  let uploaded = 0;
98
116
  const batchSize = 10;
99
117
  for (let i = 0; i < files.length; i += batchSize) {
@@ -101,9 +119,10 @@ async function syncToS3(client, bucketName, buildDir) {
101
119
  await Promise.all(
102
120
  batch.map(async (filePath) => {
103
121
  const key = relative(buildDir, filePath);
122
+ newKeys.add(key);
104
123
  const body = readFileSync(filePath);
105
124
  const contentType = getContentType(filePath);
106
- const cacheControl = getCacheControl(key);
125
+ const cacheControl = getCacheControl(key, cacheTtl);
107
126
  await client.send(
108
127
  new PutObjectCommand({
109
128
  Bucket: bucketName,
@@ -117,7 +136,22 @@ async function syncToS3(client, bucketName, buildDir) {
117
136
  })
118
137
  );
119
138
  }
120
- return uploaded;
139
+ const existingKeys = await listAllKeys(client, bucketName);
140
+ const staleKeys = [...existingKeys].filter((key) => !newKeys.has(key));
141
+ let deleted = 0;
142
+ for (let i = 0; i < staleKeys.length; i += 1e3) {
143
+ const batch = staleKeys.slice(i, i + 1e3);
144
+ await client.send(
145
+ new DeleteObjectsCommand({
146
+ Bucket: bucketName,
147
+ Delete: {
148
+ Objects: batch.map((key) => ({ Key: key }))
149
+ }
150
+ })
151
+ );
152
+ deleted += batch.length;
153
+ }
154
+ return { uploaded, deleted };
121
155
  }
122
156
  async function emptyBucket(client, bucketName) {
123
157
  let continuationToken;
@@ -840,8 +874,8 @@ function aws(awsConfig = {}) {
840
874
  console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
841
875
  console.log("[qlara/aws] Syncing build output to S3...");
842
876
  const s3 = createS3Client(res.region);
843
- const fileCount = await syncToS3(s3, res.bucketName, buildDir);
844
- console.log(`[qlara/aws] Uploaded ${fileCount} files to S3`);
877
+ const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir, cacheTtl);
878
+ console.log(`[qlara/aws] Uploaded ${uploaded} files, deleted ${deleted} stale files`);
845
879
  console.log("[qlara/aws] Bundling edge handler...");
846
880
  const edgeZip = await bundleEdgeHandler({
847
881
  bucketName: res.bucketName,
@@ -858,7 +892,8 @@ function aws(awsConfig = {}) {
858
892
  await lambda.send(
859
893
  new UpdateFunctionConfigurationCommand({
860
894
  FunctionName: res.edgeFunctionArn,
861
- Timeout: 30
895
+ Timeout: 30,
896
+ MemorySize: 512
862
897
  })
863
898
  );
864
899
  await waitUntilFunctionUpdatedV2(
@@ -945,7 +980,7 @@ function aws(awsConfig = {}) {
945
980
  new UpdateFunctionConfigurationCommand({
946
981
  FunctionName: res.rendererFunctionArn,
947
982
  Layers: [],
948
- MemorySize: 256,
983
+ MemorySize: 512,
949
984
  Timeout: 30,
950
985
  Environment: {
951
986
  Variables: config.env ?? {}
package/dist/cli.js CHANGED
@@ -77,12 +77,12 @@ function getContentType(filePath) {
77
77
  const ext = extname(filePath).toLowerCase();
78
78
  return CONTENT_TYPES[ext] || "application/octet-stream";
79
79
  }
80
- function getCacheControl(key) {
80
+ function getCacheControl(key, cacheTtl) {
81
81
  if (key.includes("_next/static/") || key.includes(".chunk.")) {
82
82
  return "public, max-age=31536000, immutable";
83
83
  }
84
84
  if (key.endsWith(".html")) {
85
- return "public, max-age=0, must-revalidate";
85
+ return `public, max-age=0, s-maxage=${cacheTtl}, stale-while-revalidate=60`;
86
86
  }
87
87
  return "public, max-age=86400";
88
88
  }
@@ -103,8 +103,26 @@ function listFiles(dir) {
103
103
  }
104
104
  return files;
105
105
  }
106
- async function syncToS3(client, bucketName, buildDir) {
106
+ async function listAllKeys(client, bucketName) {
107
+ const keys = /* @__PURE__ */ new Set();
108
+ let continuationToken;
109
+ do {
110
+ const response = await client.send(
111
+ new ListObjectsV2Command({
112
+ Bucket: bucketName,
113
+ ContinuationToken: continuationToken
114
+ })
115
+ );
116
+ for (const obj of response.Contents || []) {
117
+ if (obj.Key) keys.add(obj.Key);
118
+ }
119
+ continuationToken = response.NextContinuationToken;
120
+ } while (continuationToken);
121
+ return keys;
122
+ }
123
+ async function syncToS3(client, bucketName, buildDir, cacheTtl = 3600) {
107
124
  const files = listFiles(buildDir);
125
+ const newKeys = /* @__PURE__ */ new Set();
108
126
  let uploaded = 0;
109
127
  const batchSize = 10;
110
128
  for (let i = 0; i < files.length; i += batchSize) {
@@ -112,9 +130,10 @@ async function syncToS3(client, bucketName, buildDir) {
112
130
  await Promise.all(
113
131
  batch.map(async (filePath) => {
114
132
  const key = relative(buildDir, filePath);
133
+ newKeys.add(key);
115
134
  const body = readFileSync(filePath);
116
135
  const contentType = getContentType(filePath);
117
- const cacheControl = getCacheControl(key);
136
+ const cacheControl = getCacheControl(key, cacheTtl);
118
137
  await client.send(
119
138
  new PutObjectCommand({
120
139
  Bucket: bucketName,
@@ -128,7 +147,22 @@ async function syncToS3(client, bucketName, buildDir) {
128
147
  })
129
148
  );
130
149
  }
131
- return uploaded;
150
+ const existingKeys = await listAllKeys(client, bucketName);
151
+ const staleKeys = [...existingKeys].filter((key) => !newKeys.has(key));
152
+ let deleted = 0;
153
+ for (let i = 0; i < staleKeys.length; i += 1e3) {
154
+ const batch = staleKeys.slice(i, i + 1e3);
155
+ await client.send(
156
+ new DeleteObjectsCommand({
157
+ Bucket: bucketName,
158
+ Delete: {
159
+ Objects: batch.map((key) => ({ Key: key }))
160
+ }
161
+ })
162
+ );
163
+ deleted += batch.length;
164
+ }
165
+ return { uploaded, deleted };
132
166
  }
133
167
  async function emptyBucket(client, bucketName) {
134
168
  let continuationToken;
@@ -848,8 +882,8 @@ function aws(awsConfig = {}) {
848
882
  console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
849
883
  console.log("[qlara/aws] Syncing build output to S3...");
850
884
  const s3 = createS3Client(res.region);
851
- const fileCount = await syncToS3(s3, res.bucketName, buildDir);
852
- console.log(`[qlara/aws] Uploaded ${fileCount} files to S3`);
885
+ const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir, cacheTtl);
886
+ console.log(`[qlara/aws] Uploaded ${uploaded} files, deleted ${deleted} stale files`);
853
887
  console.log("[qlara/aws] Bundling edge handler...");
854
888
  const edgeZip = await bundleEdgeHandler({
855
889
  bucketName: res.bucketName,
@@ -866,7 +900,8 @@ function aws(awsConfig = {}) {
866
900
  await lambda.send(
867
901
  new UpdateFunctionConfigurationCommand({
868
902
  FunctionName: res.edgeFunctionArn,
869
- Timeout: 30
903
+ Timeout: 30,
904
+ MemorySize: 512
870
905
  })
871
906
  );
872
907
  await waitUntilFunctionUpdatedV2(
@@ -953,7 +988,7 @@ function aws(awsConfig = {}) {
953
988
  new UpdateFunctionConfigurationCommand({
954
989
  FunctionName: res.rendererFunctionArn,
955
990
  Layers: [],
956
- MemorySize: 256,
991
+ MemorySize: 512,
957
992
  Timeout: 30,
958
993
  Environment: {
959
994
  Variables: config.env ?? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qlara",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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": [
@@ -276,13 +276,20 @@ export async function handler(
276
276
 
277
277
  // At this point, the file does NOT exist in S3 (403 from OAC or 404)
278
278
 
279
- // 2. Fetch manifest and check if this URL matches a Qlara dynamic route
279
+ // 2. Skip non-HTML file requests these are never dynamic routes
280
+ // e.g. /product/20.txt (RSC flight data), /product/20.json, etc.
281
+ const nonHtmlExt = uri.match(/\.([a-z0-9]+)$/)?.[1];
282
+ if (nonHtmlExt && nonHtmlExt !== 'html') {
283
+ return response;
284
+ }
285
+
286
+ // 3. Fetch manifest and check if this URL matches a Qlara dynamic route
280
287
  const manifest = await getManifest();
281
288
  // Strip .html suffix that the URL rewrite function adds before matching
282
289
  const cleanUri = uri.replace(/\.html$/, '');
283
290
  const match = manifest ? matchRoute(cleanUri, manifest.routes) : null;
284
291
 
285
- // 3. If route matches: invoke renderer synchronously to get fully rendered HTML
292
+ // 4. If route matches: invoke renderer synchronously to get fully rendered HTML
286
293
  if (match) {
287
294
  // Try to render with full SEO metadata (synchronous — waits for result)
288
295
  const renderedHtml = await invokeRenderer(cleanUri, match);
@@ -299,6 +306,6 @@ export async function handler(
299
306
  }
300
307
  }
301
308
 
302
- // 4. No match or no fallback — return original error
309
+ // 5. No match or no fallback — return original error
303
310
  return response;
304
311
  }
@@ -79,6 +79,9 @@ interface RendererResult {
79
79
 
80
80
  const FALLBACK_PLACEHOLDER = '__QLARA_FALLBACK__';
81
81
 
82
+ // Module-scope S3 client — reused across warm invocations (avoids recreating TCP/TLS connections)
83
+ const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
84
+
82
85
  /**
83
86
  * Derive the S3 key for a rendered page.
84
87
  * Matches the Next.js static export convention: /product/42 → product/42.html
@@ -86,6 +89,7 @@ const FALLBACK_PLACEHOLDER = '__QLARA_FALLBACK__';
86
89
  function deriveS3Key(uri: string): string {
87
90
  const cleanUri = uri.replace(/^\//, '').replace(/\/$/, '');
88
91
  if (!cleanUri) return 'index.html';
92
+ if (cleanUri.endsWith('.html')) return cleanUri;
89
93
  return `${cleanUri}.html`;
90
94
  }
91
95
 
@@ -762,9 +766,6 @@ export async function handler(event: RendererEvent & { warmup?: boolean }): Prom
762
766
  }
763
767
 
764
768
  const { uri, bucket, routePattern, params } = event;
765
- const region = process.env.AWS_REGION || 'us-east-1';
766
-
767
- const s3 = new S3Client({ region });
768
769
 
769
770
  try {
770
771
  // 0. Check if already rendered + read fallback in parallel