qlara 0.1.8 → 0.1.10
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 +4 -3
- package/dist/aws.d.cts +1 -1
- package/dist/aws.d.ts +1 -1
- package/dist/aws.js +4 -3
- package/dist/cli.js +4 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/plugin/next.cjs +2 -1
- package/dist/plugin/next.d.cts +1 -1
- package/dist/plugin/next.d.ts +1 -1
- package/dist/plugin/next.js +2 -1
- package/dist/{types-gl2xFqEX.d.cts → types--KPPgCtc.d.cts} +11 -0
- package/dist/{types-gl2xFqEX.d.ts → types--KPPgCtc.d.ts} +11 -0
- package/package.json +1 -1
- package/src/provider/aws/renderer.ts +368 -3
- package/src/types.ts +11 -0
package/dist/aws.cjs
CHANGED
|
@@ -562,7 +562,7 @@ async function bundleEdgeHandler(config) {
|
|
|
562
562
|
});
|
|
563
563
|
return createZip(outfile, "edge-handler.js");
|
|
564
564
|
}
|
|
565
|
-
async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
565
|
+
async function bundleRenderer(routeFile, cacheTtl = 3600, framework) {
|
|
566
566
|
(0, import_node_fs2.mkdirSync)(BUNDLE_DIR, { recursive: true });
|
|
567
567
|
const outfile = (0, import_node_path2.join)(BUNDLE_DIR, "renderer.js");
|
|
568
568
|
const alias = {};
|
|
@@ -584,7 +584,8 @@ async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
|
584
584
|
minify: true,
|
|
585
585
|
alias,
|
|
586
586
|
define: {
|
|
587
|
-
__QLARA_CACHE_TTL__: String(cacheTtl)
|
|
587
|
+
__QLARA_CACHE_TTL__: String(cacheTtl),
|
|
588
|
+
__QLARA_FRAMEWORK__: JSON.stringify(framework || "")
|
|
588
589
|
},
|
|
589
590
|
external: []
|
|
590
591
|
});
|
|
@@ -964,7 +965,7 @@ function aws(awsConfig = {}) {
|
|
|
964
965
|
const cf = new import_client_cloudfront.CloudFrontClient({ region: res.region });
|
|
965
966
|
await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
|
|
966
967
|
console.log("[qlara/aws] Bundling renderer...");
|
|
967
|
-
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
|
|
968
|
+
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl, config.framework);
|
|
968
969
|
await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
|
|
969
970
|
{ client: lambda, maxWaitTime: 120 },
|
|
970
971
|
{ FunctionName: res.rendererFunctionArn }
|
package/dist/aws.d.cts
CHANGED
package/dist/aws.d.ts
CHANGED
package/dist/aws.js
CHANGED
|
@@ -558,7 +558,7 @@ async function bundleEdgeHandler(config) {
|
|
|
558
558
|
});
|
|
559
559
|
return createZip(outfile, "edge-handler.js");
|
|
560
560
|
}
|
|
561
|
-
async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
561
|
+
async function bundleRenderer(routeFile, cacheTtl = 3600, framework) {
|
|
562
562
|
mkdirSync(BUNDLE_DIR, { recursive: true });
|
|
563
563
|
const outfile = join2(BUNDLE_DIR, "renderer.js");
|
|
564
564
|
const alias = {};
|
|
@@ -580,7 +580,8 @@ async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
|
580
580
|
minify: true,
|
|
581
581
|
alias,
|
|
582
582
|
define: {
|
|
583
|
-
__QLARA_CACHE_TTL__: String(cacheTtl)
|
|
583
|
+
__QLARA_CACHE_TTL__: String(cacheTtl),
|
|
584
|
+
__QLARA_FRAMEWORK__: JSON.stringify(framework || "")
|
|
584
585
|
},
|
|
585
586
|
external: []
|
|
586
587
|
});
|
|
@@ -960,7 +961,7 @@ function aws(awsConfig = {}) {
|
|
|
960
961
|
const cf = new CloudFrontClient({ region: res.region });
|
|
961
962
|
await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
|
|
962
963
|
console.log("[qlara/aws] Bundling renderer...");
|
|
963
|
-
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
|
|
964
|
+
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl, config.framework);
|
|
964
965
|
await waitUntilFunctionUpdatedV2(
|
|
965
966
|
{ client: lambda, maxWaitTime: 120 },
|
|
966
967
|
{ FunctionName: res.rendererFunctionArn }
|
package/dist/cli.js
CHANGED
|
@@ -569,7 +569,7 @@ async function bundleEdgeHandler(config) {
|
|
|
569
569
|
});
|
|
570
570
|
return createZip(outfile, "edge-handler.js");
|
|
571
571
|
}
|
|
572
|
-
async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
572
|
+
async function bundleRenderer(routeFile, cacheTtl = 3600, framework) {
|
|
573
573
|
mkdirSync(BUNDLE_DIR, { recursive: true });
|
|
574
574
|
const outfile = join2(BUNDLE_DIR, "renderer.js");
|
|
575
575
|
const alias = {};
|
|
@@ -591,7 +591,8 @@ async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
|
591
591
|
minify: true,
|
|
592
592
|
alias,
|
|
593
593
|
define: {
|
|
594
|
-
__QLARA_CACHE_TTL__: String(cacheTtl)
|
|
594
|
+
__QLARA_CACHE_TTL__: String(cacheTtl),
|
|
595
|
+
__QLARA_FRAMEWORK__: JSON.stringify(framework || "")
|
|
595
596
|
},
|
|
596
597
|
external: []
|
|
597
598
|
});
|
|
@@ -968,7 +969,7 @@ function aws(awsConfig = {}) {
|
|
|
968
969
|
const cf = new CloudFrontClient({ region: res.region });
|
|
969
970
|
await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
|
|
970
971
|
console.log("[qlara/aws] Bundling renderer...");
|
|
971
|
-
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
|
|
972
|
+
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl, config.framework);
|
|
972
973
|
await waitUntilFunctionUpdatedV2(
|
|
973
974
|
{ client: lambda, maxWaitTime: 120 },
|
|
974
975
|
{ FunctionName: res.rendererFunctionArn }
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types
|
|
2
|
-
export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraVerification } from './types
|
|
1
|
+
import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types--KPPgCtc.cjs';
|
|
2
|
+
export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraVerification } from './types--KPPgCtc.cjs';
|
|
3
3
|
|
|
4
4
|
declare function validateConfig(config: QlaraPluginConfig, routes: QlaraRoute[]): void;
|
|
5
5
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types
|
|
2
|
-
export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraVerification } from './types
|
|
1
|
+
import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types--KPPgCtc.js';
|
|
2
|
+
export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraVerification } from './types--KPPgCtc.js';
|
|
3
3
|
|
|
4
4
|
declare function validateConfig(config: QlaraPluginConfig, routes: QlaraRoute[]): void;
|
|
5
5
|
|
package/dist/plugin/next.cjs
CHANGED
|
@@ -153,7 +153,8 @@ function withQlara(qlaraConfig) {
|
|
|
153
153
|
},
|
|
154
154
|
outputDir,
|
|
155
155
|
routeFile: (0, import_node_path.resolve)(qlaraConfig.routeFile),
|
|
156
|
-
env
|
|
156
|
+
env,
|
|
157
|
+
framework: "next"
|
|
157
158
|
};
|
|
158
159
|
(0, import_node_fs.mkdirSync)(QLARA_DIR, { recursive: true });
|
|
159
160
|
(0, import_node_fs.writeFileSync)(
|
package/dist/plugin/next.d.cts
CHANGED
package/dist/plugin/next.d.ts
CHANGED
package/dist/plugin/next.js
CHANGED
|
@@ -368,6 +368,12 @@ interface QlaraPluginConfig {
|
|
|
368
368
|
provider: QlaraProvider;
|
|
369
369
|
/** Env var names to forward to the renderer Lambda (values read from process.env at build time) */
|
|
370
370
|
env?: string[];
|
|
371
|
+
/**
|
|
372
|
+
* The framework identifier. Set automatically by framework plugins (e.g. 'next' by withQlara).
|
|
373
|
+
* Used by the renderer to enable framework-specific post-render behavior
|
|
374
|
+
* (e.g. generating .txt RSC flight data files for Next.js client-side navigation).
|
|
375
|
+
*/
|
|
376
|
+
framework?: string;
|
|
371
377
|
}
|
|
372
378
|
interface QlaraProvider {
|
|
373
379
|
name: string;
|
|
@@ -394,6 +400,11 @@ interface QlaraDeployConfig {
|
|
|
394
400
|
routeFile: string;
|
|
395
401
|
/** Environment variables for the renderer Lambda (key-value pairs resolved at build time) */
|
|
396
402
|
env?: Record<string, string>;
|
|
403
|
+
/**
|
|
404
|
+
* The framework identifier (e.g. 'next'). Set by framework plugins.
|
|
405
|
+
* Passed to the renderer to enable framework-specific post-render behavior.
|
|
406
|
+
*/
|
|
407
|
+
framework?: string;
|
|
397
408
|
}
|
|
398
409
|
interface QlaraManifest {
|
|
399
410
|
version: 1;
|
|
@@ -368,6 +368,12 @@ interface QlaraPluginConfig {
|
|
|
368
368
|
provider: QlaraProvider;
|
|
369
369
|
/** Env var names to forward to the renderer Lambda (values read from process.env at build time) */
|
|
370
370
|
env?: string[];
|
|
371
|
+
/**
|
|
372
|
+
* The framework identifier. Set automatically by framework plugins (e.g. 'next' by withQlara).
|
|
373
|
+
* Used by the renderer to enable framework-specific post-render behavior
|
|
374
|
+
* (e.g. generating .txt RSC flight data files for Next.js client-side navigation).
|
|
375
|
+
*/
|
|
376
|
+
framework?: string;
|
|
371
377
|
}
|
|
372
378
|
interface QlaraProvider {
|
|
373
379
|
name: string;
|
|
@@ -394,6 +400,11 @@ interface QlaraDeployConfig {
|
|
|
394
400
|
routeFile: string;
|
|
395
401
|
/** Environment variables for the renderer Lambda (key-value pairs resolved at build time) */
|
|
396
402
|
env?: Record<string, string>;
|
|
403
|
+
/**
|
|
404
|
+
* The framework identifier (e.g. 'next'). Set by framework plugins.
|
|
405
|
+
* Passed to the renderer to enable framework-specific post-render behavior.
|
|
406
|
+
*/
|
|
407
|
+
framework?: string;
|
|
397
408
|
}
|
|
398
409
|
interface QlaraManifest {
|
|
399
410
|
version: 1;
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* route definitions, each with a pattern and a metaDataGenerator function.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
18
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
19
19
|
import type {
|
|
20
20
|
QlaraMetadata,
|
|
21
21
|
QlaraOpenGraph,
|
|
@@ -54,6 +54,7 @@ import type {
|
|
|
54
54
|
// At bundle time: '__qlara_routes__' → './qlara.routes.ts' (or wherever the dev put it)
|
|
55
55
|
// Injected at bundle time by esbuild define
|
|
56
56
|
declare const __QLARA_CACHE_TTL__: number;
|
|
57
|
+
declare const __QLARA_FRAMEWORK__: string;
|
|
57
58
|
|
|
58
59
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
59
60
|
// @ts-ignore — resolved at bundle time by esbuild alias
|
|
@@ -759,6 +760,345 @@ function metadataToRscEntries(metadata: QlaraMetadata): string {
|
|
|
759
760
|
return entries.join('');
|
|
760
761
|
}
|
|
761
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Extract RSC flight data from rendered HTML (Next.js-specific).
|
|
765
|
+
*
|
|
766
|
+
* Next.js embeds RSC data inside <script>self.__next_f.push([1,"..."])</script> blocks.
|
|
767
|
+
* The .txt file is these payloads concatenated with JSON string escapes resolved.
|
|
768
|
+
* Next.js's client-side router fetches the .txt file for client-side navigation
|
|
769
|
+
* instead of the full .html — so we must generate it for renderer-created pages.
|
|
770
|
+
*
|
|
771
|
+
* Only called when __QLARA_FRAMEWORK__ === 'next'.
|
|
772
|
+
*/
|
|
773
|
+
function extractRscFlightData(html: string): string | null {
|
|
774
|
+
const chunks: string[] = [];
|
|
775
|
+
const regex = /self\.__next_f\.push\(\[1,"((?:[^"\\]|\\.)*)"\]\)/g;
|
|
776
|
+
let match;
|
|
777
|
+
|
|
778
|
+
while ((match = regex.exec(html)) !== null) {
|
|
779
|
+
// Unescape the JSON string: \" → ", \\ → \, \n → newline
|
|
780
|
+
const unescaped = match[1]
|
|
781
|
+
.replace(/\\n/g, '\n')
|
|
782
|
+
.replace(/\\"/g, '"')
|
|
783
|
+
.replace(/\\\\/g, '\\');
|
|
784
|
+
chunks.push(unescaped);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (chunks.length === 0) return null;
|
|
788
|
+
return chunks.join('');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ── Per-segment prefetch file generation (Next.js 16+) ───────────
|
|
792
|
+
//
|
|
793
|
+
// Next.js 16 introduced per-segment prefetch files in a subdirectory
|
|
794
|
+
// per page (e.g. product/1/__next._tree.txt). The renderer must generate
|
|
795
|
+
// these for renderer-created pages so they match build-time pages.
|
|
796
|
+
//
|
|
797
|
+
// Approach: discover segment files from a build-time reference page on S3,
|
|
798
|
+
// classify each file, and copy/patch as needed. If no reference page exists
|
|
799
|
+
// (Next.js 15 or no build-time pages), segment generation is skipped.
|
|
800
|
+
|
|
801
|
+
/** Cache reference segment directory per route prefix across warm invocations */
|
|
802
|
+
const referencePageCache = new Map<string, string | null>();
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Find a build-time page's segment directory in S3 to use as a template.
|
|
806
|
+
* Returns the S3 key prefix (e.g. 'product/1/') or null.
|
|
807
|
+
*/
|
|
808
|
+
async function findReferenceSegmentDir(bucket: string, routePrefix: string): Promise<string | null> {
|
|
809
|
+
const cached = referencePageCache.get(routePrefix);
|
|
810
|
+
if (cached !== undefined) return cached;
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
const response = await s3.send(new ListObjectsV2Command({
|
|
814
|
+
Bucket: bucket,
|
|
815
|
+
Prefix: `${routePrefix}/`,
|
|
816
|
+
MaxKeys: 200,
|
|
817
|
+
}));
|
|
818
|
+
|
|
819
|
+
for (const obj of response.Contents || []) {
|
|
820
|
+
const key = obj.Key || '';
|
|
821
|
+
if (key.endsWith('/__next._tree.txt')) {
|
|
822
|
+
const dir = key.slice(0, key.length - '__next._tree.txt'.length);
|
|
823
|
+
referencePageCache.set(routePrefix, dir);
|
|
824
|
+
return dir;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
} catch {
|
|
828
|
+
// S3 error — skip segment generation
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
referencePageCache.set(routePrefix, null);
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
type SegmentFileType = 'shared' | 'tree' | 'head' | 'full' | 'page';
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Classify a segment file by its name.
|
|
839
|
+
*/
|
|
840
|
+
function classifySegmentFile(name: string): SegmentFileType {
|
|
841
|
+
if (name === '__next._tree.txt') return 'tree';
|
|
842
|
+
if (name === '__next._head.txt') return 'head';
|
|
843
|
+
if (name === '__next._full.txt') return 'full';
|
|
844
|
+
if (name.includes('__PAGE__')) return 'page';
|
|
845
|
+
return 'shared';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* List all segment file names in a reference directory.
|
|
850
|
+
*/
|
|
851
|
+
async function listReferenceSegmentFiles(bucket: string, refDir: string): Promise<string[]> {
|
|
852
|
+
try {
|
|
853
|
+
const response = await s3.send(new ListObjectsV2Command({
|
|
854
|
+
Bucket: bucket,
|
|
855
|
+
Prefix: `${refDir}__next.`,
|
|
856
|
+
MaxKeys: 50,
|
|
857
|
+
}));
|
|
858
|
+
|
|
859
|
+
return (response.Contents || [])
|
|
860
|
+
.map(obj => obj.Key || '')
|
|
861
|
+
.filter(key => key.endsWith('.txt'))
|
|
862
|
+
.map(key => key.slice(refDir.length));
|
|
863
|
+
} catch {
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Patch the _tree.txt segment: replace the dynamic paramKey value.
|
|
870
|
+
*/
|
|
871
|
+
function patchTreeSegment(template: string, newValue: string): string {
|
|
872
|
+
return template.replace(
|
|
873
|
+
/"paramType":"d","paramKey":"[^"]*"/,
|
|
874
|
+
`"paramType":"d","paramKey":"${newValue}"`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Patch the __PAGE__.txt segment: replace param values in the component props.
|
|
880
|
+
*/
|
|
881
|
+
function patchPageSegment(template: string, params: Record<string, string>): string {
|
|
882
|
+
let result = template;
|
|
883
|
+
for (const [key, value] of Object.entries(params)) {
|
|
884
|
+
// RSC wire format: ["$","$L2",null,{"id":"OLD"}]
|
|
885
|
+
result = result.replace(
|
|
886
|
+
new RegExp(`"${key}":"[^"]*"`),
|
|
887
|
+
`"${key}":"${value}"`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
return result;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Generate the _head.txt segment from a reference template and new metadata.
|
|
895
|
+
* Extracts preamble (module declarations) and buildId from the reference,
|
|
896
|
+
* rebuilds line 0 with new metadata.
|
|
897
|
+
*/
|
|
898
|
+
function generateHeadSegment(template: string, metadata: QlaraMetadata): string {
|
|
899
|
+
// Split into lines — preamble is everything before the line starting with '0:'
|
|
900
|
+
const lines = template.split('\n');
|
|
901
|
+
const preambleLines: string[] = [];
|
|
902
|
+
let line0 = '';
|
|
903
|
+
|
|
904
|
+
for (const line of lines) {
|
|
905
|
+
if (line.startsWith('0:')) {
|
|
906
|
+
line0 = line;
|
|
907
|
+
} else if (line.trim()) {
|
|
908
|
+
preambleLines.push(line);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Extract buildId from reference
|
|
913
|
+
const buildIdMatch = line0.match(/"buildId":"([^"]+)"/);
|
|
914
|
+
const buildId = buildIdMatch ? buildIdMatch[1] : '';
|
|
915
|
+
|
|
916
|
+
// Extract the viewport/charset meta tags from the reference (preserve them)
|
|
917
|
+
// These are inside the rsc at children[1] (the $L2 viewport boundary)
|
|
918
|
+
// We keep the structure identical but replace the metadata children
|
|
919
|
+
const viewportMatch = line0.match(/"children":\[(\["\\?\$","meta"[^\]]*\](?:,\["\\?\$","meta"[^\]]*\])*)\]/);
|
|
920
|
+
const viewportTags = viewportMatch ? viewportMatch[1] : '["\$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]';
|
|
921
|
+
|
|
922
|
+
// Build metadata RSC children array (standard JSON, not HTML-escaped)
|
|
923
|
+
const metaChildren: string[] = [];
|
|
924
|
+
let idx = 0;
|
|
925
|
+
|
|
926
|
+
// Title
|
|
927
|
+
metaChildren.push(`["$","title","${idx++}",{"children":"${escapeJson(metadata.title)}"}]`);
|
|
928
|
+
|
|
929
|
+
// Description
|
|
930
|
+
if (metadata.description) {
|
|
931
|
+
metaChildren.push(`["$","meta","${idx++}",{"name":"description","content":"${escapeJson(metadata.description)}"}]`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Open Graph
|
|
935
|
+
if (metadata.openGraph) {
|
|
936
|
+
const og = metadata.openGraph;
|
|
937
|
+
if (og.title) metaChildren.push(`["$","meta","${idx++}",{"property":"og:title","content":"${escapeJson(og.title)}"}]`);
|
|
938
|
+
if (og.description) metaChildren.push(`["$","meta","${idx++}",{"property":"og:description","content":"${escapeJson(og.description)}"}]`);
|
|
939
|
+
if (og.url) metaChildren.push(`["$","meta","${idx++}",{"property":"og:url","content":"${escapeJson(og.url)}"}]`);
|
|
940
|
+
if (og.siteName) metaChildren.push(`["$","meta","${idx++}",{"property":"og:site_name","content":"${escapeJson(og.siteName)}"}]`);
|
|
941
|
+
if (og.type) metaChildren.push(`["$","meta","${idx++}",{"property":"og:type","content":"${escapeJson(og.type)}"}]`);
|
|
942
|
+
for (const img of toArray(og.images)) {
|
|
943
|
+
if (typeof img === 'string') {
|
|
944
|
+
metaChildren.push(`["$","meta","${idx++}",{"property":"og:image","content":"${escapeJson(img)}"}]`);
|
|
945
|
+
} else {
|
|
946
|
+
metaChildren.push(`["$","meta","${idx++}",{"property":"og:image","content":"${escapeJson(img.url)}"}]`);
|
|
947
|
+
if (img.alt) metaChildren.push(`["$","meta","${idx++}",{"property":"og:image:alt","content":"${escapeJson(img.alt)}"}]`);
|
|
948
|
+
if (img.width !== undefined) metaChildren.push(`["$","meta","${idx++}",{"property":"og:image:width","content":"${String(img.width)}"}]`);
|
|
949
|
+
if (img.height !== undefined) metaChildren.push(`["$","meta","${idx++}",{"property":"og:image:height","content":"${String(img.height)}"}]`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Twitter
|
|
955
|
+
if (metadata.twitter) {
|
|
956
|
+
const tw = metadata.twitter;
|
|
957
|
+
const card = ('card' in tw && tw.card) ? tw.card : 'summary';
|
|
958
|
+
metaChildren.push(`["$","meta","${idx++}",{"name":"twitter:card","content":"${escapeJson(card)}"}]`);
|
|
959
|
+
if (tw.title) metaChildren.push(`["$","meta","${idx++}",{"name":"twitter:title","content":"${escapeJson(tw.title)}"}]`);
|
|
960
|
+
if (tw.description) metaChildren.push(`["$","meta","${idx++}",{"name":"twitter:description","content":"${escapeJson(tw.description)}"}]`);
|
|
961
|
+
for (const img of toArray(tw.images)) {
|
|
962
|
+
if (typeof img === 'string') {
|
|
963
|
+
metaChildren.push(`["$","meta","${idx++}",{"name":"twitter:image","content":"${escapeJson(img)}"}]`);
|
|
964
|
+
} else {
|
|
965
|
+
metaChildren.push(`["$","meta","${idx++}",{"name":"twitter:image","content":"${escapeJson(img.url)}"}]`);
|
|
966
|
+
if (img.alt) metaChildren.push(`["$","meta","${idx++}",{"name":"twitter:image:alt","content":"${escapeJson(img.alt)}"}]`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Reconstruct the _head.txt content by replacing the metadata children
|
|
972
|
+
// in the reference template's line 0 structure
|
|
973
|
+
const metaChildrenStr = metaChildren.join(',');
|
|
974
|
+
|
|
975
|
+
// The _head.txt line 0 structure (from analysis):
|
|
976
|
+
// 0:{"buildId":"...","rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[viewport tags]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[METADATA HERE]}]}]}],null]}],"loading":null,"isPartial":false}
|
|
977
|
+
// Replace the "Next.Metadata" children array
|
|
978
|
+
const newLine0 = line0.replace(
|
|
979
|
+
/("name":"Next\.Metadata","children":\[)[\s\S]*?(\]\})/,
|
|
980
|
+
`$1${metaChildrenStr}$2`
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
return preambleLines.join('\n') + '\n' + newLine0 + '\n';
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Generate per-segment prefetch files for a renderer-created page (Next.js 16+).
|
|
988
|
+
* Reads segment files from a build-time reference page, patches page-specific data,
|
|
989
|
+
* and uploads to the new page's subdirectory.
|
|
990
|
+
*
|
|
991
|
+
* Skips gracefully if no reference page exists (Next.js 15 or first deploy).
|
|
992
|
+
*/
|
|
993
|
+
async function generateSegmentFiles(
|
|
994
|
+
bucket: string,
|
|
995
|
+
uri: string,
|
|
996
|
+
params: Record<string, string>,
|
|
997
|
+
rscData: string | null,
|
|
998
|
+
metadata: QlaraMetadata | null,
|
|
999
|
+
): Promise<void> {
|
|
1000
|
+
const cleanUri = uri.replace(/^\//, '').replace(/\/$/, '');
|
|
1001
|
+
const parts = cleanUri.split('/');
|
|
1002
|
+
const routePrefix = parts.slice(0, -1).join('/'); // 'product'
|
|
1003
|
+
const segmentDir = `${cleanUri}/`; // 'product/42/'
|
|
1004
|
+
|
|
1005
|
+
// Find a build-time page with segment files to use as reference
|
|
1006
|
+
const refDir = await findReferenceSegmentDir(bucket, routePrefix);
|
|
1007
|
+
if (!refDir) return; // No segment files on S3 — Next.js 15 or no build-time pages
|
|
1008
|
+
|
|
1009
|
+
// List all segment files in the reference directory
|
|
1010
|
+
const fileNames = await listReferenceSegmentFiles(bucket, refDir);
|
|
1011
|
+
if (fileNames.length === 0) return;
|
|
1012
|
+
|
|
1013
|
+
// Read all reference files we need (skip _full — we use rscData directly)
|
|
1014
|
+
const filesToRead = fileNames.filter(name => classifySegmentFile(name) !== 'full');
|
|
1015
|
+
const readResults = await Promise.allSettled(
|
|
1016
|
+
filesToRead.map(async (name) => {
|
|
1017
|
+
const result = await s3.send(new GetObjectCommand({
|
|
1018
|
+
Bucket: bucket,
|
|
1019
|
+
Key: `${refDir}${name}`,
|
|
1020
|
+
}));
|
|
1021
|
+
return {
|
|
1022
|
+
name,
|
|
1023
|
+
content: await result.Body?.transformToString('utf-8') || '',
|
|
1024
|
+
};
|
|
1025
|
+
})
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
const refMap = new Map<string, string>();
|
|
1029
|
+
for (const result of readResults) {
|
|
1030
|
+
if (result.status === 'fulfilled' && result.value.content) {
|
|
1031
|
+
refMap.set(result.value.name, result.value.content);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Generate each segment file
|
|
1036
|
+
const paramValue = parts[parts.length - 1]; // last path segment = dynamic param value
|
|
1037
|
+
const uploads: Promise<unknown>[] = [];
|
|
1038
|
+
const cacheControl = `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60`;
|
|
1039
|
+
|
|
1040
|
+
for (const name of fileNames) {
|
|
1041
|
+
let content: string | null = null;
|
|
1042
|
+
const type = classifySegmentFile(name);
|
|
1043
|
+
|
|
1044
|
+
switch (type) {
|
|
1045
|
+
case 'shared':
|
|
1046
|
+
content = refMap.get(name) || null;
|
|
1047
|
+
break;
|
|
1048
|
+
case 'tree': {
|
|
1049
|
+
const template = refMap.get(name);
|
|
1050
|
+
if (template) {
|
|
1051
|
+
content = patchTreeSegment(template, paramValue);
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
case 'head': {
|
|
1056
|
+
const template = refMap.get(name);
|
|
1057
|
+
if (template && metadata) {
|
|
1058
|
+
content = generateHeadSegment(template, metadata);
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
case 'full':
|
|
1063
|
+
content = rscData;
|
|
1064
|
+
break;
|
|
1065
|
+
case 'page': {
|
|
1066
|
+
const template = refMap.get(name);
|
|
1067
|
+
if (template) {
|
|
1068
|
+
content = patchPageSegment(template, params);
|
|
1069
|
+
}
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (content) {
|
|
1075
|
+
uploads.push(
|
|
1076
|
+
s3.send(new PutObjectCommand({
|
|
1077
|
+
Bucket: bucket,
|
|
1078
|
+
Key: `${segmentDir}${name}`,
|
|
1079
|
+
Body: content,
|
|
1080
|
+
ContentType: 'text/plain; charset=utf-8',
|
|
1081
|
+
CacheControl: cacheControl,
|
|
1082
|
+
}))
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
await Promise.all(uploads);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Escape a string for use inside a JSON string value.
|
|
1092
|
+
*/
|
|
1093
|
+
function escapeJson(str: string): string {
|
|
1094
|
+
return str
|
|
1095
|
+
.replace(/\\/g, '\\\\')
|
|
1096
|
+
.replace(/"/g, '\\"')
|
|
1097
|
+
.replace(/\n/g, '\\n')
|
|
1098
|
+
.replace(/\r/g, '\\r')
|
|
1099
|
+
.replace(/\t/g, '\\t');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
762
1102
|
export async function handler(event: RendererEvent & { warmup?: boolean }): Promise<RendererResult> {
|
|
763
1103
|
// Warmup invocation — just initialize the runtime and return
|
|
764
1104
|
if (event.warmup) {
|
|
@@ -816,16 +1156,17 @@ export async function handler(event: RendererEvent & { warmup?: boolean }): Prom
|
|
|
816
1156
|
|
|
817
1157
|
// 3. Call the metaDataGenerator to fetch metadata from the data source
|
|
818
1158
|
const routeDef = routes?.find((r: { route: string }) => r.route === routePattern);
|
|
1159
|
+
let metadata: QlaraMetadata | null = null;
|
|
819
1160
|
|
|
820
1161
|
if (routeDef?.metaDataGenerator) {
|
|
821
|
-
|
|
1162
|
+
metadata = await routeDef.metaDataGenerator(params);
|
|
822
1163
|
if (metadata) {
|
|
823
1164
|
// 4. Patch the HTML with real metadata
|
|
824
1165
|
html = patchMetadata(html, metadata);
|
|
825
1166
|
}
|
|
826
1167
|
}
|
|
827
1168
|
|
|
828
|
-
// 5. Upload to S3
|
|
1169
|
+
// 5. Upload HTML to S3
|
|
829
1170
|
await s3.send(
|
|
830
1171
|
new PutObjectCommand({
|
|
831
1172
|
Bucket: bucket,
|
|
@@ -836,6 +1177,30 @@ export async function handler(event: RendererEvent & { warmup?: boolean }): Prom
|
|
|
836
1177
|
})
|
|
837
1178
|
);
|
|
838
1179
|
|
|
1180
|
+
// 6. Framework-specific post-render uploads
|
|
1181
|
+
// Next.js: extract RSC flight data, upload .txt for client-side navigation,
|
|
1182
|
+
// and generate per-segment prefetch files (Next.js 16+).
|
|
1183
|
+
if (__QLARA_FRAMEWORK__ === 'next') {
|
|
1184
|
+
const rscData = extractRscFlightData(html);
|
|
1185
|
+
if (rscData) {
|
|
1186
|
+
const txtKey = s3Key.replace(/\.html$/, '.txt');
|
|
1187
|
+
await s3.send(
|
|
1188
|
+
new PutObjectCommand({
|
|
1189
|
+
Bucket: bucket,
|
|
1190
|
+
Key: txtKey,
|
|
1191
|
+
Body: rscData,
|
|
1192
|
+
ContentType: 'text/plain; charset=utf-8',
|
|
1193
|
+
CacheControl: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60`,
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// 7. Generate per-segment prefetch files (Next.js 16+ Segment Cache)
|
|
1199
|
+
// Reads templates from a build-time reference page, patches page-specific data.
|
|
1200
|
+
// Skips automatically for Next.js 15 (no segment files on S3).
|
|
1201
|
+
await generateSegmentFiles(bucket, uri, params, rscData, metadata);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
839
1204
|
return {
|
|
840
1205
|
statusCode: 200,
|
|
841
1206
|
body: JSON.stringify({
|
package/src/types.ts
CHANGED
|
@@ -513,6 +513,12 @@ export interface QlaraPluginConfig {
|
|
|
513
513
|
provider: QlaraProvider;
|
|
514
514
|
/** Env var names to forward to the renderer Lambda (values read from process.env at build time) */
|
|
515
515
|
env?: string[];
|
|
516
|
+
/**
|
|
517
|
+
* The framework identifier. Set automatically by framework plugins (e.g. 'next' by withQlara).
|
|
518
|
+
* Used by the renderer to enable framework-specific post-render behavior
|
|
519
|
+
* (e.g. generating .txt RSC flight data files for Next.js client-side navigation).
|
|
520
|
+
*/
|
|
521
|
+
framework?: string;
|
|
516
522
|
}
|
|
517
523
|
|
|
518
524
|
export interface QlaraProvider {
|
|
@@ -542,6 +548,11 @@ export interface QlaraDeployConfig {
|
|
|
542
548
|
routeFile: string;
|
|
543
549
|
/** Environment variables for the renderer Lambda (key-value pairs resolved at build time) */
|
|
544
550
|
env?: Record<string, string>;
|
|
551
|
+
/**
|
|
552
|
+
* The framework identifier (e.g. 'next'). Set by framework plugins.
|
|
553
|
+
* Passed to the renderer to enable framework-specific post-render behavior.
|
|
554
|
+
*/
|
|
555
|
+
framework?: string;
|
|
545
556
|
}
|
|
546
557
|
|
|
547
558
|
export interface QlaraManifest {
|