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 +44 -9
- package/dist/aws.js +44 -9
- package/dist/cli.js +44 -9
- package/package.json +1 -1
- package/src/provider/aws/edge-handler.ts +10 -3
- package/src/provider/aws/renderer.ts +4 -3
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
|
|
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
|
|
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
|
-
|
|
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
|
|
848
|
-
console.log(`[qlara/aws] Uploaded ${
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
844
|
-
console.log(`[qlara/aws] Uploaded ${
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
852
|
-
console.log(`[qlara/aws] Uploaded ${
|
|
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:
|
|
991
|
+
MemorySize: 512,
|
|
957
992
|
Timeout: 30,
|
|
958
993
|
Environment: {
|
|
959
994
|
Variables: config.env ?? {}
|
package/package.json
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|