From ae8d925666dac0008d8a607afa5f6223f95689a4 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 9 Mar 2022 18:09:48 -0600 Subject: [PATCH] Improve `Astro.slots` API (#2695) * feat: update Astro.slots API * fix: migrate Markdown to public `Astro.slots.render` API * chore: update internal AstroGlobal types * chore: add changeset * Update clean-bottles-drive.md * refactor(test): update slot tests to new syntax --- .changeset/clean-bottles-drive.md | 13 +++++ packages/astro/components/Markdown.astro | 3 +- packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/core/render/result.ts | 49 +++++++++++++++++-- .../src/components/SlottedAPI.astro | 8 +-- 5 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 .changeset/clean-bottles-drive.md diff --git a/.changeset/clean-bottles-drive.md b/.changeset/clean-bottles-drive.md new file mode 100644 index 0000000000..cb9c6af51e --- /dev/null +++ b/.changeset/clean-bottles-drive.md @@ -0,0 +1,13 @@ +--- +'astro': patch +--- + +Update `Astro.slots` API with new public `has` and `render` methods. + +This is a backwards-compatible change—`Astro.slots.default` will still be `true` if the component has been passed a `default` slot. + +```ts +if (Astro.slots.has("default")) { + const content = await Astro.slots.render("default"); +} +``` diff --git a/packages/astro/components/Markdown.astro b/packages/astro/components/Markdown.astro index fc666218d1..0dc2147325 100644 --- a/packages/astro/components/Markdown.astro +++ b/packages/astro/components/Markdown.astro @@ -28,8 +28,7 @@ const { privateRenderMarkdownDoNotUse: renderMarkdown } = Astro as any; // If no content prop provided, use the slot. if (!content) { - const { privateRenderSlotDoNotUse: renderSlot } = Astro as any; - content = await renderSlot('default'); + content = await Astro.slots.render('default'); if (content !== undefined && content !== null) { content = dedent(content); } diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index e945b8dd62..975afd1923 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -51,7 +51,7 @@ export interface AstroGlobal extends AstroGlobalPartial { params: Params; }; /** see if slots are used */ - slots: Record; + slots: Record & { has(slotName: string): boolean; render(slotName: string): Promise }; } export interface AstroGlobalPartial { diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index f846ac4083..fc53428e09 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -21,6 +21,47 @@ export interface CreateResultArgs { scripts?: Set; } +class Slots { + #cache = new Map(); + #result: SSRResult; + #slots: Record | null; + + constructor(result: SSRResult, slots: Record | null) { + this.#result = result; + this.#slots = slots; + if (slots) { + for (const key of Object.keys(slots)) { + if ((this as any)[key] !== undefined) { + throw new Error(`Unable to create a slot named "${key}". "${key}" is a reserved slot name!\nPlease update the name of this slot.`) + } + Object.defineProperty(this, key, { + get() { + return true; + }, + enumerable: true + }) + } + } + } + + public has(name: string) { + if (!this.#slots) return false; + return Boolean(this.#slots[name]); + } + + public async render(name: string) { + if (!this.#slots) return undefined; + if (this.#cache.has(name)) { + const result = this.#cache.get(name) + return result; + }; + if (!this.has(name)) return undefined; + const content = await renderSlot(this.#result, this.#slots[name]).then(res => res != null ? res.toString() : res); + this.#cache.set(name, content); + return content; + } +} + export function createResult(args: CreateResultArgs): SSRResult { const { legacyBuild, origin, markdownRender, params, pathname, renderers, resolve, site: buildOptionsSite } = args; @@ -36,6 +77,8 @@ export function createResult(args: CreateResultArgs): SSRResult { const site = new URL(origin); const url = new URL('.' + pathname, site); const canonicalURL = getCanonicalURL('.' + pathname, buildOptionsSite || origin); + const astroSlots = new Slots(result, slots); + return { __proto__: astroGlobal, props, @@ -79,11 +122,7 @@ ${extra}` return astroGlobal.resolve(path); }, - slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), - // This is used for but shouldn't be used publicly - privateRenderSlotDoNotUse(slotName: string) { - return renderSlot(result, slots ? slots[slotName] : null); - }, + slots: astroSlots, // also needs the same `astroConfig.markdownOptions.render` as `.md` pages async privateRenderMarkdownDoNotUse(content: string, opts: any) { let [mdRender, renderOpts] = markdownRender; diff --git a/packages/astro/test/fixtures/astro-slots/src/components/SlottedAPI.astro b/packages/astro/test/fixtures/astro-slots/src/components/SlottedAPI.astro index e1b00fff89..36ab317390 100644 --- a/packages/astro/test/fixtures/astro-slots/src/components/SlottedAPI.astro +++ b/packages/astro/test/fixtures/astro-slots/src/components/SlottedAPI.astro @@ -1,15 +1,15 @@ -{Astro.slots.a &&
+{Astro.slots.has("a") &&
} -{Astro.slots.b &&
+{Astro.slots.has("b") &&
} -{Astro.slots.c &&
+{Astro.slots.has("c") &&
} -{Astro.slots.default &&
+{Astro.slots.has("default") &&
}