From d47baa466aaeedde9c79ed5375d0be34762ac8b6 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 7 May 2024 13:13:03 -0400 Subject: [PATCH 1/7] Support React 19 (#10942) * Support React 19 * Fix lint * Update .changeset/short-phones-breathe.md * fix: update types peer dep --------- Co-authored-by: bholmesdev --- .changeset/short-phones-breathe.md | 5 ++ packages/integrations/react/package.json | 8 +-- packages/integrations/react/src/index.ts | 67 +++++++++++++++++------- 3 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 .changeset/short-phones-breathe.md diff --git a/.changeset/short-phones-breathe.md b/.changeset/short-phones-breathe.md new file mode 100644 index 000000000000..d27d015afbec --- /dev/null +++ b/.changeset/short-phones-breathe.md @@ -0,0 +1,5 @@ +--- +"@astrojs/react": patch +--- + +Updates package to support React 19 beta diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 90eaba4c3f3d..e088be21d8cc 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -60,10 +60,10 @@ "vite": "^5.2.10" }, "peerDependencies": { - "@types/react": "^17.0.50 || ^18.0.21", - "@types/react-dom": "^17.0.17 || ^18.0.6", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" + "@types/react": "^17.0.50 || ^18.0.21 || npm:types-react@beta", + "@types/react-dom": "^17.0.17 || ^18.0.6 || npm:types-react-dom@beta", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0-beta" }, "engines": { "node": "^18.17.1 || ^20.3.0 || >=21.0.0" diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index e0149e8e76c1..62803e788c8f 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -12,15 +12,44 @@ export type ReactIntegrationOptions = Pick< const FAST_REFRESH_PREAMBLE = react.preambleCode; -function getRenderer() { +const versionsConfig = { + 17: { + server: '@astrojs/react/server-v17.js', + client: '@astrojs/react/client-v17.js', + externals: ['react-dom/server.js', 'react-dom/client.js'], + }, + 18: { + server: '@astrojs/react/server.js', + client: '@astrojs/react/client.js', + externals: ['react-dom/server', 'react-dom/client'] + }, + 19: { + server: '@astrojs/react/server.js', + client: '@astrojs/react/client.js', + externals: ['react-dom/server', 'react-dom/client'] + } +}; + +type SupportedReactVersion = keyof (typeof versionsConfig); +type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion]; + +function getReactMajorVersion(): number { + const matches = /\d+\./.exec(ReactVersion); + if(!matches) { + return NaN; + } + return Number(matches[0]); +} + +function isUnsupportedVersion(majorVersion: number) { + return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion); +} + +function getRenderer(reactConfig: ReactVersionConfig) { return { name: '@astrojs/react', - clientEntrypoint: ReactVersion.startsWith('18.') - ? '@astrojs/react/client.js' - : '@astrojs/react/client-v17.js', - serverEntrypoint: ReactVersion.startsWith('18.') - ? '@astrojs/react/server.js' - : '@astrojs/react/server-v17.js', + clientEntrypoint: reactConfig.client, + serverEntrypoint: reactConfig.server, }; } @@ -51,22 +80,18 @@ function getViteConfiguration({ exclude, babel, experimentalReactChildren, -}: ReactIntegrationOptions = {}) { +}: ReactIntegrationOptions = {}, reactConfig: ReactVersionConfig) { return { optimizeDeps: { include: [ - ReactVersion.startsWith('18.') - ? '@astrojs/react/client.js' - : '@astrojs/react/client-v17.js', + reactConfig.client, 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', ], exclude: [ - ReactVersion.startsWith('18.') - ? '@astrojs/react/server.js' - : '@astrojs/react/server-v17.js', + reactConfig.server, ], }, plugins: [react({ include, exclude, babel }), optionsPlugin(!!experimentalReactChildren)], @@ -74,9 +99,7 @@ function getViteConfiguration({ dedupe: ['react', 'react-dom', 'react-dom/server'], }, ssr: { - external: ReactVersion.startsWith('18.') - ? ['react-dom/server', 'react-dom/client'] - : ['react-dom/server.js', 'react-dom/client.js'], + external: reactConfig.externals, noExternal: [ // These are all needed to get mui to work. '@mui/material', @@ -95,13 +118,19 @@ export default function ({ babel, experimentalReactChildren, }: ReactIntegrationOptions = {}): AstroIntegration { + const majorVersion = getReactMajorVersion(); + if(isUnsupportedVersion(majorVersion)) { + throw new Error(`Unsupported React version: ${majorVersion}.`); + } + const versionConfig = versionsConfig[majorVersion as SupportedReactVersion]; + return { name: '@astrojs/react', hooks: { 'astro:config:setup': ({ command, addRenderer, updateConfig, injectScript }) => { - addRenderer(getRenderer()); + addRenderer(getRenderer(versionConfig)); updateConfig({ - vite: getViteConfiguration({ include, exclude, babel, experimentalReactChildren }), + vite: getViteConfiguration({ include, exclude, babel, experimentalReactChildren }, versionConfig), }); if (command === 'dev') { const preamble = FAST_REFRESH_PREAMBLE.replace(`__BASE__`, '/'); From cceeafb62adf96b6f52b87024d774a30adf7f376 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 7 May 2024 17:13:57 +0000 Subject: [PATCH 2/7] [ci] format --- packages/integrations/react/src/index.ts | 31 ++++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index 62803e788c8f..838640239ee3 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -21,21 +21,21 @@ const versionsConfig = { 18: { server: '@astrojs/react/server.js', client: '@astrojs/react/client.js', - externals: ['react-dom/server', 'react-dom/client'] + externals: ['react-dom/server', 'react-dom/client'], }, 19: { server: '@astrojs/react/server.js', client: '@astrojs/react/client.js', - externals: ['react-dom/server', 'react-dom/client'] - } + externals: ['react-dom/server', 'react-dom/client'], + }, }; -type SupportedReactVersion = keyof (typeof versionsConfig); +type SupportedReactVersion = keyof typeof versionsConfig; type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion]; function getReactMajorVersion(): number { const matches = /\d+\./.exec(ReactVersion); - if(!matches) { + if (!matches) { return NaN; } return Number(matches[0]); @@ -75,12 +75,10 @@ function optionsPlugin(experimentalReactChildren: boolean): vite.Plugin { }; } -function getViteConfiguration({ - include, - exclude, - babel, - experimentalReactChildren, -}: ReactIntegrationOptions = {}, reactConfig: ReactVersionConfig) { +function getViteConfiguration( + { include, exclude, babel, experimentalReactChildren }: ReactIntegrationOptions = {}, + reactConfig: ReactVersionConfig +) { return { optimizeDeps: { include: [ @@ -90,9 +88,7 @@ function getViteConfiguration({ 'react/jsx-dev-runtime', 'react-dom', ], - exclude: [ - reactConfig.server, - ], + exclude: [reactConfig.server], }, plugins: [react({ include, exclude, babel }), optionsPlugin(!!experimentalReactChildren)], resolve: { @@ -119,7 +115,7 @@ export default function ({ experimentalReactChildren, }: ReactIntegrationOptions = {}): AstroIntegration { const majorVersion = getReactMajorVersion(); - if(isUnsupportedVersion(majorVersion)) { + if (isUnsupportedVersion(majorVersion)) { throw new Error(`Unsupported React version: ${majorVersion}.`); } const versionConfig = versionsConfig[majorVersion as SupportedReactVersion]; @@ -130,7 +126,10 @@ export default function ({ 'astro:config:setup': ({ command, addRenderer, updateConfig, injectScript }) => { addRenderer(getRenderer(versionConfig)); updateConfig({ - vite: getViteConfiguration({ include, exclude, babel, experimentalReactChildren }, versionConfig), + vite: getViteConfiguration( + { include, exclude, babel, experimentalReactChildren }, + versionConfig + ), }); if (command === 'dev') { const preamble = FAST_REFRESH_PREAMBLE.replace(`__BASE__`, '/'); From 685fc22bc6247be69a34c3f6945dec058c19fd71 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Wed, 8 May 2024 17:24:47 +0800 Subject: [PATCH 3/7] Improve content collection styles and scripts build perf (#10959) * Improve content collection styles and scripts build perf * Update test It was actually a bug. There was an empty module script injected. * Skip test * Fix test not matching non-ccc behaviour * Workaround bug to make test pass * Update .changeset/grumpy-pillows-develop.md Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Sarah Rainsberger --- .changeset/grumpy-pillows-develop.md | 5 + .../src/content/vite-plugin-content-assets.ts | 59 ++++------- packages/astro/src/core/build/internal.ts | 11 ++- packages/astro/src/core/build/page-data.ts | 4 - .../src/core/build/plugins/plugin-analyzer.ts | 99 ++++--------------- .../src/core/build/plugins/plugin-content.ts | 14 ++- .../src/core/build/plugins/plugin-css.ts | 68 ++++--------- .../build/plugins/plugin-hoisted-scripts.ts | 53 ++++------ packages/astro/src/core/build/types.ts | 2 - ...collections-css-inline-stylesheets.test.js | 5 +- ...imental-content-collections-render.test.js | 2 +- .../css-inline-stylesheets-3/package.json | 8 ++ .../src/components/Button.astro | 86 ++++++++++++++++ .../src/content/en/endeavour.md | 15 +++ .../css-inline-stylesheets-3/src/imported.css | 15 +++ .../src/layouts/Layout.astro | 35 +++++++ .../src/pages/index.astro | 17 ++++ .../mdx/test/css-head-mdx.test.js | 2 +- pnpm-lock.yaml | 6 ++ 19 files changed, 287 insertions(+), 219 deletions(-) create mode 100644 .changeset/grumpy-pillows-develop.md create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/package.json create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro diff --git a/.changeset/grumpy-pillows-develop.md b/.changeset/grumpy-pillows-develop.md new file mode 100644 index 000000000000..bba2a6fdcc97 --- /dev/null +++ b/.changeset/grumpy-pillows-develop.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Refactors internal handling of styles and scripts for content collections to improve build performance diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index 591cad3c70f6..d3228270a6dc 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -3,8 +3,7 @@ import { pathToFileURL } from 'node:url'; import type { Plugin, Rollup } from 'vite'; import type { AstroSettings, SSRElement } from '../@types/astro.js'; import { getAssetsPrefix } from '../assets/utils/getAssetsPrefix.js'; -import { getParentModuleInfos, moduleIsTopLevelPage } from '../core/build/graph.js'; -import { type BuildInternals, getPageDataByViteID } from '../core/build/internal.js'; +import type { BuildInternals } from '../core/build/internal.js'; import type { AstroBuildPlugin } from '../core/build/plugin.js'; import type { StaticBuildOptions } from '../core/build/types.js'; import type { ModuleLoader } from '../core/module-loader/loader.js'; @@ -163,49 +162,25 @@ export function astroConfigBuildPlugin( chunk.type === 'chunk' && (chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER)) ) { - let entryStyles = new Set(); - let entryLinks = new Set(); - let entryScripts = new Set(); + const entryStyles = new Set(); + const entryLinks = new Set(); + const entryScripts = new Set(); - if (options.settings.config.experimental.contentCollectionCache) { - // TODO: hoisted scripts are still handled on the pageData rather than the asset propagation point - for (const id of chunk.moduleIds) { - const _entryCss = internals.propagatedStylesMap.get(id); - const _entryScripts = internals.propagatedScriptsMap.get(id); - if (_entryCss) { - for (const value of _entryCss) { - if (value.type === 'inline') entryStyles.add(value.content); - if (value.type === 'external') entryLinks.add(value.src); - } - } - if (_entryScripts) { - for (const value of _entryScripts) { - entryScripts.add(value); - } + for (const id of chunk.moduleIds) { + const _entryCss = internals.propagatedStylesMap.get(id); + const _entryScripts = internals.propagatedScriptsMap.get(id); + if (_entryCss) { + // TODO: Separating styles and links this way is not ideal. The `entryCss` list is order-sensitive + // and splitting them into two sets causes the order to be lost, because styles are rendered after + // links. Refactor this away in the future. + for (const value of _entryCss) { + if (value.type === 'inline') entryStyles.add(value.content); + if (value.type === 'external') entryLinks.add(value.src); } } - } else { - for (const id of Object.keys(chunk.modules)) { - for (const pageInfo of getParentModuleInfos(id, ssrPluginContext!)) { - if (moduleIsTopLevelPage(pageInfo)) { - const pageViteID = pageInfo.id; - const pageData = getPageDataByViteID(internals, pageViteID); - if (!pageData) continue; - - const _entryCss = pageData.propagatedStyles?.get(id); - const _entryScripts = pageData.propagatedScripts?.get(id); - if (_entryCss) { - for (const value of _entryCss) { - if (value.type === 'inline') entryStyles.add(value.content); - if (value.type === 'external') entryLinks.add(value.src); - } - } - if (_entryScripts) { - for (const value of _entryScripts) { - entryScripts.add(value); - } - } - } + if (_entryScripts) { + for (const value of _entryScripts) { + entryScripts.add(value); } } } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index a7ff537dc8a4..a2c74271f496 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -25,8 +25,6 @@ export interface BuildInternals { hoistedScriptIdToHoistedMap: Map>; // A mapping of hoisted script ids back to the pages which reference it hoistedScriptIdToPagesMap: Map>; - // A mapping of hoisted script ids back to the content which reference it - hoistedScriptIdToContentMap: Map>; /** * Used by the `directRenderScript` option. If script is inlined, its id and @@ -93,7 +91,15 @@ export interface BuildInternals { cachedClientEntries: string[]; cacheManifestUsed: boolean; + /** + * Map of propagated module ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) + * to a set of stylesheets that it uses. + */ propagatedStylesMap: Map>; + /** + * Map of propagated module ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) + * to a set of hoisted scripts that it uses. + */ propagatedScriptsMap: Map>; // A list of all static files created during the build. Used for SSR. @@ -125,7 +131,6 @@ export function createBuildInternals(): BuildInternals { cssModuleToChunkIdMap: new Map(), hoistedScriptIdToHoistedMap, hoistedScriptIdToPagesMap, - hoistedScriptIdToContentMap: new Map(), inlinedScripts: new Map(), entrySpecifierToBundleMap: new Map(), pageToBundleMap: new Map(), diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index ce9e60622ec4..a151bae2c6f3 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -53,8 +53,6 @@ export async function collectPagesData( route, moduleSpecifier: '', styles: [], - propagatedStyles: new Map(), - propagatedScripts: new Map(), hoistedScript: undefined, hasSharedModules: false, }; @@ -78,8 +76,6 @@ export async function collectPagesData( route, moduleSpecifier: '', styles: [], - propagatedStyles: new Map(), - propagatedScripts: new Map(), hoistedScript: undefined, hasSharedModules: false, }; diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index 5bc0c53e04ea..06ba6fe0025c 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -6,7 +6,6 @@ import type { AstroBuildPlugin } from '../plugin.js'; import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { prependForwardSlash } from '../../../core/path.js'; -import { isContentCollectionsCacheEnabled } from '../../../core/util.js'; import { getParentModuleInfos, getTopLevelPageModuleInfos, @@ -32,9 +31,7 @@ export function vitePluginAnalyzer( const pageScripts = new Map< string, { - type: 'page' | 'content'; hoistedSet: Set; - propagatedMapByImporter: Map>; } >(); @@ -53,48 +50,12 @@ export function vitePluginAnalyzer( if (hoistedScripts.size) { for (const parentInfo of getParentModuleInfos(from, this, isPropagatedAsset)) { if (isPropagatedAsset(parentInfo.id)) { - if (isContentCollectionsCacheEnabled(options.settings.config)) { - if (!pageScripts.has(parentInfo.id)) { - pageScripts.set(parentInfo.id, { - type: 'content', - hoistedSet: new Set(), - propagatedMapByImporter: new Map(), - }); - } - const propagaters = pageScripts.get(parentInfo.id)!.propagatedMapByImporter; - for (const hid of hoistedScripts) { - if (!propagaters.has(parentInfo.id)) { - propagaters.set(parentInfo.id, new Set()); - } - propagaters.get(parentInfo.id)!.add(hid); - } - } else { - for (const nestedParentInfo of getParentModuleInfos(from, this)) { - if (moduleIsTopLevelPage(nestedParentInfo)) { - for (const hid of hoistedScripts) { - if (!pageScripts.has(nestedParentInfo.id)) { - pageScripts.set(nestedParentInfo.id, { - type: 'page', - hoistedSet: new Set(), - propagatedMapByImporter: new Map(), - }); - } - const entry = pageScripts.get(nestedParentInfo.id)!; - if (!entry.propagatedMapByImporter.has(parentInfo.id)) { - entry.propagatedMapByImporter.set(parentInfo.id, new Set()); - } - entry.propagatedMapByImporter.get(parentInfo.id)!.add(hid); - } - } - } - } + internals.propagatedScriptsMap.set(parentInfo.id, hoistedScripts); } else if (moduleIsTopLevelPage(parentInfo)) { for (const hid of hoistedScripts) { if (!pageScripts.has(parentInfo.id)) { pageScripts.set(parentInfo.id, { - type: 'page', hoistedSet: new Set(), - propagatedMapByImporter: new Map(), }); } pageScripts.get(parentInfo.id)?.hoistedSet.add(hid); @@ -105,21 +66,20 @@ export function vitePluginAnalyzer( }, finalize() { - for (const [pageId, { hoistedSet, propagatedMapByImporter, type }] of pageScripts) { - let astroModuleId: string; - if (type === 'page') { - const pageData = getPageDataByViteID(internals, pageId); - if (!pageData) { - continue; - } - const { component } = pageData; - astroModuleId = prependForwardSlash(component); - - // Keep track of the importers - pageData.propagatedScripts = propagatedMapByImporter; - } else { - astroModuleId = pageId; + // Add propagated scripts to client build, + // but DON'T add to pages -> hoisted script map. + for (const propagatedScripts of internals.propagatedScriptsMap.values()) { + for (const propagatedScript of propagatedScripts) { + internals.discoveredScripts.add(propagatedScript); } + } + + for (const [pageId, { hoistedSet }] of pageScripts) { + const pageData = getPageDataByViteID(internals, pageId); + if (!pageData) continue; + + const { component } = pageData; + const astroModuleId = prependForwardSlash(component); const uniqueHoistedId = JSON.stringify(Array.from(hoistedSet).sort()); let moduleId: string; @@ -134,32 +94,13 @@ export function vitePluginAnalyzer( } internals.discoveredScripts.add(moduleId); - // Add propagated scripts to client build, - // but DON'T add to pages -> hoisted script map. - for (const propagatedScripts of propagatedMapByImporter.values()) { - for (const propagatedScript of propagatedScripts) { - internals.discoveredScripts.add(propagatedScript); - } - } - - if (type === 'page') { - // Make sure to track that this page uses this set of hoisted scripts - if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { - const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); - pages!.add(astroModuleId); - } else { - internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId])); - internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); - } + // Make sure to track that this page uses this set of hoisted scripts + if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { + const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); + pages!.add(astroModuleId); } else { - // For content collections save to hoistedScriptIdToContentMap instead - if (internals.hoistedScriptIdToContentMap.has(moduleId)) { - const contentModules = internals.hoistedScriptIdToContentMap.get(moduleId); - contentModules!.add(astroModuleId); - } else { - internals.hoistedScriptIdToContentMap.set(moduleId, new Set([astroModuleId])); - internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); - } + internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId])); + internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); } } }, diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts index 7210dd4f184b..b6843e52b351 100644 --- a/packages/astro/src/core/build/plugins/plugin-content.ts +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -171,6 +171,7 @@ function vitePluginContent( outputOptions(outputOptions) { const rootPath = normalizePath(fileURLToPath(opts.settings.config.root)); const srcPath = normalizePath(fileURLToPath(opts.settings.config.srcDir)); + const entryCache = new Map(); extendManualChunks(outputOptions, { before(id, meta) { if (id.startsWith(srcPath) && id.slice(srcPath.length).startsWith('content')) { @@ -186,7 +187,11 @@ function vitePluginContent( return resultId; } const [srcRelativePath, flag] = id.replace(rootPath, '/').split('?'); - const collectionEntry = findEntryFromSrcRelativePath(lookupMap, srcRelativePath); + const collectionEntry = findEntryFromSrcRelativePath( + lookupMap, + srcRelativePath, + entryCache + ); if (collectionEntry) { let suffix = '.mjs'; if (flag === PROPAGATED_ASSET_FLAG) { @@ -273,8 +278,11 @@ function vitePluginContent( }; } -const entryCache = new Map(); -function findEntryFromSrcRelativePath(lookupMap: ContentLookupMap, srcRelativePath: string) { +function findEntryFromSrcRelativePath( + lookupMap: ContentLookupMap, + srcRelativePath: string, + entryCache: Map +) { let value = entryCache.get(srcRelativePath); if (value) return value; for (const collection of Object.values(lookupMap)) { diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index e0dce339f769..c50951e0b081 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -5,7 +5,6 @@ import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin, BuildTarget } from '../plugin.js'; import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js'; -import { RESOLVED_VIRTUAL_MODULE_ID as ASTRO_CONTENT_VIRTUAL_MODULE_ID } from '../../../content/consts.js'; import { hasAssetPropagationFlag } from '../../../content/index.js'; import type { AstroPluginCssMetadata } from '../../../vite-plugin-astro/index.js'; import * as assetName from '../css-asset-name.js'; @@ -63,11 +62,8 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { // stylesheet filenames are kept in here until "post", when they are rendered and ready to be inlined const pagesToCss: Record> = {}; - const pagesToPropagatedCss: Record>> = {}; - - const isContentCollectionCache = - options.buildOptions.settings.config.output === 'static' && - options.buildOptions.settings.config.experimental.contentCollectionCache; + // Map of module Ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) to its imported CSS + const moduleIdToPropagatedCss: Record> = {}; const cssBuildPlugin: VitePlugin = { name: 'astro:rollup-plugin-build-css', @@ -141,20 +137,9 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const parentModuleInfos = getParentExtendedModuleInfos(id, this, hasAssetPropagationFlag); for (const { info: pageInfo, depth, order } of parentModuleInfos) { if (hasAssetPropagationFlag(pageInfo.id)) { - const walkId = isContentCollectionCache ? ASTRO_CONTENT_VIRTUAL_MODULE_ID : id; - for (const parentInfo of getParentModuleInfos(walkId, this)) { - if (moduleIsTopLevelPage(parentInfo) === false) continue; - - const pageViteID = parentInfo.id; - const pageData = getPageDataByViteID(internals, pageViteID); - if (pageData === undefined) continue; - - for (const css of meta.importedCss) { - const propagatedStyles = (pagesToPropagatedCss[pageData.moduleSpecifier] ??= {}); - const existingCss = (propagatedStyles[pageInfo.id] ??= new Set()); - - existingCss.add(css); - } + const propagatedCss = (moduleIdToPropagatedCss[pageInfo.id] ??= new Set()); + for (const css of meta.importedCss) { + propagatedCss.add(css); } } else if (moduleIsTopLevelPage(pageInfo)) { const pageViteID = pageInfo.id; @@ -251,41 +236,30 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { ? { type: 'inline', content: stylesheet.source } : { type: 'external', src: stylesheet.fileName }; - const pages = Array.from(eachPageData(internals)); let sheetAddedToPage = false; - pages.forEach((pageData) => { + // Apply `pagesToCss` information to the respective `pageData.styles` + for (const pageData of eachPageData(internals)) { const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName]; if (orderingInfo !== undefined) { pageData.styles.push({ ...orderingInfo, sheet }); sheetAddedToPage = true; - return; } + } - const propagatedPaths = pagesToPropagatedCss[pageData.moduleSpecifier]; - if (propagatedPaths === undefined) return; - Object.entries(propagatedPaths).forEach(([pageInfoId, css]) => { - // return early if sheet does not need to be propagated - if (css.has(stylesheet.fileName) !== true) return; - - // return early if the stylesheet needing propagation has already been included - if (pageData.styles.some((s) => s.sheet === sheet)) return; - - let propagatedStyles: Set; - if (isContentCollectionCache) { - propagatedStyles = - internals.propagatedStylesMap.get(pageInfoId) ?? - internals.propagatedStylesMap.set(pageInfoId, new Set()).get(pageInfoId)!; - } else { - propagatedStyles = - pageData.propagatedStyles.get(pageInfoId) ?? - pageData.propagatedStyles.set(pageInfoId, new Set()).get(pageInfoId)!; - } - - propagatedStyles.add(sheet); - sheetAddedToPage = true; - }); - }); + // Apply `moduleIdToPropagatedCss` information to `internals.propagatedStylesMap`. + // NOTE: It's pretty much a copy over to `internals.propagatedStylesMap` as it should be + // completely empty. The whole propagation handling could be better refactored in the future. + for (const moduleId in moduleIdToPropagatedCss) { + if (!moduleIdToPropagatedCss[moduleId].has(stylesheet.fileName)) continue; + let propagatedStyles = internals.propagatedStylesMap.get(moduleId); + if (!propagatedStyles) { + propagatedStyles = new Set(); + internals.propagatedStylesMap.set(moduleId, propagatedStyles); + } + propagatedStyles.add(sheet); + sheetAddedToPage = true; + } if (toBeInlined && sheetAddedToPage) { // CSS is already added to all used pages, we can delete it from the bundle diff --git a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts index 80bfa6a6e964..2ed3c7fa746d 100644 --- a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -1,6 +1,6 @@ import type { BuildOptions, Plugin as VitePlugin } from 'vite'; import type { AstroSettings } from '../../../@types/astro.js'; -import { isContentCollectionsCacheEnabled, viteID } from '../../util.js'; +import { viteID } from '../../util.js'; import type { BuildInternals } from '../internal.js'; import { getPageDataByViteID } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; @@ -72,42 +72,23 @@ export function vitePluginHoistedScripts( output.dynamicImports.length === 0 && shouldInlineAsset(output.code, output.fileName, assetsInlineLimit); let removeFromBundle = false; - const facadeId = output.facadeModuleId!; - - // Pages - if (internals.hoistedScriptIdToPagesMap.has(facadeId)) { - const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; - for (const pathname of pages) { - const vid = viteID(new URL('.' + pathname, settings.config.root)); - - const pageInfo = getPageDataByViteID(internals, vid); - if (pageInfo) { - if (canBeInlined) { - pageInfo.hoistedScript = { - type: 'inline', - value: output.code, - }; - removeFromBundle = true; - } else { - pageInfo.hoistedScript = { - type: 'external', - value: id, - }; - } - } - } - } - // Content collection entries - else { - const contentModules = internals.hoistedScriptIdToContentMap.get(facadeId)!; - for (const contentId of contentModules) { - if (isContentCollectionsCacheEnabled(settings.config)) { - const scripts = - internals.propagatedScriptsMap.get(contentId) ?? - internals.propagatedScriptsMap.set(contentId, new Set()).get(contentId)!; - - scripts.add(facadeId); + const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; + for (const pathname of pages) { + const vid = viteID(new URL('.' + pathname, settings.config.root)); + const pageInfo = getPageDataByViteID(internals, vid); + if (pageInfo) { + if (canBeInlined) { + pageInfo.hoistedScript = { + type: 'inline', + value: output.code, + }; + removeFromBundle = true; + } else { + pageInfo.hoistedScript = { + type: 'external', + value: id, + }; } } } diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 9b418f7bc56d..4b502c353c5a 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -26,8 +26,6 @@ export interface PageBuildData { component: ComponentPath; route: RouteData; moduleSpecifier: string; - propagatedStyles: Map>; - propagatedScripts: Map>; hoistedScript: HoistedScriptAsset | undefined; styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>; hasSharedModules: boolean; diff --git a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js index a4aede4966a5..d6c509de5b64 100644 --- a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js +++ b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js @@ -154,7 +154,10 @@ describe('Setting inlineStylesheets to always in static output', () => { // to bust cache and prevent modules and their state // from being reused site: 'https://test.net/', - root: './fixtures/css-inline-stylesheets/', + // TODO: Uses -3 variant to bust ESM module cache when rendering the pages. Particularly in + // `node_modules/.astro/content/entry.mjs` and `import('./en/endeavour.mjs')`. Ideally this + // should be solved in core, but using this workaround for now. + root: './fixtures/css-inline-stylesheets-3/', output: 'static', build: { inlineStylesheets: 'always', diff --git a/packages/astro/test/experimental-content-collections-render.test.js b/packages/astro/test/experimental-content-collections-render.test.js index 19349dc39764..9e1dcdb4614e 100644 --- a/packages/astro/test/experimental-content-collections-render.test.js +++ b/packages/astro/test/experimental-content-collections-render.test.js @@ -72,7 +72,7 @@ if (!isWindows) { // Includes hoisted script assert.notEqual( - [...allScripts].find((script) => $(script).attr('src')?.includes('hoisted')), + [...allScripts].find((script) => $(script).attr('src')?.includes('/_astro/WithScripts')), undefined, 'hoisted script missing from head.' ); diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/package.json b/packages/astro/test/fixtures/css-inline-stylesheets-3/package.json new file mode 100644 index 000000000000..00e58c587604 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/css-inline-stylesheets-3", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro new file mode 100644 index 000000000000..3f25cbd3e3ae --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro @@ -0,0 +1,86 @@ +--- +const { class: className = '', style, href } = Astro.props; +const { variant = 'primary' } = Astro.props; +--- + + + + + + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md new file mode 100644 index 000000000000..240eeeae3993 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md @@ -0,0 +1,15 @@ +--- +title: Endeavour +description: 'Learn about the Endeavour NASA space shuttle.' +publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)' +layout: '../../layouts/Layout.astro' +tags: [space, 90s] +--- + +**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) + +Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly. + +The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986. + +NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly. \ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css new file mode 100644 index 000000000000..3959523ff16e --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css @@ -0,0 +1,15 @@ +.bg-skyblue { + background: skyblue; +} + +.bg-lightcoral { + background: lightcoral; +} + +.red { + color: darkred; +} + +.blue { + color: royalblue; +} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro new file mode 100644 index 000000000000..0a26655189f5 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro @@ -0,0 +1,35 @@ +--- +import Button from '../components/Button.astro'; +import '../imported.css'; + +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + {title} + + + + + + + diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro new file mode 100644 index 000000000000..2aecfb0f2eb7 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro @@ -0,0 +1,17 @@ +--- +import { getEntryBySlug } from 'astro:content'; +import Button from '../components/Button.astro'; + +const entry = await getEntryBySlug('en', 'endeavour'); +const { Content } = await entry.render(); +--- + +
+

Welcome to Astro

+ + +
diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index 5caab3d059b8..083348015c54 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -50,7 +50,7 @@ describe('Head injection w/ MDX', () => { assert.equal(links.length, 1); const scripts = document.querySelectorAll('head script[type=module]'); - assert.equal(scripts.length, 2); + assert.equal(scripts.length, 1); }); it('Using component using slots.render() API', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be362ddc2641..225e8e10ec66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2612,6 +2612,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/css-inline-stylesheets-3: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/css-no-code-split: dependencies: astro: From ddd8e49d1a179bec82310fb471f822a1567a6610 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Wed, 8 May 2024 17:25:27 +0800 Subject: [PATCH 4/7] MDX integration v3 (#10935) * fix(mdx): convert remark-images-to-component plugin to a rehype plugin (#10697) * Remove fs read for MDX transform (#10866) * Tag MDX component for faster checks when rendering (#10864) * Use unified plugin only for MDX transform (#10869) * Only traverse children and handle mdxJsxTextElement when optimizing (#10885) * Rename to `optimize.ignoreComponentNames` in MDX (#10884) * Allow remark/rehype plugins added after mdx to work (#10877) * Improve MDX optimize with sibling nodes (#10887) * Improve types in rehype-optimize-static.ts * Rename `ignoreComponentNames` to `ignoreElementNames` I think this better reflects what it's actually doing * Simplify plain MDX nodes in optimize option (#10934) * Format code * Minimize diff changes * Update .changeset/slimy-cobras-end.md Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Armand Philippot <59021693+ArmandPhilippot@users.noreply.github.com> Co-authored-by: Sarah Rainsberger --- .changeset/blue-geese-visit.md | 5 + .changeset/chilly-items-help.md | 5 + .changeset/fresh-masks-agree.md | 5 + .changeset/friendly-plants-leave.md | 5 + .changeset/large-glasses-jam.md | 5 + .changeset/slimy-cobras-end.md | 7 + .changeset/small-oranges-report.md | 5 + .changeset/smart-rats-mate.md | 5 + .changeset/sweet-goats-own.md | 5 + .changeset/tame-avocados-relax.md | 5 + .changeset/violet-snails-call.md | 5 + .changeset/young-chicken-exercise.md | 5 + packages/astro/package.json | 2 + packages/astro/src/jsx/babel.ts | 3 + packages/astro/src/jsx/rehype.ts | 320 ++++++++++++++++++ packages/astro/src/jsx/server.ts | 38 ++- packages/astro/src/jsx/transform-options.ts | 3 + packages/astro/src/vite-plugin-mdx/index.ts | 4 +- packages/astro/src/vite-plugin-mdx/tag.ts | 2 + .../src/vite-plugin-mdx/transform-jsx.ts | 3 + .../units/dev/collections-renderentry.test.js | 12 - packages/integrations/mdx/package.json | 4 +- packages/integrations/mdx/src/README.md | 39 ++- packages/integrations/mdx/src/index.ts | 29 +- packages/integrations/mdx/src/plugins.ts | 16 +- .../mdx/src/rehype-images-to-component.ts | 166 +++++++++ .../mdx/src/rehype-optimize-static.ts | 251 ++++++++++++-- .../mdx/src/remark-images-to-component.ts | 156 --------- .../mdx/src/vite-plugin-mdx-postprocess.ts | 75 ++-- .../integrations/mdx/src/vite-plugin-mdx.ts | 50 +-- .../fixtures/mdx-optimize/astro.config.mjs | 2 +- .../integrations/mdx/test/mdx-plugins.test.js | 24 ++ .../test/units/rehype-optimize-static.test.js | 89 +++++ pnpm-lock.yaml | 12 + 34 files changed, 1081 insertions(+), 281 deletions(-) create mode 100644 .changeset/blue-geese-visit.md create mode 100644 .changeset/chilly-items-help.md create mode 100644 .changeset/fresh-masks-agree.md create mode 100644 .changeset/friendly-plants-leave.md create mode 100644 .changeset/large-glasses-jam.md create mode 100644 .changeset/slimy-cobras-end.md create mode 100644 .changeset/small-oranges-report.md create mode 100644 .changeset/smart-rats-mate.md create mode 100644 .changeset/sweet-goats-own.md create mode 100644 .changeset/tame-avocados-relax.md create mode 100644 .changeset/violet-snails-call.md create mode 100644 .changeset/young-chicken-exercise.md create mode 100644 packages/astro/src/jsx/rehype.ts create mode 100644 packages/integrations/mdx/src/rehype-images-to-component.ts delete mode 100644 packages/integrations/mdx/src/remark-images-to-component.ts create mode 100644 packages/integrations/mdx/test/units/rehype-optimize-static.test.js diff --git a/.changeset/blue-geese-visit.md b/.changeset/blue-geese-visit.md new file mode 100644 index 000000000000..408386d046c3 --- /dev/null +++ b/.changeset/blue-geese-visit.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Simplifies plain MDX components as hast element nodes to further improve HTML string inlining for the `optimize` option diff --git a/.changeset/chilly-items-help.md b/.changeset/chilly-items-help.md new file mode 100644 index 000000000000..7e868474e32c --- /dev/null +++ b/.changeset/chilly-items-help.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Improves the error message when failed to render MDX components diff --git a/.changeset/fresh-masks-agree.md b/.changeset/fresh-masks-agree.md new file mode 100644 index 000000000000..08fc812c8841 --- /dev/null +++ b/.changeset/fresh-masks-agree.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": major +--- + +Refactors the MDX transformation to rely only on the unified pipeline. Babel and esbuild transformations are removed, which should result in faster build times. The refactor requires using Astro v4.8.0 but no other changes are necessary. diff --git a/.changeset/friendly-plants-leave.md b/.changeset/friendly-plants-leave.md new file mode 100644 index 000000000000..c972fa42c4db --- /dev/null +++ b/.changeset/friendly-plants-leave.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Exports `astro/jsx/rehype.js` with utilities to generate an Astro metadata object diff --git a/.changeset/large-glasses-jam.md b/.changeset/large-glasses-jam.md new file mode 100644 index 000000000000..885471d82fba --- /dev/null +++ b/.changeset/large-glasses-jam.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Allows Vite plugins to transform `.mdx` files before the MDX plugin transforms it diff --git a/.changeset/slimy-cobras-end.md b/.changeset/slimy-cobras-end.md new file mode 100644 index 000000000000..58f22ac07c12 --- /dev/null +++ b/.changeset/slimy-cobras-end.md @@ -0,0 +1,7 @@ +--- +"@astrojs/mdx": major +--- + +Allows integrations after the MDX integration to update `markdown.remarkPlugins` and `markdown.rehypePlugins`, and have the plugins work in MDX too. + +If your integration relies on Astro's previous behavior that prevents integrations from adding remark/rehype plugins for MDX, you will now need to configure `@astrojs/mdx` with `extendMarkdownConfig: false` and explicitly specify any `remarkPlugins` and `rehypePlugins` options instead. diff --git a/.changeset/small-oranges-report.md b/.changeset/small-oranges-report.md new file mode 100644 index 000000000000..8d0906e0530b --- /dev/null +++ b/.changeset/small-oranges-report.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": major +--- + +Renames the `optimize.customComponentNames` option to `optimize.ignoreElementNames` to better reflect its usecase. Its behaviour is not changed and should continue to work as before. diff --git a/.changeset/smart-rats-mate.md b/.changeset/smart-rats-mate.md new file mode 100644 index 000000000000..b779a86c8a5b --- /dev/null +++ b/.changeset/smart-rats-mate.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Updates the `optimize` option to group static sibling nodes as a ``. This reduces the number of AST nodes and simplifies runtime rendering of MDX pages. diff --git a/.changeset/sweet-goats-own.md b/.changeset/sweet-goats-own.md new file mode 100644 index 000000000000..6689246c33b3 --- /dev/null +++ b/.changeset/sweet-goats-own.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": major +--- + +Replaces the internal `remark-images-to-component` plugin with `rehype-images-to-component` to let users use additional rehype plugins for images diff --git a/.changeset/tame-avocados-relax.md b/.changeset/tame-avocados-relax.md new file mode 100644 index 000000000000..9b6a36881c03 --- /dev/null +++ b/.changeset/tame-avocados-relax.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Tags the MDX component export for quicker component checks while rendering diff --git a/.changeset/violet-snails-call.md b/.changeset/violet-snails-call.md new file mode 100644 index 000000000000..b7f06a7b9321 --- /dev/null +++ b/.changeset/violet-snails-call.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Fixes `export const components` keys detection for the `optimize` option diff --git a/.changeset/young-chicken-exercise.md b/.changeset/young-chicken-exercise.md new file mode 100644 index 000000000000..04b7417bbe21 --- /dev/null +++ b/.changeset/young-chicken-exercise.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Improves `optimize` handling for MDX components with attributes and inline MDX components diff --git a/packages/astro/package.json b/packages/astro/package.json index 6c3bcfeddbf3..572d5a9863f8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -209,6 +209,8 @@ "astro-scripts": "workspace:*", "cheerio": "1.0.0-rc.12", "eol": "^0.9.1", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.1.2", "memfs": "^4.9.1", "node-mocks-http": "^1.14.1", "parse-srcset": "^1.0.2", diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts index e8f9da87e2e1..d5fc0ccd30b0 100644 --- a/packages/astro/src/jsx/babel.ts +++ b/packages/astro/src/jsx/babel.ts @@ -134,6 +134,9 @@ function addClientOnlyMetadata( } } +/** + * @deprecated This plugin is no longer used. Remove in Astro 5.0 + */ export default function astroJSX(): PluginObj { return { visitor: { diff --git a/packages/astro/src/jsx/rehype.ts b/packages/astro/src/jsx/rehype.ts new file mode 100644 index 000000000000..40a8359cbe5c --- /dev/null +++ b/packages/astro/src/jsx/rehype.ts @@ -0,0 +1,320 @@ +import type { RehypePlugin } from '@astrojs/markdown-remark'; +import type { RootContent } from 'hast'; +import type { + MdxJsxAttribute, + MdxJsxFlowElementHast, + MdxJsxTextElementHast, +} from 'mdast-util-mdx-jsx'; +import { visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; +import { AstroError } from '../core/errors/errors.js'; +import { AstroErrorData } from '../core/errors/index.js'; +import { resolvePath } from '../core/util.js'; +import type { PluginMetadata } from '../vite-plugin-astro/types.js'; + +// This import includes ambient types for hast to include mdx nodes +import type {} from 'mdast-util-mdx'; + +const ClientOnlyPlaceholder = 'astro-client-only'; + +export const rehypeAnalyzeAstroMetadata: RehypePlugin = () => { + return (tree, file) => { + // Initial metadata for this MDX file, it will be mutated as we traverse the tree + const metadata: PluginMetadata['astro'] = { + clientOnlyComponents: [], + hydratedComponents: [], + scripts: [], + containsHead: false, + propagation: 'none', + pageOptions: {}, + }; + + // Parse imports in this file. This is used to match components with their import source + const imports = parseImports(tree.children); + + visit(tree, (node) => { + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return; + + const tagName = node.name; + if (!tagName || !isComponent(tagName) || !hasClientDirective(node)) return; + + // From this point onwards, `node` is confirmed to be an island component + + // Match this component with its import source + const matchedImport = findMatchingImport(tagName, imports); + if (!matchedImport) { + throw new AstroError({ + ...AstroErrorData.NoMatchingImport, + message: AstroErrorData.NoMatchingImport.message(node.name!), + }); + } + + // If this is an Astro component, that means the `client:` directive is misused as it doesn't + // work on Astro components as it's server-side only. Warn the user about this. + if (matchedImport.path.endsWith('.astro')) { + const clientAttribute = node.attributes.find( + (attr) => attr.type === 'mdxJsxAttribute' && attr.name.startsWith('client:') + ) as MdxJsxAttribute | undefined; + if (clientAttribute) { + // eslint-disable-next-line + console.warn( + `You are attempting to render <${node.name!} ${ + clientAttribute.name + } />, but ${node.name!} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.` + ); + } + } + + const resolvedPath = resolvePath(matchedImport.path, file.path); + + if (hasClientOnlyDirective(node)) { + // Add this component to the metadata + metadata.clientOnlyComponents.push({ + exportName: matchedImport.name, + specifier: tagName, + resolvedPath, + }); + // Mutate node with additional island attributes + addClientOnlyMetadata(node, matchedImport, resolvedPath); + } else { + // Add this component to the metadata + metadata.hydratedComponents.push({ + exportName: '*', + specifier: tagName, + resolvedPath, + }); + // Mutate node with additional island attributes + addClientMetadata(node, matchedImport, resolvedPath); + } + }); + + // Attach final metadata here, which can later be retrieved by `getAstroMetadata` + file.data.__astroMetadata = metadata; + }; +}; + +export function getAstroMetadata(file: VFile) { + return file.data.__astroMetadata as PluginMetadata['astro'] | undefined; +} + +type ImportSpecifier = { local: string; imported: string }; + +/** + * ``` + * import Foo from './Foo.jsx' + * import { Bar } from './Bar.jsx' + * import { Baz as Wiz } from './Bar.jsx' + * import * as Waz from './BaWazz.jsx' + * + * // => Map { + * // "./Foo.jsx" => Set { { local: "Foo", imported: "default" } }, + * // "./Bar.jsx" => Set { + * // { local: "Bar", imported: "Bar" } + * // { local: "Wiz", imported: "Baz" }, + * // }, + * // "./Waz.jsx" => Set { { local: "Waz", imported: "*" } }, + * // } + * ``` + */ +function parseImports(children: RootContent[]) { + // Map of import source to its imported specifiers + const imports = new Map>(); + + for (const child of children) { + if (child.type !== 'mdxjsEsm') continue; + + const body = child.data?.estree?.body; + if (!body) continue; + + for (const ast of body) { + if (ast.type !== 'ImportDeclaration') continue; + + const source = ast.source.value as string; + const specs: ImportSpecifier[] = ast.specifiers.map((spec) => { + switch (spec.type) { + case 'ImportDefaultSpecifier': + return { local: spec.local.name, imported: 'default' }; + case 'ImportNamespaceSpecifier': + return { local: spec.local.name, imported: '*' }; + case 'ImportSpecifier': + return { local: spec.local.name, imported: spec.imported.name }; + default: + throw new Error('Unknown import declaration specifier: ' + spec); + } + }); + + // Get specifiers set from source or initialize a new one + let specSet = imports.get(source); + if (!specSet) { + specSet = new Set(); + imports.set(source, specSet); + } + + for (const spec of specs) { + specSet.add(spec); + } + } + } + + return imports; +} + +function isComponent(tagName: string) { + return ( + (tagName[0] && tagName[0].toLowerCase() !== tagName[0]) || + tagName.includes('.') || + /[^a-zA-Z]/.test(tagName[0]) + ); +} + +function hasClientDirective(node: MdxJsxFlowElementHast | MdxJsxTextElementHast) { + return node.attributes.some( + (attr) => attr.type === 'mdxJsxAttribute' && attr.name.startsWith('client:') + ); +} + +function hasClientOnlyDirective(node: MdxJsxFlowElementHast | MdxJsxTextElementHast) { + return node.attributes.some( + (attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'client:only' + ); +} + +type MatchedImport = { name: string; path: string }; + +/** + * ``` + * import Button from './Button.jsx' + *