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
This commit is contained in:
parent
bae5e46fbc
commit
adf9fccfdd
9 changed files with 115 additions and 46 deletions
5
.changeset/brave-cheetahs-float.md
Normal file
5
.changeset/brave-cheetahs-float.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Do not throw Error when users pass an object with a "type" property
|
|
@ -1,6 +1,7 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
import type { SSRResult } from '../../../@types/astro';
|
||||||
import type { RenderInstruction } from './types.js';
|
import type { RenderInstruction } from './instruction.js';
|
||||||
|
|
||||||
|
import { isRenderInstruction } from './instruction.js';
|
||||||
import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
|
import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
|
||||||
import {
|
import {
|
||||||
determineIfNeedsHydrationScript,
|
determineIfNeedsHydrationScript,
|
||||||
|
@ -52,8 +53,8 @@ function stringifyChunk(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
chunk: string | HTMLString | SlotString | RenderInstruction
|
chunk: string | HTMLString | SlotString | RenderInstruction
|
||||||
): string {
|
): string {
|
||||||
if (typeof (chunk as any).type === 'string') {
|
if (isRenderInstruction(chunk)) {
|
||||||
const instruction = chunk as RenderInstruction;
|
const instruction = chunk;
|
||||||
switch (instruction.type) {
|
switch (instruction.type) {
|
||||||
case 'directive': {
|
case 'directive': {
|
||||||
const { hydration } = instruction;
|
const { hydration } = instruction;
|
||||||
|
@ -64,8 +65,8 @@ function stringifyChunk(
|
||||||
let prescriptType: PrescriptType = needsHydrationScript
|
let prescriptType: PrescriptType = needsHydrationScript
|
||||||
? 'both'
|
? 'both'
|
||||||
: needsDirectiveScript
|
: needsDirectiveScript
|
||||||
? 'directive'
|
? 'directive'
|
||||||
: null;
|
: null;
|
||||||
if (prescriptType) {
|
if (prescriptType) {
|
||||||
let prescripts = getPrescripts(result, prescriptType, hydration.directive);
|
let prescripts = getPrescripts(result, prescriptType, hydration.directive);
|
||||||
return markHTMLString(prescripts);
|
return markHTMLString(prescripts);
|
||||||
|
@ -86,27 +87,24 @@ function stringifyChunk(
|
||||||
return renderAllHeadContent(result);
|
return renderAllHeadContent(result);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
if (chunk instanceof Response) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
|
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (chunk instanceof Response) {
|
||||||
if (isSlotString(chunk as string)) {
|
return '';
|
||||||
let out = '';
|
} else if (isSlotString(chunk as string)) {
|
||||||
const c = chunk as SlotString;
|
let out = '';
|
||||||
if (c.instructions) {
|
const c = chunk as SlotString;
|
||||||
for (const instr of c.instructions) {
|
if (c.instructions) {
|
||||||
out += stringifyChunk(result, instr);
|
for (const instr of c.instructions) {
|
||||||
}
|
out += stringifyChunk(result, instr);
|
||||||
}
|
}
|
||||||
out += chunk.toString();
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
out += chunk.toString();
|
||||||
return chunk.toString();
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return chunk.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function chunkToString(result: SSRResult, chunk: Exclude<RenderDestinationChunk, Response>) {
|
export function chunkToString(result: SSRResult, chunk: Exclude<RenderDestinationChunk, Response>) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type {
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
import type { RenderInstruction } from './types.js';
|
import { createRenderInstruction, type RenderInstruction } from './instruction.js';
|
||||||
|
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
||||||
|
@ -371,7 +371,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
destination.write(instruction);
|
destination.write(instruction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
destination.write({ type: 'directive', hydration });
|
destination.write(createRenderInstruction({ type: 'directive', hydration }));
|
||||||
destination.write(markHTMLString(renderElement('astro-island', island, false)));
|
destination.write(markHTMLString(renderElement('astro-island', island, false)));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
import type { SSRResult } from '../../../@types/astro';
|
||||||
|
|
||||||
import { markHTMLString } from '../escape.js';
|
import { markHTMLString } from '../escape.js';
|
||||||
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './types';
|
import { createRenderInstruction } from './instruction.js';
|
||||||
|
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js';
|
||||||
import { renderElement } from './util.js';
|
import { renderElement } from './util.js';
|
||||||
|
|
||||||
// Filter out duplicate elements in our set
|
// Filter out duplicate elements in our set
|
||||||
|
@ -45,7 +46,7 @@ export function renderAllHeadContent(result: SSRResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* renderHead(): Generator<RenderHeadInstruction> {
|
export function* renderHead(): Generator<RenderHeadInstruction> {
|
||||||
yield { type: 'head' };
|
yield createRenderInstruction({ type: 'head' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is called by Astro components that do not contain a <head> component
|
// This function is called by Astro components that do not contain a <head> component
|
||||||
|
@ -55,5 +56,5 @@ export function* renderHead(): Generator<RenderHeadInstruction> {
|
||||||
export function* maybeRenderHead(): Generator<MaybeRenderHeadInstruction> {
|
export function* maybeRenderHead(): Generator<MaybeRenderHeadInstruction> {
|
||||||
// This is an instruction informing the page rendering that head might need rendering.
|
// This is an instruction informing the page rendering that head might need rendering.
|
||||||
// This allows the page to deduplicate head injections.
|
// This allows the page to deduplicate head injections.
|
||||||
yield { type: 'maybe-head' };
|
yield createRenderInstruction({ type: 'maybe-head' });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
|
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
|
||||||
|
export type { RenderInstruction } from './instruction';
|
||||||
export { createHeadAndContent, renderTemplate, renderToString } from './astro/index.js';
|
export { createHeadAndContent, renderTemplate, renderToString } from './astro/index.js';
|
||||||
export { Fragment, Renderer, chunkToByteArray, chunkToString } from './common.js';
|
export { Fragment, Renderer, chunkToByteArray, chunkToString } from './common.js';
|
||||||
export { renderComponent, renderComponentToString } from './component.js';
|
export { renderComponent, renderComponentToString } from './component.js';
|
||||||
|
@ -7,5 +8,4 @@ export { maybeRenderHead, renderHead } from './head.js';
|
||||||
export { renderPage } from './page.js';
|
export { renderPage } from './page.js';
|
||||||
export { renderSlot, renderSlotToString, type ComponentSlots } from './slot.js';
|
export { renderSlot, renderSlotToString, type ComponentSlots } from './slot.js';
|
||||||
export { renderScriptElement, renderUniqueStylesheet } from './tags.js';
|
export { renderScriptElement, renderUniqueStylesheet } from './tags.js';
|
||||||
export type { RenderInstruction } from './types';
|
|
||||||
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
|
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
|
||||||
|
|
32
packages/astro/src/runtime/server/render/instruction.ts
Normal file
32
packages/astro/src/runtime/server/render/instruction.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import type { HydrationMetadata } from '../hydration.js';
|
||||||
|
|
||||||
|
const RenderInstructionSymbol = Symbol.for('astro:render');
|
||||||
|
|
||||||
|
export type RenderDirectiveInstruction = {
|
||||||
|
type: 'directive';
|
||||||
|
hydration: HydrationMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderHeadInstruction = {
|
||||||
|
type: 'head';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaybeRenderHeadInstruction = {
|
||||||
|
type: 'maybe-head';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderInstruction =
|
||||||
|
| RenderDirectiveInstruction
|
||||||
|
| RenderHeadInstruction
|
||||||
|
| MaybeRenderHeadInstruction;
|
||||||
|
|
||||||
|
export function createRenderInstruction(instruction: RenderDirectiveInstruction): RenderDirectiveInstruction;
|
||||||
|
export function createRenderInstruction(instruction: RenderHeadInstruction): RenderHeadInstruction;
|
||||||
|
export function createRenderInstruction(instruction: MaybeRenderHeadInstruction): MaybeRenderHeadInstruction;
|
||||||
|
export function createRenderInstruction(instruction: { type: string }): RenderInstruction {
|
||||||
|
return Object.defineProperty(instruction as RenderInstruction, RenderInstructionSymbol, { value: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRenderInstruction(chunk: any): chunk is RenderInstruction {
|
||||||
|
return chunk && typeof chunk === 'object' && chunk[RenderInstructionSymbol];
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import type { SSRResult } from '../../../@types/astro.js';
|
import type { SSRResult } from '../../../@types/astro.js';
|
||||||
import type { renderTemplate } from './astro/render-template.js';
|
import type { renderTemplate } from './astro/render-template.js';
|
||||||
import type { RenderInstruction } from './types.js';
|
import type { RenderInstruction } from './instruction.js';
|
||||||
|
|
||||||
import { HTMLString, markHTMLString } from '../escape.js';
|
import { HTMLString, markHTMLString } from '../escape.js';
|
||||||
import { renderChild } from './any.js';
|
import { renderChild } from './any.js';
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import type { HydrationMetadata } from '../hydration.js';
|
|
||||||
|
|
||||||
export type RenderDirectiveInstruction = {
|
|
||||||
type: 'directive';
|
|
||||||
hydration: HydrationMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RenderHeadInstruction = {
|
|
||||||
type: 'head';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MaybeRenderHeadInstruction = {
|
|
||||||
type: 'maybe-head';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RenderInstruction =
|
|
||||||
| RenderDirectiveInstruction
|
|
||||||
| RenderHeadInstruction
|
|
||||||
| MaybeRenderHeadInstruction;
|
|
52
packages/astro/test/units/render/chunk.test.js
Normal file
52
packages/astro/test/units/render/chunk.test.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { createFs, createRequestAndResponse, runInContainer } from '../test-utils.js';
|
||||||
|
|
||||||
|
const root = new URL('../../fixtures/alias/', import.meta.url);
|
||||||
|
|
||||||
|
describe('core/render chunk', () => {
|
||||||
|
it('does not throw on user object with type', async () => {
|
||||||
|
const fs = createFs(
|
||||||
|
{
|
||||||
|
'/src/pages/index.astro': `
|
||||||
|
---
|
||||||
|
const value = { type: 'foobar' }
|
||||||
|
---
|
||||||
|
<div id="chunk">{value}</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
root
|
||||||
|
);
|
||||||
|
|
||||||
|
await runInContainer(
|
||||||
|
{
|
||||||
|
fs,
|
||||||
|
inlineConfig: {
|
||||||
|
root: fileURLToPath(root),
|
||||||
|
logLevel: 'silent',
|
||||||
|
integrations: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (container) => {
|
||||||
|
const { req, res, done, text } = createRequestAndResponse({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
container.handle(req, res);
|
||||||
|
|
||||||
|
await done;
|
||||||
|
try {
|
||||||
|
const html = await text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const target = $('#chunk');
|
||||||
|
|
||||||
|
expect(target).not.to.be.undefined;
|
||||||
|
expect(target.text()).to.equal('[object Object]');
|
||||||
|
} catch (e) {
|
||||||
|
expect(false).to.be.ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue