Skip to content

Commit b12c847

Browse files
authored
Stringify shouldn't throw on user object during rendering (#8127)
* fix(#7923): do not throw on user { type } object * chore: remove unused type export * chore: guess it wasn't unused
1 parent c7de971 commit b12c847

9 files changed

Lines changed: 115 additions & 46 deletions

File tree

.changeset/brave-cheetahs-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Do not throw Error when users pass an object with a "type" property

packages/astro/src/runtime/server/render/common.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SSRResult } from '../../../@types/astro';
2-
import type { RenderInstruction } from './types.js';
2+
import type { RenderInstruction } from './instruction.js';
33

4+
import { isRenderInstruction } from './instruction.js';
45
import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
56
import {
67
determineIfNeedsHydrationScript,
@@ -52,8 +53,8 @@ function stringifyChunk(
5253
result: SSRResult,
5354
chunk: string | HTMLString | SlotString | RenderInstruction
5455
): string {
55-
if (typeof (chunk as any).type === 'string') {
56-
const instruction = chunk as RenderInstruction;
56+
if (isRenderInstruction(chunk)) {
57+
const instruction = chunk;
5758
switch (instruction.type) {
5859
case 'directive': {
5960
const { hydration } = instruction;
@@ -64,8 +65,8 @@ function stringifyChunk(
6465
let prescriptType: PrescriptType = needsHydrationScript
6566
? 'both'
6667
: needsDirectiveScript
67-
? 'directive'
68-
: null;
68+
? 'directive'
69+
: null;
6970
if (prescriptType) {
7071
let prescripts = getPrescripts(result, prescriptType, hydration.directive);
7172
return markHTMLString(prescripts);
@@ -86,27 +87,24 @@ function stringifyChunk(
8687
return renderAllHeadContent(result);
8788
}
8889
default: {
89-
if (chunk instanceof Response) {
90-
return '';
91-
}
9290
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
9391
}
9492
}
95-
} else {
96-
if (isSlotString(chunk as string)) {
97-
let out = '';
98-
const c = chunk as SlotString;
99-
if (c.instructions) {
100-
for (const instr of c.instructions) {
101-
out += stringifyChunk(result, instr);
102-
}
93+
} else if (chunk instanceof Response) {
94+
return '';
95+
} else if (isSlotString(chunk as string)) {
96+
let out = '';
97+
const c = chunk as SlotString;
98+
if (c.instructions) {
99+
for (const instr of c.instructions) {
100+
out += stringifyChunk(result, instr);
103101
}
104-
out += chunk.toString();
105-
return out;
106102
}
107-
108-
return chunk.toString();
103+
out += chunk.toString();
104+
return out;
109105
}
106+
107+
return chunk.toString();
110108
}
111109

112110
export function chunkToString(result: SSRResult, chunk: Exclude<RenderDestinationChunk, Response>) {

packages/astro/src/runtime/server/render/component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
SSRLoadedRenderer,
55
SSRResult,
66
} from '../../../@types/astro';
7-
import type { RenderInstruction } from './types.js';
7+
import { createRenderInstruction, type RenderInstruction } from './instruction.js';
88

99
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
1010
import { HTMLBytes, markHTMLString } from '../escape.js';
@@ -370,7 +370,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
370370
destination.write(instruction);
371371
}
372372
}
373-
destination.write({ type: 'directive', hydration });
373+
destination.write(createRenderInstruction({ type: 'directive', hydration }));
374374
destination.write(markHTMLString(renderElement('astro-island', island, false)));
375375
},
376376
};

packages/astro/src/runtime/server/render/head.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { SSRResult } from '../../../@types/astro';
22

33
import { markHTMLString } from '../escape.js';
4-
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './types';
4+
import { createRenderInstruction } from './instruction.js';
5+
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js';
56
import { renderElement } from './util.js';
67

78
// Filter out duplicate elements in our set
@@ -45,7 +46,7 @@ export function renderAllHeadContent(result: SSRResult) {
4546
}
4647

4748
export function* renderHead(): Generator<RenderHeadInstruction> {
48-
yield { type: 'head' };
49+
yield createRenderInstruction({ type: 'head' });
4950
}
5051

5152
// This function is called by Astro components that do not contain a <head> component
@@ -55,5 +56,5 @@ export function* renderHead(): Generator<RenderHeadInstruction> {
5556
export function* maybeRenderHead(): Generator<MaybeRenderHeadInstruction> {
5657
// This is an instruction informing the page rendering that head might need rendering.
5758
// This allows the page to deduplicate head injections.
58-
yield { type: 'maybe-head' };
59+
yield createRenderInstruction({ type: 'maybe-head' });
5960
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
2+
export type { RenderInstruction } from './instruction';
23
export { createHeadAndContent, renderTemplate, renderToString } from './astro/index.js';
34
export { Fragment, Renderer, chunkToByteArray, chunkToString } from './common.js';
45
export { renderComponent, renderComponentToString } from './component.js';
@@ -7,5 +8,4 @@ export { maybeRenderHead, renderHead } from './head.js';
78
export { renderPage } from './page.js';
89
export { renderSlot, renderSlotToString, type ComponentSlots } from './slot.js';
910
export { renderScriptElement, renderUniqueStylesheet } from './tags.js';
10-
export type { RenderInstruction } from './types';
1111
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { HydrationMetadata } from '../hydration.js';
2+
3+
const RenderInstructionSymbol = Symbol.for('astro:render');
4+
5+
export type RenderDirectiveInstruction = {
6+
type: 'directive';
7+
hydration: HydrationMetadata;
8+
};
9+
10+
export type RenderHeadInstruction = {
11+
type: 'head';
12+
};
13+
14+
export type MaybeRenderHeadInstruction = {
15+
type: 'maybe-head';
16+
};
17+
18+
export type RenderInstruction =
19+
| RenderDirectiveInstruction
20+
| RenderHeadInstruction
21+
| MaybeRenderHeadInstruction;
22+
23+
export function createRenderInstruction(instruction: RenderDirectiveInstruction): RenderDirectiveInstruction;
24+
export function createRenderInstruction(instruction: RenderHeadInstruction): RenderHeadInstruction;
25+
export function createRenderInstruction(instruction: MaybeRenderHeadInstruction): MaybeRenderHeadInstruction;
26+
export function createRenderInstruction(instruction: { type: string }): RenderInstruction {
27+
return Object.defineProperty(instruction as RenderInstruction, RenderInstructionSymbol, { value: true });
28+
}
29+
30+
export function isRenderInstruction(chunk: any): chunk is RenderInstruction {
31+
return chunk && typeof chunk === 'object' && chunk[RenderInstructionSymbol];
32+
}

packages/astro/src/runtime/server/render/slot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SSRResult } from '../../../@types/astro.js';
22
import type { renderTemplate } from './astro/render-template.js';
3-
import type { RenderInstruction } from './types.js';
3+
import type { RenderInstruction } from './instruction.js';
44

55
import { HTMLString, markHTMLString } from '../escape.js';
66
import { renderChild } from './any.js';

packages/astro/src/runtime/server/render/types.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect } from 'chai';
2+
import * as cheerio from 'cheerio';
3+
import { fileURLToPath } from 'node:url';
4+
import { createFs, createRequestAndResponse, runInContainer } from '../test-utils.js';
5+
6+
const root = new URL('../../fixtures/alias/', import.meta.url);
7+
8+
describe('core/render chunk', () => {
9+
it('does not throw on user object with type', async () => {
10+
const fs = createFs(
11+
{
12+
'/src/pages/index.astro': `
13+
---
14+
const value = { type: 'foobar' }
15+
---
16+
<div id="chunk">{value}</div>
17+
`,
18+
},
19+
root
20+
);
21+
22+
await runInContainer(
23+
{
24+
fs,
25+
inlineConfig: {
26+
root: fileURLToPath(root),
27+
logLevel: 'silent',
28+
integrations: [],
29+
},
30+
},
31+
async (container) => {
32+
const { req, res, done, text } = createRequestAndResponse({
33+
method: 'GET',
34+
url: '/',
35+
});
36+
container.handle(req, res);
37+
38+
await done;
39+
try {
40+
const html = await text();
41+
const $ = cheerio.load(html);
42+
const target = $('#chunk');
43+
44+
expect(target).not.to.be.undefined;
45+
expect(target.text()).to.equal('[object Object]');
46+
} catch (e) {
47+
expect(false).to.be.ok;
48+
}
49+
}
50+
);
51+
});
52+
});

0 commit comments

Comments
 (0)