Merge branch 'main' into redirects-ssg

This commit is contained in:
Matthew Phillips 2023-05-23 09:34:22 -04:00 committed by GitHub
commit 11a517b1f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 404 additions and 157 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Add types for `import.meta.env.ASSETS_PREFIX` and `import.meta.env.SITE`

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
value of var can be undefined when using `define:vars`

View file

@ -1,9 +0,0 @@
import { defineCollection, z } from 'astro:content';
const docs = defineCollection({
schema: z.object({
title: z.string(),
}),
});
export const collections = { docs };

View file

@ -1,5 +1,13 @@
# astro # 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 ## 2.5.2
### Patch Changes ### Patch Changes

View file

@ -1,5 +1,32 @@
/// <reference path="./import-meta.d.ts" /> /// <reference path="./import-meta.d.ts" />
// 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 projects 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' { 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 // Exporting things one by one is a bit cumbersome, not sure if there's a better way - erika, 2023-02-03
type AstroAssets = { type AstroAssets = {
@ -387,9 +414,3 @@ declare module '*?inline' {
const src: string; const src: string;
export default src; 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 {}
}

View file

@ -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. // 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 { interface ImageMetadata {
src: string; src: string;

View file

@ -21,10 +21,6 @@ declare module '*.svg' {
const src: string; const src: string;
export default src; export default src;
} }
declare module '*.ico' {
const src: string;
export default src;
}
declare module '*.webp' { declare module '*.webp' {
const src: string; const src: string;
export default src; export default src;

View file

@ -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 // This file is an augmentation to the built-in ImportMeta interface
// Thus cannot contain any top-level imports // Thus cannot contain any top-level imports
@ -6,13 +6,6 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */ /* 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 { interface ImportMeta {
url: string; url: string;

View file

@ -1,6 +1,6 @@
{ {
"name": "astro", "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.", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module", "type": "module",
"author": "withastro", "author": "withastro",

View file

@ -1280,6 +1280,12 @@ export interface ContentEntryType {
} }
): rollup.LoadResult | Promise<rollup.LoadResult>; ): rollup.LoadResult | Promise<rollup.LoadResult>;
contentModuleTypes?: string; 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 = { type GetContentEntryInfoReturnType = {

View file

@ -1,11 +1,18 @@
export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets'; export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets';
export const CONTENT_RENDER_FLAG = 'astroRenderContent';
export const CONTENT_FLAG = 'astroContentCollectionEntry'; export const CONTENT_FLAG = 'astroContentCollectionEntry';
export const DATA_FLAG = 'astroDataCollectionEntry'; 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 VIRTUAL_MODULE_ID = 'astro:content';
export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@'; export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@';
export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@'; export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@';
export const SCRIPTS_PLACEHOLDER = '@@ASTRO-SCRIPTS@@'; 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'; export const CONTENT_TYPES_FILE = 'types.d.ts';

View file

@ -270,62 +270,82 @@ async function render({
const baseMod = await renderEntryImport(); const baseMod = await renderEntryImport();
if (baseMod == null || typeof baseMod !== 'object') throw UnexpectedRenderError; if (baseMod == null || typeof baseMod !== 'object') throw UnexpectedRenderError;
const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod; if (
if (typeof getMod !== 'function') throw UnexpectedRenderError; baseMod.default != null &&
const mod = await getMod(); typeof baseMod.default === 'object' &&
if (mod == null || typeof mod !== 'object') throw UnexpectedRenderError; 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({ const Content = createComponent({
factory(result, baseProps, slots) { factory(result, baseProps, slots) {
let styles = '', let styles = '',
links = '', links = '',
scripts = ''; scripts = '';
if (Array.isArray(collectedStyles)) { if (Array.isArray(collectedStyles)) {
styles = collectedStyles styles = collectedStyles
.map((style: any) => { .map((style: any) => {
return renderUniqueStylesheet(result, { return renderUniqueStylesheet(result, {
type: 'inline', type: 'inline',
content: style, content: style,
}); });
}) })
.join(''); .join('');
} }
if (Array.isArray(collectedLinks)) { if (Array.isArray(collectedLinks)) {
links = collectedLinks links = collectedLinks
.map((link: any) => { .map((link: any) => {
return renderUniqueStylesheet(result, { return renderUniqueStylesheet(result, {
type: 'external', type: 'external',
src: prependForwardSlash(link), src: prependForwardSlash(link),
}); });
}) })
.join(''); .join('');
} }
if (Array.isArray(collectedScripts)) { if (Array.isArray(collectedScripts)) {
scripts = collectedScripts.map((script: any) => renderScriptElement(script)).join(''); scripts = collectedScripts.map((script: any) => renderScriptElement(script)).join('');
} }
let props = baseProps; let props = baseProps;
// Auto-apply MDX components export // Auto-apply MDX components export
if (id.endsWith('mdx')) { if (id.endsWith('mdx')) {
props = { props = {
components: mod.components ?? {}, components: propagationMod.components ?? {},
...baseProps, ...baseProps,
}; };
} }
return createHeadAndContent( return createHeadAndContent(
unescapeHTML(styles + links + scripts) as any, unescapeHTML(styles + links + scripts) as any,
renderTemplate`${renderComponent(result, 'Content', mod.Content, props, slots)}` renderTemplate`${renderComponent(
); result,
}, 'Content',
propagation: 'self', propagationMod.Content,
}); props,
slots
)}`
);
},
propagation: 'self',
});
return { return {
Content, Content,
headings: mod.getHeadings?.() ?? [], headings: propagationMod.getHeadings?.() ?? [],
remarkPluginFrontmatter: mod.frontmatter ?? {}, 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 }) { export function createReference({ lookupMap }: { lookupMap: ContentLookupMap }) {

View file

@ -46,7 +46,7 @@ function createGlobLookup(glob) {
} }
const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', { const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', {
query: { astroPropagatedAssets: true }, query: { astroRenderContent: true },
}); });
const collectionToRenderEntryMap = createCollectionToGlobResultMap({ const collectionToRenderEntryMap = createCollectionToGlobResultMap({
globResult: renderEntryGlob, globResult: renderEntryGlob,

View file

@ -14,6 +14,7 @@ import type {
} from '../@types/astro.js'; } from '../@types/astro.js';
import { VALID_INPUT_FORMATS } from '../assets/consts.js'; import { VALID_INPUT_FORMATS } from '../assets/consts.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { formatYAMLException, isYAMLException } from '../core/errors/utils.js'; import { formatYAMLException, isYAMLException } from '../core/errors/utils.js';
import { CONTENT_FLAGS, CONTENT_TYPES_FILE } from './consts.js'; import { CONTENT_FLAGS, CONTENT_TYPES_FILE } from './consts.js';
import { errorMap } from './error-map.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 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] ?? ''); const flags = new URLSearchParams(viteId.split('?')[1] ?? '');
return flags.has(flag); return flags.has(flag);
} }

View file

@ -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 { Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js'; import type { AstroSettings } from '../@types/astro.js';
import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.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 { getStylesForURL } from '../core/render/dev/css.js';
import { getScriptsForURL } from '../core/render/dev/scripts.js'; import { getScriptsForURL } from '../core/render/dev/scripts.js';
import { import {
CONTENT_RENDER_FLAG,
LINKS_PLACEHOLDER, LINKS_PLACEHOLDER,
PROPAGATED_ASSET_FLAG, PROPAGATED_ASSET_FLAG,
SCRIPTS_PLACEHOLDER, SCRIPTS_PLACEHOLDER,
STYLES_PLACEHOLDER, STYLES_PLACEHOLDER,
} from './consts.js'; } from './consts.js';
import { hasContentFlag } from './utils.js';
function isPropagatedAsset(viteId: string) {
const flags = new URLSearchParams(viteId.split('?')[1]);
return flags.has(PROPAGATED_ASSET_FLAG);
}
export function astroContentAssetPropagationPlugin({ export function astroContentAssetPropagationPlugin({
mode, mode,
@ -32,13 +30,31 @@ export function astroContentAssetPropagationPlugin({
let devModuleLoader: ModuleLoader; let devModuleLoader: ModuleLoader;
return { return {
name: 'astro:content-asset-propagation', 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) { configureServer(server) {
if (mode === 'dev') { if (mode === 'dev') {
devModuleLoader = createViteLoader(server); devModuleLoader = createViteLoader(server);
} }
}, },
async transform(_, id, options) { async transform(_, id, options) {
if (isPropagatedAsset(id)) { if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) {
const basePath = id.split('?')[0]; const basePath = id.split('?')[0];
let stringifiedLinks: string, stringifiedStyles: string, stringifiedScripts: string; let stringifiedLinks: string, stringifiedStyles: string, stringifiedScripts: string;
@ -73,14 +89,17 @@ export function astroContentAssetPropagationPlugin({
} }
const code = ` const code = `
export async function getMod() { async function getMod() {
return import(${JSON.stringify(basePath)}); return import(${JSON.stringify(basePath)});
} }
export const collectedLinks = ${stringifiedLinks}; const collectedLinks = ${stringifiedLinks};
export const collectedStyles = ${stringifiedStyles}; const collectedStyles = ${stringifiedStyles};
export const collectedScripts = ${stringifiedScripts}; 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: '' } }; return { code, map: { mappings: '' } };
} }
}, },

View file

@ -144,19 +144,19 @@ export class App {
if (routeData.type === 'page') { if (routeData.type === 'page') {
let response = await this.#renderPage(request, routeData, mod, defaultStatus); let response = await this.#renderPage(request, routeData, mod, defaultStatus);
// If there was a 500 error, try sending the 500 page. // If there was a known error code, try sending the according page (e.g. 404.astro / 500.astro).
if (response.status === 500) { if (response.status === 500 || response.status === 404) {
const fiveHundredRouteData = matchRoute('/500', this.#manifestData); const errorPageData = matchRoute('/' + response.status, this.#manifestData);
if (fiveHundredRouteData) { if (errorPageData && errorPageData.route !== routeData.route) {
mod = await this.#manifest.pageMap.get(fiveHundredRouteData.component)!(); mod = await this.#manifest.pageMap.get(errorPageData.component)!();
try { try {
let fiveHundredResponse = await this.#renderPage( let errorResponse = await this.#renderPage(
request, request,
fiveHundredRouteData, errorPageData,
mod, mod,
500 response.status
); );
return fiveHundredResponse; return errorResponse;
} catch {} } catch {}
} }
} }

View file

@ -1,7 +1,6 @@
import type { ModuleLoader, ModuleNode } from '../../module-loader/index'; import type { ModuleLoader, ModuleNode } from '../../module-loader/index';
import npath from 'path'; import npath from 'path';
import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js';
import { unwrapId } from '../../util.js'; import { unwrapId } from '../../util.js';
import { isCSSRequest } 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 * List of file extensions signalling we can (and should) SSR ahead-of-time
* See usage below * 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 STRIP_QUERY_PARAMS_REGEX = /\?.*$/;
const ASTRO_PROPAGATED_ASSET_REGEX = /\?astroPropagatedAssets/;
/** recursively crawl the module graph to get all style files imported by parent id */ /** recursively crawl the module graph to get all style files imported by parent id */
export async function* crawlGraph( export async function* crawlGraph(
@ -23,7 +23,6 @@ export async function* crawlGraph(
): AsyncGenerator<ModuleNode, void, unknown> { ): AsyncGenerator<ModuleNode, void, unknown> {
const id = unwrapId(_id); const id = unwrapId(_id);
const importedModules = new Set<ModuleNode>(); const importedModules = new Set<ModuleNode>();
if (new URL(id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) return;
const moduleEntriesForId = isRootFile const moduleEntriesForId = isRootFile
? // "getModulesByFile" pulls from a delayed module cache (fun implementation detail), ? // "getModulesByFile" pulls from a delayed module cache (fun implementation detail),
@ -44,6 +43,7 @@ export async function* crawlGraph(
if (id === entry.id) { if (id === entry.id) {
scanned.add(id); scanned.add(id);
const entryIsStyle = isCSSRequest(id); const entryIsStyle = isCSSRequest(id);
for (const importedModule of entry.importedModules) { for (const importedModule of entry.importedModules) {
// some dynamically imported modules are *not* server rendered in time // some dynamically imported modules are *not* server rendered in time
// to only SSR modules that we can safely transform, we check against // to only SSR modules that we can safely transform, we check against
@ -60,15 +60,13 @@ export async function* crawlGraph(
if (entryIsStyle && !isCSSRequest(importedModulePathname)) { if (entryIsStyle && !isCSSRequest(importedModulePathname)) {
continue; continue;
} }
const isFileTypeNeedingSSR = fileExtensionsToSSR.has(
npath.extname(importedModulePathname)
);
if ( if (
fileExtensionsToSSR.has( isFileTypeNeedingSSR &&
npath.extname( // Should not SSR a module with ?astroPropagatedAssets
// Use `id` instead of `pathname` to preserve query params. !ASTRO_PROPAGATED_ASSET_REGEX.test(importedModule.id)
// Should not SSR a module with an unexpected query param,
// like "?astroPropagatedAssets"
importedModule.id
)
)
) { ) {
const mod = loader.getModuleById(importedModule.id); const mod = loader.getModuleById(importedModule.id);
if (!mod?.ssrModule) { if (!mod?.ssrModule) {

View file

@ -43,7 +43,7 @@ export function defineScriptVars(vars: Record<any, any>) {
for (const [key, value] of Object.entries(vars)) { for (const [key, value] of Object.entries(vars)) {
// Use const instead of let as let global unsupported with Safari // Use const instead of let as let global unsupported with Safari
// https://stackoverflow.com/questions/29194024/cant-use-let-keyword-in-safari-javascript // 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, /<\/script>/g,
'\\x3C/script>' '\\x3C/script>'
)};\n`; )};\n`;

View file

@ -9,7 +9,8 @@ import { getTopLevelPages, walkParentInfos } from '../core/build/graph.js';
import type { BuildInternals } from '../core/build/internal.js'; import type { BuildInternals } from '../core/build/internal.js';
import { getAstroMetadata } from '../vite-plugin-astro/index.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({ export default function configHeadVitePlugin({
settings, settings,
@ -32,6 +33,7 @@ export default function configHeadVitePlugin({
seen.add(id); seen.add(id);
const mod = server.moduleGraph.getModuleById(id); const mod = server.moduleGraph.getModuleById(id);
const info = this.getModuleInfo(id); const info = this.getModuleInfo(id);
if (info?.meta.astro) { if (info?.meta.astro) {
const astroMetadata = getAstroMetadata(info); const astroMetadata = getAstroMetadata(info);
if (astroMetadata) { if (astroMetadata) {

View file

@ -13,6 +13,8 @@ export const markdownContentEntryType: ContentEntryType = {
rawData: parsed.matter, 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, 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' { contentModuleTypes: `declare module 'astro:content' {
interface Render { interface Render {
'.mdx': Promise<{ '.mdx': Promise<{

View file

@ -14,7 +14,7 @@ describe('Directives', async () => {
const html = await fixture.readFile('/define-vars/index.html'); const html = await fixture.readFile('/define-vars/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('script')).to.have.lengthOf(4); expect($('script')).to.have.lengthOf(5);
let i = 0; let i = 0;
for (const script of $('script').toArray()) { for (const script of $('script').toArray()) {
@ -27,9 +27,12 @@ describe('Directives', async () => {
} else if (i < 3) { } else if (i < 3) {
// Convert invalid keys to valid identifiers // Convert invalid keys to valid identifiers
expect($(script).toString()).to.include('const dashCase = "bar"'); expect($(script).toString()).to.include('const dashCase = "bar"');
} else { } else if (i < 4) {
// Closing script tags in strings are escaped // Closing script tags in strings are escaped
expect($(script).toString()).to.include('const bar = "<script>bar\\x3C/script>"'); expect($(script).toString()).to.include('const bar = "<script>bar\\x3C/script>"');
} else {
// Vars with undefined values are handled
expect($(script).toString()).to.include('const undef = undefined');
} }
i++; i++;
} }

View file

@ -4,6 +4,7 @@ let foo = 'bar'
let bg = 'white' let bg = 'white'
let fg = 'black' let fg = 'black'
let bar = '<script>bar</script>' let bar = '<script>bar</script>'
let undef: undefined;
--- ---
<html> <html>
@ -32,6 +33,9 @@ let bar = '<script>bar</script>'
<script id="inline-4" define:vars={{ bar }}> <script id="inline-4" define:vars={{ bar }}>
console.log(bar); console.log(bar);
</script> </script>
<script id="inline-5" define:vars={{ undef }}>
console.log(undef);
</script>
<Title /> <Title />
</body> </body>

View file

@ -0,0 +1,6 @@
---
return new Response(null, {
status: 404,
statusText: 'Not found',
});
---

View file

@ -62,6 +62,16 @@ describe('404 and 500 pages', () => {
expect($('h1').text()).to.equal('Something went horribly wrong!'); 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 () => { it('500 page returned when there is an error', async () => {
const app = await fixture.loadTestAdapterApp(); const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/causes-error'); const request = new Request('http://example.com/causes-error');

View file

@ -42,7 +42,7 @@
"tiny-glob": "^0.2.9" "tiny-glob": "^0.2.9"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2" "astro": "workspace:^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"astro": "workspace:*", "astro": "workspace:*",

View file

@ -36,7 +36,7 @@
"esbuild": "^0.15.18" "esbuild": "^0.15.18"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2" "astro": "workspace:^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"astro": "workspace:*", "astro": "workspace:*",

View file

@ -62,7 +62,7 @@
"vite": "^4.3.1" "vite": "^4.3.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2", "astro": "workspace:^2.5.3",
"sharp": ">=0.31.0" "sharp": ">=0.31.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {

View file

@ -1,5 +1,14 @@
# @astrojs/markdoc # @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 ## 0.2.1
### Patch Changes ### Patch Changes

View file

@ -1,4 +1,5 @@
--- ---
//! astro-head-inject
import type { Config } from '@markdoc/markdoc'; import type { Config } from '@markdoc/markdoc';
import Markdoc from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc';
import { ComponentNode, createTreeNode } from './TreeNode.js'; import { ComponentNode, createTreeNode } from './TreeNode.js';
@ -14,4 +15,4 @@ const ast = Markdoc.Ast.fromJSON(stringifiedAst);
const content = Markdoc.transform(ast, config); const content = Markdoc.transform(ast, config);
--- ---
<ComponentNode treeNode={createTreeNode(content)} /> <ComponentNode treeNode={await createTreeNode(content)} />

View file

@ -2,7 +2,16 @@ import type { AstroInstance } from 'astro';
import { Fragment } from 'astro/jsx-runtime'; import { Fragment } from 'astro/jsx-runtime';
import type { RenderableTreeNode } from '@markdoc/markdoc'; import type { RenderableTreeNode } from '@markdoc/markdoc';
import Markdoc 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 = export type TreeNode =
| { | {
@ -12,6 +21,9 @@ export type TreeNode =
| { | {
type: 'component'; type: 'component';
component: AstroInstance['default']; component: AstroInstance['default'];
collectedLinks?: string[];
collectedStyles?: string[];
collectedScripts?: string[];
props: Record<string, any>; props: Record<string, any>;
children: TreeNode[]; children: TreeNode[];
} }
@ -32,20 +44,67 @@ export const ComponentNode = createComponent({
)}`, )}`,
}; };
if (treeNode.type === 'component') { if (treeNode.type === 'component') {
return renderComponent( let styles = '',
result, links = '',
treeNode.component.name, scripts = '';
treeNode.component, if (Array.isArray(treeNode.collectedStyles)) {
treeNode.props, styles = treeNode.collectedStyles
slots .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); 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') { if (typeof node === 'string' || typeof node === 'number') {
return { type: 'text', content: String(node) }; return { type: 'text', content: String(node) };
} else if (Array.isArray(node)) { } else if (Array.isArray(node)) {
@ -53,16 +112,17 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
type: 'component', type: 'component',
component: Fragment, component: Fragment,
props: {}, 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)) { } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
return { type: 'text', content: '' }; return { type: 'text', content: '' };
} }
const children = await Promise.all(node.children.map((child) => createTreeNode(child)));
if (typeof node.name === 'function') { if (typeof node.name === 'function') {
const component = node.name; const component = node.name;
const props = node.attributes; const props = node.attributes;
const children = node.children.map((child) => createTreeNode(child));
return { return {
type: 'component', type: 'component',
@ -70,12 +130,38 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
props, props,
children, 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 { } else {
return { return {
type: 'element', type: 'element',
tag: node.name, tag: node.name,
attributes: node.attributes, 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;
}

View file

@ -1,7 +1,7 @@
{ {
"name": "@astrojs/markdoc", "name": "@astrojs/markdoc",
"description": "Add support for Markdoc pages in your Astro site", "description": "Add support for Markdoc pages in your Astro site",
"version": "0.2.1", "version": "0.2.2",
"type": "module", "type": "module",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"author": "withastro", "author": "withastro",
@ -47,7 +47,7 @@
"zod": "^3.17.3" "zod": "^3.17.3"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2" "astro": "workspace:^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/markdown-remark": "^2.2.1", "@astrojs/markdown-remark": "^2.2.1",

View file

@ -32,7 +32,11 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
name: '@astrojs/markdoc', name: '@astrojs/markdoc',
hooks: { hooks: {
'astro:config:setup': async (params) => { 'astro:config:setup': async (params) => {
const { config: astroConfig, addContentEntryType } = params as SetupHookParams; const {
config: astroConfig,
updateConfig,
addContentEntryType,
} = params as SetupHookParams;
markdocConfigResult = await loadMarkdocConfig(astroConfig); markdocConfigResult = await loadMarkdocConfig(astroConfig);
const userMarkdocConfig = markdocConfigResult?.config ?? {}; const userMarkdocConfig = markdocConfigResult?.config ?? {};
@ -49,6 +53,9 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
addContentEntryType({ addContentEntryType({
extensions: ['.mdoc'], extensions: ['.mdoc'],
getEntryInfo, getEntryInfo,
// Markdoc handles script / style propagation
// for Astro components internally
handlePropagation: false,
async getRenderModule({ entry, viteId }) { async getRenderModule({ entry, viteId }) {
const ast = Markdoc.parse(entry.body); const ast = Markdoc.parse(entry.body);
const pluginContext = this; 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 { Renderer } from '@astrojs/markdoc/components';
import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime'; import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime';
import * as entry from ${JSON.stringify(viteId + '?astroContentCollectionEntry')}; import * as entry from ${JSON.stringify(viteId + '?astroContentCollectionEntry')};
@ -119,14 +129,24 @@ export function getHeadings() {
const content = Markdoc.transform(ast, config); const content = Markdoc.transform(ast, config);
return collectHeadings(Array.isArray(content) ? content : content.children); 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 }; return { code: res };
}, },
contentModuleTypes: await fs.promises.readFile( contentModuleTypes: await fs.promises.readFile(
@ -134,6 +154,27 @@ export async function Content (props) {
'utf-8' '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 }) => { 'astro:server:setup': async ({ server }) => {
server.watcher.on('all', (event, entry) => { server.watcher.on('all', (event, entry) => {

View file

@ -37,13 +37,14 @@ export const heading: Schema = {
const slug = getSlug(attributes, children, config.ctx.headingSlugger); const slug = getSlug(attributes, children, config.ctx.headingSlugger);
const render = config.nodes?.heading?.render ?? `h${level}`; const render = config.nodes?.heading?.render ?? `h${level}`;
const tagProps = const tagProps =
// For components, pass down `level` as a prop, // For components, pass down `level` as a prop,
// alongside `__collectHeading` for our `headings` collector. // alongside `__collectHeading` for our `headings` collector.
// Avoid accidentally rendering `level` as an HTML attribute otherwise! // Avoid accidentally rendering `level` as an HTML attribute otherwise!
typeof render === 'function' typeof render === 'string'
? { ...attributes, id: slug, __collectHeading: true, level } ? { ...attributes, id: slug }
: { ...attributes, id: slug }; : { ...attributes, id: slug, __collectHeading: true, level };
return new Markdoc.Tag(render, tagProps, children); return new Markdoc.Tag(render, tagProps, children);
}, },

View file

@ -1,5 +1,11 @@
# @astrojs/mdx # @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 ## 0.19.2
### Patch Changes ### Patch Changes

View file

@ -1,7 +1,7 @@
{ {
"name": "@astrojs/mdx", "name": "@astrojs/mdx",
"description": "Add support for MDX pages in your Astro site", "description": "Add support for MDX pages in your Astro site",
"version": "0.19.2", "version": "0.19.3",
"type": "module", "type": "module",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"author": "withastro", "author": "withastro",

View file

@ -55,6 +55,9 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
new URL('../template/content-module-types.d.ts', import.meta.url), new URL('../template/content-module-types.d.ts', import.meta.url),
'utf-8' 'utf-8'
), ),
// MDX can import scripts and styles,
// so wrap all MDX files with script / style propagation checks
handlePropagation: true,
}); });
const extendMarkdownConfig = const extendMarkdownConfig =

View file

@ -42,7 +42,7 @@
"esbuild": "^0.15.18" "esbuild": "^0.15.18"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2" "astro": "workspace:^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"@netlify/edge-functions": "^2.0.0", "@netlify/edge-functions": "^2.0.0",

View file

@ -38,7 +38,7 @@
"server-destroy": "^1.0.1" "server-destroy": "^1.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2" "astro": "workspace:^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"@types/send": "^0.17.1", "@types/send": "^0.17.1",

View file

@ -48,7 +48,7 @@
"vite": "^4.3.1" "vite": "^4.3.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2", "astro": "workspace:^2.5.3",
"svelte": "^3.54.0" "svelte": "^3.54.0"
}, },
"engines": { "engines": {

View file

@ -44,7 +44,7 @@
"vite": "^4.3.1" "vite": "^4.3.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2", "astro": "workspace:^2.5.3",
"tailwindcss": "^3.0.24" "tailwindcss": "^3.0.24"
}, },
"pnpm": { "pnpm": {

View file

@ -59,7 +59,7 @@
"web-vitals": "^3.1.1" "web-vitals": "^3.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2" "astro": "workspace:^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"@types/set-cookie-parser": "^2.4.2", "@types/set-cookie-parser": "^2.4.2",

View file

@ -56,7 +56,7 @@
"vue": "^3.2.37" "vue": "^3.2.37"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.5.2", "astro": "workspace:^2.5.3",
"vue": "^3.2.30" "vue": "^3.2.30"
}, },
"engines": { "engines": {