diff --git a/.changeset/hungry-lobsters-happen.md b/.changeset/hungry-lobsters-happen.md new file mode 100644 index 000000000..00ef92584 --- /dev/null +++ b/.changeset/hungry-lobsters-happen.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Add types for `import.meta.env.ASSETS_PREFIX` and `import.meta.env.SITE` diff --git a/.changeset/odd-geese-shop.md b/.changeset/odd-geese-shop.md new file mode 100644 index 000000000..2f238d726 --- /dev/null +++ b/.changeset/odd-geese-shop.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +value of var can be undefined when using `define:vars` diff --git a/examples/with-markdoc/src/content/config.ts b/examples/with-markdoc/src/content/config.ts deleted file mode 100644 index 2eccab0a3..000000000 --- a/examples/with-markdoc/src/content/config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineCollection, z } from 'astro:content'; - -const docs = defineCollection({ - schema: z.object({ - title: z.string(), - }), -}); - -export const collections = { docs }; diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 7c16de997..ced9a16df 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,13 @@ # astro +## 2.5.3 + +### Patch Changes + +- [#6758](https://github.com/withastro/astro/pull/6758) [`f558a9e20`](https://github.com/withastro/astro/commit/f558a9e2056fc8f2e2d5814e74f199e398159fc4) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improve style and script handling across content collection files. This addresses style bleed present in `@astrojs/markdoc` v0.1.0 + +- [#7143](https://github.com/withastro/astro/pull/7143) [`b41963b77`](https://github.com/withastro/astro/commit/b41963b775149b802eea9e12c5fe266bb9a02944) Thanks [@johannesspohr](https://github.com/johannesspohr)! - Render 404 page content when a `Response` with status 404 is returned from a page + ## 2.5.2 ### Patch Changes diff --git a/packages/astro/client-base.d.ts b/packages/astro/client-base.d.ts index 5186468c8..37bae7b1c 100644 --- a/packages/astro/client-base.d.ts +++ b/packages/astro/client-base.d.ts @@ -1,5 +1,32 @@ /// +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace App { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Locals {} +} + +interface ImportMetaEnv { + /** + * The prefix for Astro-generated asset links if the build.assetsPrefix config option is set. This can be used to create asset links not handled by Astro. + */ + readonly ASSETS_PREFIX: string; + /** + * This is set to the site option specified in your project’s Astro config file. + */ + readonly SITE: string; +} + +interface ImportMeta { + /** + * Astro and Vite expose environment variables through `import.meta.env`. For a complete list of the environment variables available, see the two references below. + * + * - [Astro reference](https://docs.astro.build/en/guides/environment-variables/#default-environment-variables) + * - [Vite reference](https://vitejs.dev/guide/env-and-mode.html#env-variables) + */ + readonly env: ImportMetaEnv; +} + declare module 'astro:assets' { // Exporting things one by one is a bit cumbersome, not sure if there's a better way - erika, 2023-02-03 type AstroAssets = { @@ -387,9 +414,3 @@ declare module '*?inline' { const src: string; export default src; } - -// eslint-disable-next-line @typescript-eslint/no-namespace -declare namespace App { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Locals {} -} diff --git a/packages/astro/client-image.d.ts b/packages/astro/client-image.d.ts index 5148014a4..ffcc1c63c 100644 --- a/packages/astro/client-image.d.ts +++ b/packages/astro/client-image.d.ts @@ -2,7 +2,7 @@ // TODO: Merge this file with `client-base.d.ts` in 3.0, when the `astro:assets` feature isn't under a flag anymore. -type InputFormat = import('./dist/assets/types.js').InputFormat; +type InputFormat = import('./dist/assets/types.js').ImageInputFormat; interface ImageMetadata { src: string; diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index 44da8654b..96f59d586 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -21,10 +21,6 @@ declare module '*.svg' { const src: string; export default src; } -declare module '*.ico' { - const src: string; - export default src; -} declare module '*.webp' { const src: string; export default src; diff --git a/packages/astro/import-meta.d.ts b/packages/astro/import-meta.d.ts index 2b05d0a65..23d951cf2 100644 --- a/packages/astro/import-meta.d.ts +++ b/packages/astro/import-meta.d.ts @@ -1,4 +1,4 @@ -// File vendored from Vite itself, as a workaround to https://github.com/vitejs/vite/pull/9827 until Vite 4 comes out +// File vendored from Vite itself, as a workaround to https://github.com/vitejs/vite/issues/13309 until Vite 5 comes out // This file is an augmentation to the built-in ImportMeta interface // Thus cannot contain any top-level imports @@ -6,13 +6,6 @@ /* eslint-disable @typescript-eslint/consistent-type-imports */ -// Duplicate of import('../src/node/importGlob').GlobOptions in order to -// avoid breaking the production client type. Because this file is referenced -// in vite/client.d.ts and in production src/node/importGlob.ts doesn't exist. -interface GlobOptions { - as?: string; -} - interface ImportMeta { url: string; diff --git a/packages/astro/package.json b/packages/astro/package.json index a29ee4eb8..5094ec543 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "2.5.2", + "version": "2.5.3", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 9d09d4260..cf9ced260 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1280,6 +1280,12 @@ export interface ContentEntryType { } ): rollup.LoadResult | Promise; contentModuleTypes?: string; + /** + * Handle asset propagation for rendered content to avoid bleed. + * Ex. MDX content can import styles and scripts, so `handlePropagation` should be true. + * @default true + */ + handlePropagation?: boolean; } type GetContentEntryInfoReturnType = { diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts index bda154692..9bfb2e865 100644 --- a/packages/astro/src/content/consts.ts +++ b/packages/astro/src/content/consts.ts @@ -1,11 +1,18 @@ export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets'; +export const CONTENT_RENDER_FLAG = 'astroRenderContent'; export const CONTENT_FLAG = 'astroContentCollectionEntry'; export const DATA_FLAG = 'astroDataCollectionEntry'; -export const CONTENT_FLAGS = [CONTENT_FLAG, DATA_FLAG, PROPAGATED_ASSET_FLAG] as const; export const VIRTUAL_MODULE_ID = 'astro:content'; export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@'; export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@'; export const SCRIPTS_PLACEHOLDER = '@@ASTRO-SCRIPTS@@'; +export const CONTENT_FLAGS = [ + CONTENT_FLAG, + CONTENT_RENDER_FLAG, + DATA_FLAG, + PROPAGATED_ASSET_FLAG, +] as const; + export const CONTENT_TYPES_FILE = 'types.d.ts'; diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index f38d9192a..ece6f7cb1 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -270,62 +270,82 @@ async function render({ const baseMod = await renderEntryImport(); if (baseMod == null || typeof baseMod !== 'object') throw UnexpectedRenderError; - const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod; - if (typeof getMod !== 'function') throw UnexpectedRenderError; - const mod = await getMod(); - if (mod == null || typeof mod !== 'object') throw UnexpectedRenderError; + if ( + baseMod.default != null && + typeof baseMod.default === 'object' && + baseMod.default.__astroPropagation === true + ) { + const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod.default; + if (typeof getMod !== 'function') throw UnexpectedRenderError; + const propagationMod = await getMod(); + if (propagationMod == null || typeof propagationMod !== 'object') throw UnexpectedRenderError; - const Content = createComponent({ - factory(result, baseProps, slots) { - let styles = '', - links = '', - scripts = ''; - if (Array.isArray(collectedStyles)) { - styles = collectedStyles - .map((style: any) => { - return renderUniqueStylesheet(result, { - type: 'inline', - content: style, - }); - }) - .join(''); - } - if (Array.isArray(collectedLinks)) { - links = collectedLinks - .map((link: any) => { - return renderUniqueStylesheet(result, { - type: 'external', - src: prependForwardSlash(link), - }); - }) - .join(''); - } - if (Array.isArray(collectedScripts)) { - scripts = collectedScripts.map((script: any) => renderScriptElement(script)).join(''); - } + const Content = createComponent({ + factory(result, baseProps, slots) { + let styles = '', + links = '', + scripts = ''; + if (Array.isArray(collectedStyles)) { + styles = collectedStyles + .map((style: any) => { + return renderUniqueStylesheet(result, { + type: 'inline', + content: style, + }); + }) + .join(''); + } + if (Array.isArray(collectedLinks)) { + links = collectedLinks + .map((link: any) => { + return renderUniqueStylesheet(result, { + type: 'external', + src: prependForwardSlash(link), + }); + }) + .join(''); + } + if (Array.isArray(collectedScripts)) { + scripts = collectedScripts.map((script: any) => renderScriptElement(script)).join(''); + } - let props = baseProps; - // Auto-apply MDX components export - if (id.endsWith('mdx')) { - props = { - components: mod.components ?? {}, - ...baseProps, - }; - } + let props = baseProps; + // Auto-apply MDX components export + if (id.endsWith('mdx')) { + props = { + components: propagationMod.components ?? {}, + ...baseProps, + }; + } - return createHeadAndContent( - unescapeHTML(styles + links + scripts) as any, - renderTemplate`${renderComponent(result, 'Content', mod.Content, props, slots)}` - ); - }, - propagation: 'self', - }); + return createHeadAndContent( + unescapeHTML(styles + links + scripts) as any, + renderTemplate`${renderComponent( + result, + 'Content', + propagationMod.Content, + props, + slots + )}` + ); + }, + propagation: 'self', + }); - return { - Content, - headings: mod.getHeadings?.() ?? [], - remarkPluginFrontmatter: mod.frontmatter ?? {}, - }; + return { + Content, + headings: propagationMod.getHeadings?.() ?? [], + remarkPluginFrontmatter: propagationMod.frontmatter ?? {}, + }; + } else if (baseMod.Content && typeof baseMod.Content === 'function') { + return { + Content: baseMod.Content, + headings: baseMod.getHeadings?.() ?? [], + remarkPluginFrontmatter: baseMod.frontmatter ?? {}, + }; + } else { + throw UnexpectedRenderError; + } } export function createReference({ lookupMap }: { lookupMap: ContentLookupMap }) { diff --git a/packages/astro/src/content/template/virtual-mod.mjs b/packages/astro/src/content/template/virtual-mod.mjs index 491e594a6..e0ac7a564 100644 --- a/packages/astro/src/content/template/virtual-mod.mjs +++ b/packages/astro/src/content/template/virtual-mod.mjs @@ -46,7 +46,7 @@ function createGlobLookup(glob) { } const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', { - query: { astroPropagatedAssets: true }, + query: { astroRenderContent: true }, }); const collectionToRenderEntryMap = createCollectionToGlobResultMap({ globResult: renderEntryGlob, diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 831b25fb4..82a0f7992 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -14,6 +14,7 @@ import type { } from '../@types/astro.js'; import { VALID_INPUT_FORMATS } from '../assets/consts.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; + import { formatYAMLException, isYAMLException } from '../core/errors/utils.js'; import { CONTENT_FLAGS, CONTENT_TYPES_FILE } from './consts.js'; import { errorMap } from './error-map.js'; @@ -328,7 +329,7 @@ export function parseFrontmatter(fileContents: string, filePath: string) { */ export const globalContentConfigObserver = contentObservable({ status: 'init' }); -export function hasContentFlag(viteId: string, flag: (typeof CONTENT_FLAGS)[number]) { +export function hasContentFlag(viteId: string, flag: (typeof CONTENT_FLAGS)[number]): boolean { const flags = new URLSearchParams(viteId.split('?')[1] ?? ''); return flags.has(flag); } diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index 7e73f9f6b..bbd9749a8 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -1,4 +1,5 @@ -import { pathToFileURL } from 'url'; +import { extname } from 'node:path'; +import { pathToFileURL } from 'node:url'; import type { Plugin } from 'vite'; import type { AstroSettings } from '../@types/astro.js'; import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js'; @@ -11,16 +12,13 @@ import { joinPaths, prependForwardSlash } from '../core/path.js'; import { getStylesForURL } from '../core/render/dev/css.js'; import { getScriptsForURL } from '../core/render/dev/scripts.js'; import { + CONTENT_RENDER_FLAG, LINKS_PLACEHOLDER, PROPAGATED_ASSET_FLAG, SCRIPTS_PLACEHOLDER, STYLES_PLACEHOLDER, } from './consts.js'; - -function isPropagatedAsset(viteId: string) { - const flags = new URLSearchParams(viteId.split('?')[1]); - return flags.has(PROPAGATED_ASSET_FLAG); -} +import { hasContentFlag } from './utils.js'; export function astroContentAssetPropagationPlugin({ mode, @@ -32,13 +30,31 @@ export function astroContentAssetPropagationPlugin({ let devModuleLoader: ModuleLoader; return { name: 'astro:content-asset-propagation', + enforce: 'pre', + async resolveId(id, importer, opts) { + if (hasContentFlag(id, CONTENT_RENDER_FLAG)) { + const base = id.split('?')[0]; + + for (const { extensions, handlePropagation = true } of settings.contentEntryTypes) { + if (handlePropagation && extensions.includes(extname(base))) { + return this.resolve(`${base}?${PROPAGATED_ASSET_FLAG}`, importer, { + skipSelf: true, + ...opts, + }); + } + } + // Resolve to the base id (no content flags) + // if Astro doesn't need to handle propagation. + return this.resolve(base, importer, { skipSelf: true, ...opts }); + } + }, configureServer(server) { if (mode === 'dev') { devModuleLoader = createViteLoader(server); } }, async transform(_, id, options) { - if (isPropagatedAsset(id)) { + if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) { const basePath = id.split('?')[0]; let stringifiedLinks: string, stringifiedStyles: string, stringifiedScripts: string; @@ -73,14 +89,17 @@ export function astroContentAssetPropagationPlugin({ } const code = ` - export async function getMod() { + async function getMod() { return import(${JSON.stringify(basePath)}); } - export const collectedLinks = ${stringifiedLinks}; - export const collectedStyles = ${stringifiedStyles}; - export const collectedScripts = ${stringifiedScripts}; + const collectedLinks = ${stringifiedLinks}; + const collectedStyles = ${stringifiedStyles}; + const collectedScripts = ${stringifiedScripts}; + const defaultMod = { __astroPropagation: true, getMod, collectedLinks, collectedStyles, collectedScripts }; + export default defaultMod; `; - + // ^ Use a default export for tools like Markdoc + // to catch the `__astroPropagation` identifier return { code, map: { mappings: '' } }; } }, diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 078d43e50..1e2dd1d24 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -144,19 +144,19 @@ export class App { if (routeData.type === 'page') { let response = await this.#renderPage(request, routeData, mod, defaultStatus); - // If there was a 500 error, try sending the 500 page. - if (response.status === 500) { - const fiveHundredRouteData = matchRoute('/500', this.#manifestData); - if (fiveHundredRouteData) { - mod = await this.#manifest.pageMap.get(fiveHundredRouteData.component)!(); + // If there was a known error code, try sending the according page (e.g. 404.astro / 500.astro). + if (response.status === 500 || response.status === 404) { + const errorPageData = matchRoute('/' + response.status, this.#manifestData); + if (errorPageData && errorPageData.route !== routeData.route) { + mod = await this.#manifest.pageMap.get(errorPageData.component)!(); try { - let fiveHundredResponse = await this.#renderPage( + let errorResponse = await this.#renderPage( request, - fiveHundredRouteData, + errorPageData, mod, - 500 + response.status ); - return fiveHundredResponse; + return errorResponse; } catch {} } } diff --git a/packages/astro/src/core/render/dev/vite.ts b/packages/astro/src/core/render/dev/vite.ts index 724ad172f..bb8209ce8 100644 --- a/packages/astro/src/core/render/dev/vite.ts +++ b/packages/astro/src/core/render/dev/vite.ts @@ -1,7 +1,6 @@ import type { ModuleLoader, ModuleNode } from '../../module-loader/index'; import npath from 'path'; -import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js'; import { unwrapId } from '../../util.js'; import { isCSSRequest } from './util.js'; @@ -10,9 +9,10 @@ import { isCSSRequest } from './util.js'; * List of file extensions signalling we can (and should) SSR ahead-of-time * See usage below */ -const fileExtensionsToSSR = new Set(['.astro', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS]); +const fileExtensionsToSSR = new Set(['.astro', '.mdoc', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS]); const STRIP_QUERY_PARAMS_REGEX = /\?.*$/; +const ASTRO_PROPAGATED_ASSET_REGEX = /\?astroPropagatedAssets/; /** recursively crawl the module graph to get all style files imported by parent id */ export async function* crawlGraph( @@ -23,7 +23,6 @@ export async function* crawlGraph( ): AsyncGenerator { const id = unwrapId(_id); const importedModules = new Set(); - if (new URL(id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) return; const moduleEntriesForId = isRootFile ? // "getModulesByFile" pulls from a delayed module cache (fun implementation detail), @@ -44,6 +43,7 @@ export async function* crawlGraph( if (id === entry.id) { scanned.add(id); const entryIsStyle = isCSSRequest(id); + for (const importedModule of entry.importedModules) { // some dynamically imported modules are *not* server rendered in time // to only SSR modules that we can safely transform, we check against @@ -60,15 +60,13 @@ export async function* crawlGraph( if (entryIsStyle && !isCSSRequest(importedModulePathname)) { continue; } + const isFileTypeNeedingSSR = fileExtensionsToSSR.has( + npath.extname(importedModulePathname) + ); if ( - fileExtensionsToSSR.has( - npath.extname( - // Use `id` instead of `pathname` to preserve query params. - // Should not SSR a module with an unexpected query param, - // like "?astroPropagatedAssets" - importedModule.id - ) - ) + isFileTypeNeedingSSR && + // Should not SSR a module with ?astroPropagatedAssets + !ASTRO_PROPAGATED_ASSET_REGEX.test(importedModule.id) ) { const mod = loader.getModuleById(importedModule.id); if (!mod?.ssrModule) { diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 5ddd97716..c2599401d 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -43,7 +43,7 @@ export function defineScriptVars(vars: Record) { for (const [key, value] of Object.entries(vars)) { // Use const instead of let as let global unsupported with Safari // https://stackoverflow.com/questions/29194024/cant-use-let-keyword-in-safari-javascript - output += `const ${toIdent(key)} = ${JSON.stringify(value).replace( + output += `const ${toIdent(key)} = ${JSON.stringify(value)?.replace( /<\/script>/g, '\\x3C/script>' )};\n`; diff --git a/packages/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts index fc19893e8..4f44aaf6e 100644 --- a/packages/astro/src/vite-plugin-head/index.ts +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -9,7 +9,8 @@ import { getTopLevelPages, walkParentInfos } from '../core/build/graph.js'; import type { BuildInternals } from '../core/build/internal.js'; import { getAstroMetadata } from '../vite-plugin-astro/index.js'; -const injectExp = /^\/\/\s*astro-head-inject/; +// Detect this in comments, both in .astro components and in js/ts files. +const injectExp = /(^\/\/|\/\/!)\s*astro-head-inject/; export default function configHeadVitePlugin({ settings, @@ -32,6 +33,7 @@ export default function configHeadVitePlugin({ seen.add(id); const mod = server.moduleGraph.getModuleById(id); const info = this.getModuleInfo(id); + if (info?.meta.astro) { const astroMetadata = getAstroMetadata(info); if (astroMetadata) { diff --git a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts index cbf5cc957..a3489c940 100644 --- a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts +++ b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts @@ -13,6 +13,8 @@ export const markdownContentEntryType: ContentEntryType = { rawData: parsed.matter, }; }, + // We need to handle propagation for Markdown because they support layouts which will bring in styles. + handlePropagation: true, }; /** @@ -30,6 +32,9 @@ export const mdxContentEntryType: ContentEntryType = { rawData: parsed.matter, }; }, + // MDX can import scripts and styles, + // so wrap all MDX files with script / style propagation checks + handlePropagation: true, contentModuleTypes: `declare module 'astro:content' { interface Render { '.mdx': Promise<{ diff --git a/packages/astro/test/astro-directives.test.js b/packages/astro/test/astro-directives.test.js index c4098b4fe..9afbbbe97 100644 --- a/packages/astro/test/astro-directives.test.js +++ b/packages/astro/test/astro-directives.test.js @@ -14,7 +14,7 @@ describe('Directives', async () => { const html = await fixture.readFile('/define-vars/index.html'); const $ = cheerio.load(html); - expect($('script')).to.have.lengthOf(4); + expect($('script')).to.have.lengthOf(5); let i = 0; for (const script of $('script').toArray()) { @@ -27,9 +27,12 @@ describe('Directives', async () => { } else if (i < 3) { // Convert invalid keys to valid identifiers expect($(script).toString()).to.include('const dashCase = "bar"'); - } else { + } else if (i < 4) { // Closing script tags in strings are escaped expect($(script).toString()).to.include('const bar = "' +let undef: undefined; --- @@ -32,6 +33,9 @@ let bar = '' + </body> diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-404.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-404.astro new file mode 100644 index 000000000..9331037be --- /dev/null +++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-404.astro @@ -0,0 +1,6 @@ +--- +return new Response(null, { + status: 404, + statusText: 'Not found', +}); +--- diff --git a/packages/astro/test/ssr-404-500-pages.test.js b/packages/astro/test/ssr-404-500-pages.test.js index 2216aa762..10e311ef9 100644 --- a/packages/astro/test/ssr-404-500-pages.test.js +++ b/packages/astro/test/ssr-404-500-pages.test.js @@ -62,6 +62,16 @@ describe('404 and 500 pages', () => { expect($('h1').text()).to.equal('Something went horribly wrong!'); }); + it('404 page returned when there is an 404 response returned from route', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/causes-404'); + const response = await app.render(request); + expect(response.status).to.equal(404); + const html = await response.text(); + const $ = cheerio.load(html); + expect($('h1').text()).to.equal('Something went horribly wrong!'); + }); + it('500 page returned when there is an error', async () => { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com/causes-error'); diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 29b58521b..f37dd13d0 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -42,7 +42,7 @@ "tiny-glob": "^0.2.9" }, "peerDependencies": { - "astro": "workspace:^2.5.2" + "astro": "workspace:^2.5.3" }, "devDependencies": { "astro": "workspace:*", diff --git a/packages/integrations/deno/package.json b/packages/integrations/deno/package.json index 6d4aafd49..6b6fbffbb 100644 --- a/packages/integrations/deno/package.json +++ b/packages/integrations/deno/package.json @@ -36,7 +36,7 @@ "esbuild": "^0.15.18" }, "peerDependencies": { - "astro": "workspace:^2.5.2" + "astro": "workspace:^2.5.3" }, "devDependencies": { "astro": "workspace:*", diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index 4768f6591..b7dff730f 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -62,7 +62,7 @@ "vite": "^4.3.1" }, "peerDependencies": { - "astro": "workspace:^2.5.2", + "astro": "workspace:^2.5.3", "sharp": ">=0.31.0" }, "peerDependenciesMeta": { diff --git a/packages/integrations/markdoc/CHANGELOG.md b/packages/integrations/markdoc/CHANGELOG.md index f39c514c4..ad8b7fb57 100644 --- a/packages/integrations/markdoc/CHANGELOG.md +++ b/packages/integrations/markdoc/CHANGELOG.md @@ -1,5 +1,14 @@ # @astrojs/markdoc +## 0.2.2 + +### Patch Changes + +- [#6758](https://github.com/withastro/astro/pull/6758) [`f558a9e20`](https://github.com/withastro/astro/commit/f558a9e2056fc8f2e2d5814e74f199e398159fc4) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improve style and script handling across content collection files. This addresses style bleed present in `@astrojs/markdoc` v0.1.0 + +- Updated dependencies [[`f558a9e20`](https://github.com/withastro/astro/commit/f558a9e2056fc8f2e2d5814e74f199e398159fc4), [`b41963b77`](https://github.com/withastro/astro/commit/b41963b775149b802eea9e12c5fe266bb9a02944)]: + - astro@2.5.3 + ## 0.2.1 ### Patch Changes diff --git a/packages/integrations/markdoc/components/Renderer.astro b/packages/integrations/markdoc/components/Renderer.astro index 5e2b6833a..6571e8c71 100644 --- a/packages/integrations/markdoc/components/Renderer.astro +++ b/packages/integrations/markdoc/components/Renderer.astro @@ -1,4 +1,5 @@ --- +//! astro-head-inject import type { Config } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; import { ComponentNode, createTreeNode } from './TreeNode.js'; @@ -14,4 +15,4 @@ const ast = Markdoc.Ast.fromJSON(stringifiedAst); const content = Markdoc.transform(ast, config); --- -<ComponentNode treeNode={createTreeNode(content)} /> +<ComponentNode treeNode={await createTreeNode(content)} /> diff --git a/packages/integrations/markdoc/components/TreeNode.ts b/packages/integrations/markdoc/components/TreeNode.ts index a60597a0d..e491d1dc9 100644 --- a/packages/integrations/markdoc/components/TreeNode.ts +++ b/packages/integrations/markdoc/components/TreeNode.ts @@ -2,7 +2,16 @@ import type { AstroInstance } from 'astro'; import { Fragment } from 'astro/jsx-runtime'; import type { RenderableTreeNode } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; -import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js'; +import { + createComponent, + renderComponent, + render, + renderScriptElement, + renderUniqueStylesheet, + createHeadAndContent, + unescapeHTML, + renderTemplate, +} from 'astro/runtime/server/index.js'; export type TreeNode = | { @@ -12,6 +21,9 @@ export type TreeNode = | { type: 'component'; component: AstroInstance['default']; + collectedLinks?: string[]; + collectedStyles?: string[]; + collectedScripts?: string[]; props: Record<string, any>; children: TreeNode[]; } @@ -32,20 +44,67 @@ export const ComponentNode = createComponent({ )}`, }; if (treeNode.type === 'component') { - return renderComponent( - result, - treeNode.component.name, - treeNode.component, - treeNode.props, - slots + let styles = '', + links = '', + scripts = ''; + if (Array.isArray(treeNode.collectedStyles)) { + styles = treeNode.collectedStyles + .map((style: any) => + renderUniqueStylesheet({ + type: 'inline', + content: style, + }) + ) + .join(''); + } + if (Array.isArray(treeNode.collectedLinks)) { + links = treeNode.collectedLinks + .map((link: any) => { + return renderUniqueStylesheet(result, { + href: link[0] === '/' ? link : '/' + link, + }); + }) + .join(''); + } + if (Array.isArray(treeNode.collectedScripts)) { + scripts = treeNode.collectedScripts + .map((script: any) => renderScriptElement(script)) + .join(''); + } + + const head = unescapeHTML(styles + links + scripts); + + let headAndContent = createHeadAndContent( + head, + renderTemplate`${renderComponent( + result, + treeNode.component.name, + treeNode.component, + treeNode.props, + slots + )}` ); + + // Let the runtime know that this component is being used. + result.propagators.set( + {}, + { + init() { + return headAndContent; + }, + } + ); + + return headAndContent; } return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots); }, - propagation: 'none', + propagation: 'self', }); -export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode { +export async function createTreeNode( + node: RenderableTreeNode | RenderableTreeNode[] +): Promise<TreeNode> { if (typeof node === 'string' || typeof node === 'number') { return { type: 'text', content: String(node) }; } else if (Array.isArray(node)) { @@ -53,16 +112,17 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): type: 'component', component: Fragment, props: {}, - children: node.map((child) => createTreeNode(child)), + children: await Promise.all(node.map((child) => createTreeNode(child))), }; } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) { return { type: 'text', content: '' }; } + const children = await Promise.all(node.children.map((child) => createTreeNode(child))); + if (typeof node.name === 'function') { const component = node.name; const props = node.attributes; - const children = node.children.map((child) => createTreeNode(child)); return { type: 'component', @@ -70,12 +130,38 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): props, children, }; + } else if (isPropagatedAssetsModule(node.name)) { + const { collectedStyles, collectedLinks, collectedScripts } = node.name; + const component = (await node.name.getMod())?.default ?? Fragment; + const props = node.attributes; + + return { + type: 'component', + component, + collectedStyles, + collectedLinks, + collectedScripts, + props, + children, + }; } else { return { type: 'element', tag: node.name, attributes: node.attributes, - children: node.children.map((child) => createTreeNode(child)), + children, }; } } + +type PropagatedAssetsModule = { + __astroPropagation: true; + getMod: () => Promise<AstroInstance['default']>; + collectedStyles: string[]; + collectedLinks: string[]; + collectedScripts: string[]; +}; + +function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule { + return typeof module === 'object' && module != null && '__astroPropagation' in module; +} diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 4bdc84deb..dfe41caca 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/markdoc", "description": "Add support for Markdoc pages in your Astro site", - "version": "0.2.1", + "version": "0.2.2", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -47,7 +47,7 @@ "zod": "^3.17.3" }, "peerDependencies": { - "astro": "workspace:^2.5.2" + "astro": "workspace:^2.5.3" }, "devDependencies": { "@astrojs/markdown-remark": "^2.2.1", diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 627f08c77..ba8a0af84 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -32,7 +32,11 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration name: '@astrojs/markdoc', hooks: { 'astro:config:setup': async (params) => { - const { config: astroConfig, addContentEntryType } = params as SetupHookParams; + const { + config: astroConfig, + updateConfig, + addContentEntryType, + } = params as SetupHookParams; markdocConfigResult = await loadMarkdocConfig(astroConfig); const userMarkdocConfig = markdocConfigResult?.config ?? {}; @@ -49,6 +53,9 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration addContentEntryType({ extensions: ['.mdoc'], getEntryInfo, + // Markdoc handles script / style propagation + // for Astro components internally + handlePropagation: false, async getRenderModule({ entry, viteId }) { const ast = Markdoc.parse(entry.body); const pluginContext = this; @@ -88,7 +95,10 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration }); } - const res = `import { jsx as h } from 'astro/jsx-runtime'; + const res = `import { + createComponent, + renderComponent, + } from 'astro/runtime/server/index.js'; import { Renderer } from '@astrojs/markdoc/components'; import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime'; import * as entry from ${JSON.stringify(viteId + '?astroContentCollectionEntry')}; @@ -119,14 +129,24 @@ export function getHeadings() { const content = Markdoc.transform(ast, config); return collectHeadings(Array.isArray(content) ? content : content.children); } -export async function Content (props) { - const config = setupConfig({ - ...userConfig, - variables: { ...userConfig.variables, ...props }, - }, entry); - return h(Renderer, { config, stringifiedAst }); -}`; +export const Content = createComponent({ + factory(result, props) { + const config = setupConfig({ + ...userConfig, + variables: { ...userConfig.variables, ...props }, + }, entry); + + return renderComponent( + result, + Renderer.name, + Renderer, + { stringifiedAst, config }, + {} + ); + }, + propagation: 'self', +});`; return { code: res }; }, contentModuleTypes: await fs.promises.readFile( @@ -134,6 +154,27 @@ export async function Content (props) { 'utf-8' ), }); + + updateConfig({ + vite: { + plugins: [ + { + name: '@astrojs/markdoc:astro-propagated-assets', + enforce: 'pre', + // Astro component styles and scripts should only be injected + // When a given Markdoc file actually uses that component. + // Add the `astroPropagatedAssets` flag to inject only when rendered. + resolveId(this: rollup.TransformPluginContext, id: string, importer: string) { + if (importer === markdocConfigResult?.fileUrl.pathname && id.endsWith('.astro')) { + return this.resolve(id + '?astroPropagatedAssets', importer, { + skipSelf: true, + }); + } + }, + }, + ], + }, + }); }, 'astro:server:setup': async ({ server }) => { server.watcher.on('all', (event, entry) => { diff --git a/packages/integrations/markdoc/src/nodes/heading.ts b/packages/integrations/markdoc/src/nodes/heading.ts index 0210e9b90..cb50dd231 100644 --- a/packages/integrations/markdoc/src/nodes/heading.ts +++ b/packages/integrations/markdoc/src/nodes/heading.ts @@ -37,13 +37,14 @@ export const heading: Schema = { const slug = getSlug(attributes, children, config.ctx.headingSlugger); const render = config.nodes?.heading?.render ?? `h${level}`; + const tagProps = // For components, pass down `level` as a prop, // alongside `__collectHeading` for our `headings` collector. // Avoid accidentally rendering `level` as an HTML attribute otherwise! - typeof render === 'function' - ? { ...attributes, id: slug, __collectHeading: true, level } - : { ...attributes, id: slug }; + typeof render === 'string' + ? { ...attributes, id: slug } + : { ...attributes, id: slug, __collectHeading: true, level }; return new Markdoc.Tag(render, tagProps, children); }, diff --git a/packages/integrations/mdx/CHANGELOG.md b/packages/integrations/mdx/CHANGELOG.md index 8f23d98ba..7f7ff251b 100644 --- a/packages/integrations/mdx/CHANGELOG.md +++ b/packages/integrations/mdx/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/mdx +## 0.19.3 + +### Patch Changes + +- [#6758](https://github.com/withastro/astro/pull/6758) [`f558a9e20`](https://github.com/withastro/astro/commit/f558a9e2056fc8f2e2d5814e74f199e398159fc4) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improve style and script handling across content collection files. This addresses style bleed present in `@astrojs/markdoc` v0.1.0 + ## 0.19.2 ### Patch Changes diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 52627a5ca..a3308dc55 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/mdx", "description": "Add support for MDX pages in your Astro site", - "version": "0.19.2", + "version": "0.19.3", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index 2ccf66266..1ef23e1af 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -55,6 +55,9 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI new URL('../template/content-module-types.d.ts', import.meta.url), 'utf-8' ), + // MDX can import scripts and styles, + // so wrap all MDX files with script / style propagation checks + handlePropagation: true, }); const extendMarkdownConfig = diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 1c2fe497c..6c3cfbac2 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -42,7 +42,7 @@ "esbuild": "^0.15.18" }, "peerDependencies": { - "astro": "workspace:^2.5.2" + "astro": "workspace:^2.5.3" }, "devDependencies": { "@netlify/edge-functions": "^2.0.0", diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index b045a3601..b49d0b1a6 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -38,7 +38,7 @@ "server-destroy": "^1.0.1" }, "peerDependencies": { - "astro": "workspace:^2.5.2" + "astro": "workspace:^2.5.3" }, "devDependencies": { "@types/send": "^0.17.1", diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 1d012a4ac..4926e6afe 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -48,7 +48,7 @@ "vite": "^4.3.1" }, "peerDependencies": { - "astro": "workspace:^2.5.2", + "astro": "workspace:^2.5.3", "svelte": "^3.54.0" }, "engines": { diff --git a/packages/integrations/tailwind/package.json b/packages/integrations/tailwind/package.json index 86e690a02..8fba3cf39 100644 --- a/packages/integrations/tailwind/package.json +++ b/packages/integrations/tailwind/package.json @@ -44,7 +44,7 @@ "vite": "^4.3.1" }, "peerDependencies": { - "astro": "workspace:^2.5.2", + "astro": "workspace:^2.5.3", "tailwindcss": "^3.0.24" }, "pnpm": { diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 4e04c67a8..72bb5758f 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -59,7 +59,7 @@ "web-vitals": "^3.1.1" }, "peerDependencies": { - "astro": "workspace:^2.5.2" + "astro": "workspace:^2.5.3" }, "devDependencies": { "@types/set-cookie-parser": "^2.4.2", diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json index e422ff82a..9855ccf4c 100644 --- a/packages/integrations/vue/package.json +++ b/packages/integrations/vue/package.json @@ -56,7 +56,7 @@ "vue": "^3.2.37" }, "peerDependencies": { - "astro": "workspace:^2.5.2", + "astro": "workspace:^2.5.3", "vue": "^3.2.30" }, "engines": {