rwsdk 1.0.0-beta.27-test.20251116215153 → 1.0.0-beta.29
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/lib/e2e/environment.mjs +6 -1
- package/dist/lib/e2e/release.d.mts +1 -0
- package/dist/lib/e2e/release.mjs +16 -3
- package/dist/lib/e2e/tarball.mjs +3 -10
- package/dist/lib/e2e/testHarness.mjs +6 -3
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +34 -20
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +102 -1
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/links.d.ts +18 -25
- package/dist/runtime/lib/links.js +70 -42
- package/dist/runtime/lib/links.test.d.ts +1 -0
- package/dist/runtime/lib/links.test.js +20 -0
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +23 -9
- package/dist/runtime/lib/router.js +14 -36
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +167 -28
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +43 -9
- package/dist/runtime/worker.d.ts +3 -1
- package/dist/runtime/worker.js +1 -0
- package/dist/use-synced-state/worker.d.mts +1 -1
- package/dist/vite/constants.d.mts +1 -0
- package/dist/vite/constants.mjs +1 -0
- package/dist/vite/ssrBridgePlugin.mjs +9 -0
- package/package.json +2 -2
|
@@ -105,6 +105,72 @@ function splitStreamOnFirstNonHoistedTag(sourceStream) {
|
|
|
105
105
|
});
|
|
106
106
|
return [hoistedTagsStream, appBodyStream];
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* A utility that orchestrates and interleaves three ReadableStreams to produce a
|
|
110
|
+
* single, valid HTML response stream. It uses two special markers:
|
|
111
|
+
*
|
|
112
|
+
* - `startMarker`: Placed in the `outerHtml` stream (the document shell) to
|
|
113
|
+
* designate where the application's content should be injected.
|
|
114
|
+
* - `endMarker`: Injected into the `innerHtml` stream's RSC payload to signal
|
|
115
|
+
* the end of the initial, non-suspended render. This marker is needed for
|
|
116
|
+
* non-blocking hydration, as it allows the stitching process to send the
|
|
117
|
+
* client `<script>` tags before all suspended content has resolved.
|
|
118
|
+
*
|
|
119
|
+
* It manages three main stream readers:
|
|
120
|
+
*
|
|
121
|
+
* - `hoistedTagsReader`: Reads from the `hoistedTagsStream`, which contains only
|
|
122
|
+
* the hoisted meta tags (e.g., `<title>`, `<meta>`).
|
|
123
|
+
* - `outerReader`: Reads from the `outerHtml` stream, which is the server-rendered
|
|
124
|
+
* document shell (containing `<html>`, `<head>`, etc.).
|
|
125
|
+
* - `innerReader`: Reads from the `appBodyStream`, which contains the main
|
|
126
|
+
* application content, stripped of its hoisted tags.
|
|
127
|
+
*
|
|
128
|
+
* The function proceeds through a multi-phase state machine, managed by the
|
|
129
|
+
* `pump` function, to correctly interleave these streams.
|
|
130
|
+
*
|
|
131
|
+
* The state machine moves through the following phases:
|
|
132
|
+
*
|
|
133
|
+
* 1. `read-hoisted`:
|
|
134
|
+
* - **Goal:** Buffer all hoisted tags from the `hoistedTagsStream`.
|
|
135
|
+
* - **Action:** Reads from `hoistedTagsReader` and appends all content into
|
|
136
|
+
* the `hoistedTagsBuffer`. Does not enqueue anything yet.
|
|
137
|
+
* - **Transition:** Moves to `outer-head` when the stream is exhausted.
|
|
138
|
+
*
|
|
139
|
+
* 2. `outer-head`:
|
|
140
|
+
* - **Goal:** Stream the document up to the closing `</head>` tag, inject the
|
|
141
|
+
* hoisted tags, and then continue until the app `startMarker`.
|
|
142
|
+
* - **Action:** Reads from `outerReader`. When it finds `</head>`, it enqueues
|
|
143
|
+
* the content before it, then enqueues the `hoistedTagsBuffer`, and finally
|
|
144
|
+
* enqueues the `</head>` tag itself. It then continues reading from
|
|
145
|
+
* `outerReader` until it finds the `startMarker`.
|
|
146
|
+
* - **Transition:** Moves to `inner-shell` after finding and discarding the
|
|
147
|
+
* `startMarker`.
|
|
148
|
+
*
|
|
149
|
+
* 3. `inner-shell`:
|
|
150
|
+
* - **Goal:** Stream the initial, non-suspended part of the application.
|
|
151
|
+
* - **Action:** Switches to `innerReader`. It enqueues chunks until it finds
|
|
152
|
+
* the `endMarker`. Any content after the marker is stored in
|
|
153
|
+
* `innerSuspendedRemains`.
|
|
154
|
+
* - **Transition:** Moves to `outer-tail` after finding the `endMarker`.
|
|
155
|
+
*
|
|
156
|
+
* 4. `outer-tail`:
|
|
157
|
+
* - **Goal:** Stream the rest of the document's `<body>`, including client
|
|
158
|
+
* `<script>` tags.
|
|
159
|
+
* - **Action:** Switches back to `outerReader` and enqueues chunks until it
|
|
160
|
+
* finds the `</body>` tag.
|
|
161
|
+
* - **Transition:** Moves to `inner-suspended` after finding `</body>`.
|
|
162
|
+
*
|
|
163
|
+
* 5. `inner-suspended`:
|
|
164
|
+
* - **Goal:** Stream any suspended content from the React app.
|
|
165
|
+
* - **Action:** First enqueues any content from `innerSuspendedRemains`, then
|
|
166
|
+
* continues reading from `innerReader` until the stream is exhausted.
|
|
167
|
+
* - **Transition:** Moves to `outer-end` when the stream is exhausted.
|
|
168
|
+
*
|
|
169
|
+
* 6. `outer-end`:
|
|
170
|
+
* - **Goal:** Finish the document.
|
|
171
|
+
* - **Action:** Switches back to `outerReader` for the last time to send the
|
|
172
|
+
* closing `</body>` and `</html>` tags.
|
|
173
|
+
*/
|
|
108
174
|
export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, endMarker) {
|
|
109
175
|
const [hoistedTagsStream, appBodyStream] = splitStreamOnFirstNonHoistedTag(innerHtml);
|
|
110
176
|
const decoder = new TextDecoder();
|
|
@@ -115,138 +181,211 @@ export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, e
|
|
|
115
181
|
let buffer = "";
|
|
116
182
|
let outerBufferRemains = "";
|
|
117
183
|
let innerSuspendedRemains = "";
|
|
118
|
-
let
|
|
184
|
+
let hoistedTagsBuffer = "";
|
|
185
|
+
let hoistedTagsReady = false;
|
|
186
|
+
let phase = "read-hoisted";
|
|
119
187
|
const pump = async (controller) => {
|
|
188
|
+
const enqueue = (text) => {
|
|
189
|
+
if (text) {
|
|
190
|
+
controller.enqueue(encoder.encode(text));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const flush = () => {
|
|
194
|
+
const flushIndex = buffer.lastIndexOf("\n");
|
|
195
|
+
if (flushIndex !== -1) {
|
|
196
|
+
enqueue(buffer.slice(0, flushIndex + 1));
|
|
197
|
+
buffer = buffer.slice(flushIndex + 1);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
120
200
|
try {
|
|
121
|
-
if (phase === "
|
|
201
|
+
if (phase === "read-hoisted") {
|
|
202
|
+
// Continuously read from the hoisted tags stream and buffer the
|
|
203
|
+
// content. Once the stream is finished, transition to the next phase.
|
|
122
204
|
const { done, value } = await hoistedTagsReader.read();
|
|
205
|
+
// When the stream is done, we're ready to process the document head.
|
|
123
206
|
if (done) {
|
|
207
|
+
hoistedTagsReady = true;
|
|
124
208
|
phase = "outer-head";
|
|
125
209
|
}
|
|
126
210
|
else {
|
|
127
|
-
|
|
211
|
+
// Otherwise, keep appending to the buffer.
|
|
212
|
+
hoistedTagsBuffer += decoder.decode(value, { stream: true });
|
|
128
213
|
}
|
|
129
214
|
}
|
|
130
215
|
else if (phase === "outer-head") {
|
|
216
|
+
// Read from the document stream. Search for the closing `</head>` tag
|
|
217
|
+
// to inject the buffered hoisted tags. Then, search for the
|
|
218
|
+
// `startMarker` to know when to start injecting the app shell. Once
|
|
219
|
+
// the marker is found, transition to the next phase.
|
|
131
220
|
const { done, value } = await outerReader.read();
|
|
221
|
+
// Handle the case where the document stream ends.
|
|
132
222
|
if (done) {
|
|
223
|
+
// If there's content left in the buffer, process it for markers.
|
|
133
224
|
if (buffer) {
|
|
225
|
+
const headCloseIndex = buffer.indexOf("</head>");
|
|
226
|
+
if (headCloseIndex !== -1 &&
|
|
227
|
+
hoistedTagsReady &&
|
|
228
|
+
hoistedTagsBuffer) {
|
|
229
|
+
enqueue(buffer.slice(0, headCloseIndex));
|
|
230
|
+
enqueue(hoistedTagsBuffer);
|
|
231
|
+
hoistedTagsBuffer = "";
|
|
232
|
+
enqueue("</head>");
|
|
233
|
+
buffer = buffer.slice(headCloseIndex + "</head>".length);
|
|
234
|
+
}
|
|
134
235
|
const markerIndex = buffer.indexOf(startMarker);
|
|
135
236
|
if (markerIndex !== -1) {
|
|
136
|
-
|
|
237
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
137
238
|
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
|
|
138
239
|
}
|
|
139
240
|
else {
|
|
140
|
-
|
|
241
|
+
enqueue(buffer);
|
|
141
242
|
}
|
|
142
243
|
buffer = "";
|
|
143
244
|
}
|
|
245
|
+
else if (hoistedTagsReady && hoistedTagsBuffer) {
|
|
246
|
+
enqueue(hoistedTagsBuffer);
|
|
247
|
+
hoistedTagsBuffer = "";
|
|
248
|
+
}
|
|
249
|
+
// Even if the stream ends, we must proceed to the app shell phase.
|
|
144
250
|
phase = "inner-shell";
|
|
145
251
|
}
|
|
146
252
|
else {
|
|
253
|
+
// As chunks arrive, append them to the buffer.
|
|
147
254
|
buffer += decoder.decode(value, { stream: true });
|
|
255
|
+
// Search for the closing head tag to inject hoisted tags.
|
|
256
|
+
const headCloseIndex = buffer.indexOf("</head>");
|
|
257
|
+
if (headCloseIndex !== -1 && hoistedTagsReady && hoistedTagsBuffer) {
|
|
258
|
+
enqueue(buffer.slice(0, headCloseIndex));
|
|
259
|
+
enqueue(hoistedTagsBuffer);
|
|
260
|
+
hoistedTagsBuffer = "";
|
|
261
|
+
enqueue("</head>");
|
|
262
|
+
buffer = buffer.slice(headCloseIndex + "</head>".length);
|
|
263
|
+
}
|
|
264
|
+
// Search for the start marker to switch to the app stream.
|
|
148
265
|
const markerIndex = buffer.indexOf(startMarker);
|
|
149
266
|
if (markerIndex !== -1) {
|
|
150
|
-
|
|
267
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
151
268
|
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
|
|
152
269
|
buffer = "";
|
|
153
270
|
phase = "inner-shell";
|
|
154
271
|
}
|
|
155
272
|
else {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
160
|
-
}
|
|
273
|
+
// If no marker is found yet, flush the buffer up to the last
|
|
274
|
+
// newline to keep the stream flowing.
|
|
275
|
+
flush();
|
|
161
276
|
}
|
|
162
277
|
}
|
|
163
278
|
}
|
|
164
279
|
else if (phase === "inner-shell") {
|
|
280
|
+
// Now read from the app stream. We send the initial part of the app
|
|
281
|
+
// content until we find the `endMarker`. This marker tells us that the
|
|
282
|
+
// non-suspended part of the app is rendered. Any content after this
|
|
283
|
+
// marker is considered suspended and is buffered. Then, transition.
|
|
165
284
|
const { done, value } = await innerReader.read();
|
|
285
|
+
// Handle the case where the app stream ends.
|
|
166
286
|
if (done) {
|
|
167
287
|
if (buffer)
|
|
168
|
-
|
|
288
|
+
enqueue(buffer);
|
|
169
289
|
phase = "outer-tail";
|
|
170
290
|
}
|
|
171
291
|
else {
|
|
292
|
+
// As chunks arrive, append them to the buffer.
|
|
172
293
|
buffer += decoder.decode(value, { stream: true });
|
|
173
294
|
const markerIndex = buffer.indexOf(endMarker);
|
|
295
|
+
// If the end marker is found, enqueue content up to the marker,
|
|
296
|
+
// buffer the rest, and switch to the document tail phase.
|
|
174
297
|
if (markerIndex !== -1) {
|
|
175
298
|
const endOfMarkerIndex = markerIndex + endMarker.length;
|
|
176
|
-
|
|
299
|
+
enqueue(buffer.slice(0, endOfMarkerIndex));
|
|
177
300
|
innerSuspendedRemains = buffer.slice(endOfMarkerIndex);
|
|
178
301
|
buffer = "";
|
|
179
302
|
phase = "outer-tail";
|
|
180
303
|
}
|
|
181
304
|
else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
185
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
186
|
-
}
|
|
305
|
+
// If no marker is found yet, flush the buffer.
|
|
306
|
+
flush();
|
|
187
307
|
}
|
|
188
308
|
}
|
|
189
309
|
}
|
|
190
310
|
else if (phase === "outer-tail") {
|
|
311
|
+
// Switch back to the document stream. The goal is to send the rest of
|
|
312
|
+
// the document's body, which critically includes the client-side
|
|
313
|
+
// `<script>` tags for hydration. We stream until we find the closing
|
|
314
|
+
// `</body>` tag and then transition.
|
|
315
|
+
// First, process any leftover buffer from the `outer-head` phase.
|
|
191
316
|
if (outerBufferRemains) {
|
|
192
317
|
buffer = outerBufferRemains;
|
|
193
318
|
outerBufferRemains = "";
|
|
194
319
|
}
|
|
195
320
|
const { done, value } = await outerReader.read();
|
|
321
|
+
// Handle the case where the document stream ends.
|
|
196
322
|
if (done) {
|
|
197
323
|
if (buffer) {
|
|
324
|
+
// Search the remaining buffer for the closing body tag.
|
|
198
325
|
const markerIndex = buffer.indexOf("</body>");
|
|
199
326
|
if (markerIndex !== -1) {
|
|
200
|
-
|
|
327
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
201
328
|
buffer = buffer.slice(markerIndex);
|
|
202
329
|
}
|
|
203
330
|
else {
|
|
204
|
-
|
|
331
|
+
enqueue(buffer);
|
|
205
332
|
buffer = "";
|
|
206
333
|
}
|
|
207
334
|
}
|
|
335
|
+
// Proceed to the suspended content phase.
|
|
208
336
|
phase = "inner-suspended";
|
|
209
337
|
}
|
|
210
338
|
else {
|
|
339
|
+
// As chunks arrive, append them to the buffer.
|
|
211
340
|
buffer += decoder.decode(value, { stream: true });
|
|
341
|
+
// Search for the closing body tag to switch to suspended content.
|
|
212
342
|
const markerIndex = buffer.indexOf("</body>");
|
|
213
343
|
if (markerIndex !== -1) {
|
|
214
|
-
|
|
344
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
215
345
|
buffer = buffer.slice(markerIndex);
|
|
216
346
|
phase = "inner-suspended";
|
|
217
347
|
}
|
|
218
348
|
else {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
222
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
223
|
-
}
|
|
349
|
+
// If no marker is found yet, flush the buffer.
|
|
350
|
+
flush();
|
|
224
351
|
}
|
|
225
352
|
}
|
|
226
353
|
}
|
|
227
354
|
else if (phase === "inner-suspended") {
|
|
355
|
+
// Switch back to the app stream. First, send any buffered suspended
|
|
356
|
+
// content from the `inner-shell` phase. Then, stream the rest of the
|
|
357
|
+
// app content until it's finished. This is all the content that was
|
|
358
|
+
// behind a `<Suspense>` boundary.
|
|
359
|
+
// First, send any buffered suspended content from the `inner-shell` phase.
|
|
228
360
|
if (innerSuspendedRemains) {
|
|
229
|
-
|
|
361
|
+
enqueue(innerSuspendedRemains);
|
|
230
362
|
innerSuspendedRemains = "";
|
|
231
363
|
}
|
|
232
364
|
const { done, value } = await innerReader.read();
|
|
365
|
+
// When the app stream is done, transition to the final phase.
|
|
233
366
|
if (done) {
|
|
234
367
|
phase = "outer-end";
|
|
235
368
|
}
|
|
236
369
|
else {
|
|
370
|
+
// Otherwise, pass through the remaining app content directly.
|
|
237
371
|
controller.enqueue(value);
|
|
238
372
|
}
|
|
239
373
|
}
|
|
240
374
|
else if (phase === "outer-end") {
|
|
375
|
+
// Finally, switch back to the document stream one last time to send
|
|
376
|
+
// the closing `</body>` and `</html>` tags and finish the response.
|
|
377
|
+
// First, send any leftover buffer from the `outer-tail` phase.
|
|
241
378
|
if (buffer) {
|
|
242
|
-
|
|
379
|
+
enqueue(buffer);
|
|
243
380
|
buffer = "";
|
|
244
381
|
}
|
|
245
382
|
const { done, value } = await outerReader.read();
|
|
383
|
+
// When the document stream is done, we're finished.
|
|
246
384
|
if (done) {
|
|
247
385
|
controller.close();
|
|
248
386
|
return;
|
|
249
387
|
}
|
|
388
|
+
// Otherwise, pass through the final document content.
|
|
250
389
|
controller.enqueue(value);
|
|
251
390
|
}
|
|
252
391
|
await pump(controller);
|
|
@@ -45,7 +45,7 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
45
45
|
const startMarker = '<div id="rwsdk-app-start" />';
|
|
46
46
|
const endMarker = '<div id="rwsdk-app-end"></div>';
|
|
47
47
|
describe("meta tag hoisting", () => {
|
|
48
|
-
it("
|
|
48
|
+
it("places hoisted tags inside head, after existing head content", async () => {
|
|
49
49
|
const outerHtml = `<!DOCTYPE html>
|
|
50
50
|
<html>
|
|
51
51
|
<head>
|
|
@@ -59,10 +59,19 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
59
59
|
const innerHtml = `<title>Page Title</title><div>App content</div>${endMarker}`;
|
|
60
60
|
const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker));
|
|
61
61
|
expect(result).toContain(`<title>Page Title</title>`);
|
|
62
|
-
expect(result.
|
|
62
|
+
expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<\/head>/);
|
|
63
63
|
expect(result).toContain(`<div>App content</div>`);
|
|
64
|
+
const doctypeIndex = result.indexOf(`<!DOCTYPE html>`);
|
|
65
|
+
const headIndex = result.indexOf(`<head>`);
|
|
66
|
+
const charsetIndex = result.indexOf(`<meta charset="utf-8" />`);
|
|
67
|
+
const titleIndex = result.indexOf(`<title>Page Title</title>`);
|
|
68
|
+
const headCloseIndex = result.indexOf(`</head>`);
|
|
69
|
+
expect(doctypeIndex).toBe(0);
|
|
70
|
+
expect(doctypeIndex).toBeLessThan(headIndex);
|
|
71
|
+
expect(charsetIndex).toBeLessThan(titleIndex);
|
|
72
|
+
expect(titleIndex).toBeLessThan(headCloseIndex);
|
|
64
73
|
});
|
|
65
|
-
it("
|
|
74
|
+
it("places multiple hoisted tags inside head, after existing head content", async () => {
|
|
66
75
|
const outerHtml = `<!DOCTYPE html>
|
|
67
76
|
<html>
|
|
68
77
|
<head>
|
|
@@ -78,9 +87,14 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
78
87
|
expect(result).toContain(`<title>Page Title</title>`);
|
|
79
88
|
expect(result).toContain(`<meta name="description" content="Test" />`);
|
|
80
89
|
expect(result).toContain(`<link rel="stylesheet" href="/styles.css" />`);
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
90
|
+
expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<meta name="description" content="Test" \/>[\s\S]*<link rel="stylesheet" href="\/styles.css" \/>[\s\S]*<\/head>/);
|
|
91
|
+
const doctypeIndex = result.indexOf(`<!DOCTYPE html>`);
|
|
92
|
+
const charsetIndex = result.indexOf(`<meta charset="utf-8" />`);
|
|
93
|
+
const titleIndex = result.indexOf(`<title>Page Title</title>`);
|
|
94
|
+
const headCloseIndex = result.indexOf(`</head>`);
|
|
95
|
+
expect(doctypeIndex).toBe(0);
|
|
96
|
+
expect(charsetIndex).toBeLessThan(titleIndex);
|
|
97
|
+
expect(titleIndex).toBeLessThan(headCloseIndex);
|
|
84
98
|
});
|
|
85
99
|
it("handles app stream with no hoisted tags", async () => {
|
|
86
100
|
const outerHtml = `<!DOCTYPE html>
|
|
@@ -117,9 +131,29 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
117
131
|
const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), createChunkedStream(innerHtmlChunks), startMarker, endMarker));
|
|
118
132
|
expect(result).toContain(`<title>Page Title</title>`);
|
|
119
133
|
expect(result).toContain(`<meta name="description" content="Test" />`);
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
134
|
+
expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<\/head>/);
|
|
135
|
+
const doctypeIndex = result.indexOf(`<!DOCTYPE html>`);
|
|
136
|
+
const charsetIndex = result.indexOf(`<meta charset="utf-8" />`);
|
|
137
|
+
const titleIndex = result.indexOf(`<title>Page Title</title>`);
|
|
138
|
+
const headCloseIndex = result.indexOf(`</head>`);
|
|
139
|
+
expect(doctypeIndex).toBe(0);
|
|
140
|
+
expect(charsetIndex).toBeLessThan(titleIndex);
|
|
141
|
+
expect(titleIndex).toBeLessThan(headCloseIndex);
|
|
142
|
+
});
|
|
143
|
+
it("ensures doctype is always first", async () => {
|
|
144
|
+
const outerHtml = `<!DOCTYPE html>
|
|
145
|
+
<html>
|
|
146
|
+
<head>
|
|
147
|
+
<meta charset="utf-8" />
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
${startMarker}
|
|
151
|
+
<script src="/client.js"></script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>`;
|
|
154
|
+
const innerHtml = `<title>Page Title</title><div>App content</div>${endMarker}`;
|
|
155
|
+
const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker));
|
|
156
|
+
expect(result.trim().startsWith(`<!DOCTYPE html>`)).toBe(true);
|
|
123
157
|
});
|
|
124
158
|
});
|
|
125
159
|
describe("basic stitching flow", () => {
|
package/dist/runtime/worker.d.ts
CHANGED
|
@@ -8,9 +8,11 @@ declare global {
|
|
|
8
8
|
DB: D1Database;
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
export
|
|
11
|
+
export type AppDefinition<Routes extends readonly Route<any>[], T extends RequestInfo> = {
|
|
12
12
|
fetch: (request: Request, env: Env, cf: ExecutionContext) => Promise<Response>;
|
|
13
|
+
__rwRoutes: Routes;
|
|
13
14
|
};
|
|
15
|
+
export declare const defineApp: <T extends RequestInfo = RequestInfo<any, DefaultAppContext>, Routes extends readonly Route<T>[] = readonly Route<T>[]>(routes: Routes) => AppDefinition<Routes, T>;
|
|
14
16
|
export declare const DefaultDocument: React.FC<{
|
|
15
17
|
children: React.ReactNode;
|
|
16
18
|
}>;
|
package/dist/runtime/worker.js
CHANGED
|
@@ -12,6 +12,7 @@ import { generateNonce } from "./lib/utils";
|
|
|
12
12
|
export * from "./requestInfo/types";
|
|
13
13
|
export const defineApp = (routes) => {
|
|
14
14
|
return {
|
|
15
|
+
__rwRoutes: routes,
|
|
15
16
|
fetch: async (request, env, cf) => {
|
|
16
17
|
globalThis.__webpack_require__ = ssrWebpackRequire;
|
|
17
18
|
const router = defineRoutes(routes);
|
|
@@ -11,4 +11,4 @@ export type SyncStateRouteOptions = {
|
|
|
11
11
|
* @param options Optional overrides for base path, reset path, and object name.
|
|
12
12
|
* @returns Router entries for the sync state API and reset endpoint.
|
|
13
13
|
*/
|
|
14
|
-
export declare const syncStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncStateServer>, options?: SyncStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition
|
|
14
|
+
export declare const syncStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncStateServer>, options?: SyncStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition<`/${string}`, import("../runtime/worker.js").RequestInfo<any, import("../runtime/worker.js").DefaultAppContext>>[];
|
package/dist/vite/constants.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
2
|
import MagicString from "magic-string";
|
|
3
3
|
import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
|
|
4
|
+
import { externalModulesSet } from "./constants.mjs";
|
|
4
5
|
import { findSsrImportCallSites } from "./findSsrSpecifiers.mjs";
|
|
5
6
|
const log = debug("rwsdk:vite:ssr-bridge-plugin");
|
|
6
7
|
export const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
|
|
@@ -184,6 +185,14 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
|
|
|
184
185
|
const normalized = site.specifier.startsWith("/@id/")
|
|
185
186
|
? site.specifier.slice("/@id/".length)
|
|
186
187
|
: site.specifier;
|
|
188
|
+
// If the import is for a known external module, we must leave it
|
|
189
|
+
// as a bare specifier. Rewriting it with any prefix (`/@id/` or
|
|
190
|
+
// our virtual one) will break Vite's default externalization.
|
|
191
|
+
if (externalModulesSet.has(normalized)) {
|
|
192
|
+
const replacement = `import("${normalized}")`;
|
|
193
|
+
s.overwrite(site.start, site.end, replacement);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
187
196
|
// context(justinvdm, 11 Aug 2025):
|
|
188
197
|
// - We replace __vite_ssr_import__ and __vite_ssr_dynamic_import__
|
|
189
198
|
// with import() calls so that the module graph can be built
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rwsdk",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.29",
|
|
4
4
|
"description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"find-up": "~8.0.0",
|
|
168
168
|
"fs-extra": "~11.3.0",
|
|
169
169
|
"get-port": "^7.1.0",
|
|
170
|
-
"glob": "~11.0
|
|
170
|
+
"glob": "~11.1.0",
|
|
171
171
|
"ignore": "~7.0.4",
|
|
172
172
|
"jsonc-parser": "~3.3.1",
|
|
173
173
|
"kysely": "~0.28.2",
|