diff --git a/packages/astro/package.json b/packages/astro/package.json index 0ed228e1a..7ed4267a9 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -40,7 +40,7 @@ "test": "mocha --parallel --timeout 15000" }, "dependencies": { - "@astrojs/compiler": "^0.1.9", + "@astrojs/compiler": "^0.1.12", "@astrojs/language-server": "^0.7.16", "@astrojs/markdown-remark": "^0.3.1", "@astrojs/markdown-support": "0.3.1", diff --git a/packages/astro/src/@types/ssr.ts b/packages/astro/src/@types/ssr.ts index 3cb38cfbc..b8c41c25c 100644 --- a/packages/astro/src/@types/ssr.ts +++ b/packages/astro/src/@types/ssr.ts @@ -2,7 +2,6 @@ import { Astro as AstroGlobal } from './astro-file'; import { Renderer } from './astro'; export interface SSRMetadata { - importedModules: Record; renderers: Renderer[]; } diff --git a/packages/astro/src/internal/hydration-map.ts b/packages/astro/src/internal/hydration-map.ts new file mode 100644 index 000000000..c4325a06a --- /dev/null +++ b/packages/astro/src/internal/hydration-map.ts @@ -0,0 +1,66 @@ +import { pathToFileURL } from 'url'; + +interface ModuleInfo { + module: Record, + specifier: string; +} + +interface ComponentMetadata { + componentExport: string; + componentUrl: string +} + +class HydrationMap { + public fileURL: URL; + private metadataCache: Map; + constructor(fileURL: string, public modules: ModuleInfo[], components: any[]) { + this.fileURL = pathToFileURL(fileURL); + this.metadataCache = new Map(); + } + + getPath(Component: any): string | null { + const metadata = this.getComponentMetadata(Component); + return metadata?.componentUrl || null; + } + + getExport(Component: any): string | null { + const metadata = this.getComponentMetadata(Component); + return metadata?.componentExport || null; + } + + private getComponentMetadata(Component: any): ComponentMetadata | null { + if(this.metadataCache.has(Component)) { + return this.metadataCache.get(Component)!; + } + const metadata = this.findComponentMetadata(Component); + this.metadataCache.set(Component, metadata); + return metadata; + } + + private findComponentMetadata(Component: any): ComponentMetadata | null { + const isCustomElement = typeof Component === 'string'; + for (const { module, specifier } of this.modules) { + const id = specifier.startsWith('.') ? new URL(specifier, this.fileURL).pathname : specifier; + for (const [key, value] of Object.entries(module)) { + if(isCustomElement) { + if (key === 'tagName' && Component === value) { + return { + componentExport: key, + componentUrl: id + }; + } + } else if(Component === value) { + return { + componentExport: key, + componentUrl: id + }; + } + } + } + return null; + } +} + +export function createHydrationMap(fileURL: string, modules: ModuleInfo[], components: any[]) { + return new HydrationMap(fileURL, modules, components); +} \ No newline at end of file diff --git a/packages/astro/src/internal/index.ts b/packages/astro/src/internal/index.ts index d7df3e37f..2e41bf415 100644 --- a/packages/astro/src/internal/index.ts +++ b/packages/astro/src/internal/index.ts @@ -4,6 +4,7 @@ import type { SSRResult } from '../@types/ssr'; import { valueToEstree } from 'estree-util-value-to-estree'; import * as astring from 'astring'; import shorthash from 'shorthash'; +export { createHydrationMap } from './hydration-map.js'; const { generate, GENERATOR } = astring; @@ -83,17 +84,51 @@ export function createComponent(cb: AstroComponentFactory) { return cb; } -function extractHydrationDirectives(inputProps: Record): { hydrationDirective: [string, any] | null; props: Record } { - let props: Record = {}; - let hydrationDirective: [string, any] | null = null; +interface ExtractedHydration { + hydration: { + directive: string; + value: string; + componentUrl: string; + componentExport: { value: string; }; + } | null; + props: Record +} + +function extractHydrationDirectives(inputProps: Record): ExtractedHydration { + let extracted: ExtractedHydration = { + hydration: null, + props: {} + }; for (const [key, value] of Object.entries(inputProps)) { if (key.startsWith('client:')) { - hydrationDirective = [key.split(':')[1], value]; + if(!extracted.hydration) { + extracted.hydration = { + directive: '', + value: '', + componentUrl: '', + componentExport: { value: '' } + }; + } + switch(key) { + case 'client:component-path': { + extracted.hydration.componentUrl = value; + break; + } + case 'client:component-export': { + extracted.hydration.componentExport.value = value; + break; + } + default: { + extracted.hydration.directive = key.split(':')[1]; + extracted.hydration.value = value; + break; + } + } } else { - props[key] = value; + extracted.props[key] = value; } } - return { hydrationDirective, props }; + return extracted; } interface HydrateScriptOptions { @@ -157,32 +192,15 @@ export async function renderComponent(result: SSRResult, displayName: string, Co if (Component == null) { throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`); } - // else if (typeof Component === 'string' && !isCustomElementTag(Component)) { - // throw new Error(`Astro is unable to render ${metadata.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`); - // } - const { hydrationDirective, props } = extractHydrationDirectives(_props); + + const { hydration, props } = extractHydrationDirectives(_props); let html = ''; - if (hydrationDirective) { - metadata.hydrate = hydrationDirective[0] as AstroComponentMetadata['hydrate']; - metadata.hydrateArgs = hydrationDirective[1]; - } - - const isCustomElement = typeof Component === 'string'; - for (const [url, exported] of Object.entries(result._metadata.importedModules)) { - for (const [key, value] of Object.entries(exported as any)) { - if(isCustomElement) { - if (key === 'tagName' && Component === value) { - metadata.componentExport = { value: key }; - metadata.componentUrl = url; - break; - } - } else if(Component === value) { - metadata.componentExport = { value: key }; - metadata.componentUrl = url; - break; - } - } + if (hydration) { + metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate']; + metadata.hydrateArgs = hydration.value; + metadata.componentExport = hydration.componentExport; + metadata.componentUrl = hydration.componentUrl; } let renderer = null; @@ -207,7 +225,7 @@ export async function renderComponent(result: SSRResult, displayName: string, Co html = html + polyfillScripts; } - if (!hydrationDirective) { + if (!hydration) { return html.replace(/\<\/?astro-fragment\>/g, ''); } diff --git a/packages/astro/src/runtime/ssr.ts b/packages/astro/src/runtime/ssr.ts index 50dfcb75d..980cfa671 100644 --- a/packages/astro/src/runtime/ssr.ts +++ b/packages/astro/src/runtime/ssr.ts @@ -80,49 +80,6 @@ async function resolveRenderers(viteServer: ViteDevServer, ids: string[]): Promi return renderers; } -async function resolveImportedModules(viteServer: ViteDevServer, file: URL) { - const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(slash(fileURLToPath(file))); // note: for some reason Vite expects forward slashes here for Windows, which `slash()` helps resolve - const modulesByFile = viteServer.moduleGraph.getModulesByFile(url); - if (!modulesByFile) { - return {}; - } - - let importedModules: Record = {}; - const moduleNodes = Array.from(modulesByFile); - // Loop over the importedModules and grab the exports from each one. - // We'll pass these to the shared $$result so renderers can match - // components to their exported identifier and URL - // NOTE: Important that this is parallelized as much as possible! - await Promise.all( - moduleNodes.map((moduleNode) => { - const entries = Array.from(moduleNode.importedModules); - - return Promise.all( - entries.map((entry) => { - // Skip our internal import that every module will have - if (entry.id?.endsWith('astro/dist/internal/index.js')) { - return; - } - - return viteServer.moduleGraph.ensureEntryFromUrl(entry.url).then((mod) => { - if (mod.ssrModule) { - importedModules[mod.url] = mod.ssrModule; - return; - } else { - return viteServer.ssrLoadModule(mod.url).then((result) => { - importedModules[mod.url] = result.ssrModule; - return; - }); - } - }); - }) - ); - }) - ); - - return importedModules; -} - /** Create the Astro.fetchContent() runtime function. */ function createFetchContentFn(url: URL) { const fetchContent = (importMetaGlobResult: Record) => { @@ -145,7 +102,7 @@ function createFetchContentFn(url: URL) { .filter(Boolean); }; return fetchContent; -}; +} /** use Vite to SSR */ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise { @@ -157,10 +114,6 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna // 1.5. load module const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance; - // 1.75. resolve renderers - // important that this happens _after_ ssrLoadModule, otherwise `importedModules` would be empty - const importedModules = await resolveImportedModules(viteServer, filePath); - // 2. handle dynamic routes let params: Params = {}; let pageProps: Props = {}; @@ -230,7 +183,7 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna } }; }, - _metadata: { importedModules, renderers }, + _metadata: { renderers }, }; let html = await renderPage(result, Component, {}, null); diff --git a/packages/astro/src/runtime/vite/plugin-astro.ts b/packages/astro/src/runtime/vite/plugin-astro.ts index 72d8cdf5c..61fb586ad 100644 --- a/packages/astro/src/runtime/vite/plugin-astro.ts +++ b/packages/astro/src/runtime/vite/plugin-astro.ts @@ -1,6 +1,6 @@ import type { TransformResult } from '@astrojs/compiler'; import type { Plugin } from 'vite'; -import type { AstroConfig, Renderer } from '../../@types/astro.js'; +import type { AstroConfig } from '../../@types/astro.js'; import esbuild from 'esbuild'; import fs from 'fs'; @@ -28,7 +28,11 @@ export default function astro({ config, devServer }: AstroPluginOptions): Plugin try { // 1. Transform from `.astro` to valid `.ts` // use `sourcemap: "inline"` so that the sourcemap is included in the "code" result that we pass to esbuild. - tsResult = await transform(source, { sourcefile: id, sourcemap: 'both', internalURL: 'astro/internal' }); + tsResult = await transform(source, { + sourcefile: id, + sourcemap: 'both', + internalURL: 'astro/internal' + }); // 2. Compile `.ts` to `.js` const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'external', sourcefile: id }); diff --git a/packages/astro/test/fixtures/astro-slots/src/pages/slottedapi-filled.astro b/packages/astro/test/fixtures/astro-slots/src/pages/slottedapi-filled.astro index 01714e5d1..3bd842100 100644 --- a/packages/astro/test/fixtures/astro-slots/src/pages/slottedapi-filled.astro +++ b/packages/astro/test/fixtures/astro-slots/src/pages/slottedapi-filled.astro @@ -4,8 +4,10 @@ import Slotted from '../components/SlottedAPI.astro'; - - + diff --git a/yarn.lock b/yarn.lock index f1334738c..23e75b30b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,10 +106,10 @@ "@algolia/logger-common" "4.10.5" "@algolia/requester-common" "4.10.5" -"@astrojs/compiler@^0.1.9": - version "0.1.9" - resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.1.9.tgz#079f8618f4281f07421c961aa2161cb3ab771b4d" - integrity sha512-cFdAsjLUG9q5mwyXexKHZIWSMKacQgLkzQJuHm5kqCd6u+Njl+/iXbxsTAAkNu5L6MNM/kipezfthHppC5TRgA== +"@astrojs/compiler@^0.1.12": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.1.12.tgz#e20fd240044505bea509e62c10f6e2a725e56e7a" + integrity sha512-oBPK6Rw9K+En1rSB8/15YXF7fC4QhJKwzGaQ8a7AFMga8hJqQwY8rReiwmatAe/6lYNUKJjeLOLO3nE2lGXhzQ== dependencies: typescript "^4.3.5"