qlara 0.1.3 → 0.1.4

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
@@ -95,8 +95,26 @@ function listFiles(dir) {
95
95
  }
96
96
  return files;
97
97
  }
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
+ }
98
115
  async function syncToS3(client, bucketName, buildDir) {
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,6 +122,7 @@ 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
128
  const cacheControl = getCacheControl(key);
@@ -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);
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,
package/dist/aws.js CHANGED
@@ -92,8 +92,26 @@ function listFiles(dir) {
92
92
  }
93
93
  return files;
94
94
  }
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
+ }
95
112
  async function syncToS3(client, bucketName, buildDir) {
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,6 +119,7 @@ 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
125
  const cacheControl = getCacheControl(key);
@@ -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);
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,
package/dist/cli.js CHANGED
@@ -103,8 +103,26 @@ function listFiles(dir) {
103
103
  }
104
104
  return files;
105
105
  }
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
+ }
106
123
  async function syncToS3(client, bucketName, buildDir) {
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,6 +130,7 @@ 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
136
  const cacheControl = getCacheControl(key);
@@ -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);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qlara",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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
  }
@@ -86,6 +86,7 @@ const FALLBACK_PLACEHOLDER = '__QLARA_FALLBACK__';
86
86
  function deriveS3Key(uri: string): string {
87
87
  const cleanUri = uri.replace(/^\//, '').replace(/\/$/, '');
88
88
  if (!cleanUri) return 'index.html';
89
+ if (cleanUri.endsWith('.html')) return cleanUri;
89
90
  return `${cleanUri}.html`;
90
91
  }
91
92