Move hydration to the compiler (#1547)

* Move hydration to the compiler

* Move extracting url, export to util fn
This commit is contained in:
Matthew Phillips 2021-10-13 15:36:21 -04:00 committed by Drew Powers
parent 215f46aa01
commit b418ae31b8
8 changed files with 132 additions and 90 deletions

View file

@ -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",

View file

@ -2,7 +2,6 @@ import { Astro as AstroGlobal } from './astro-file';
import { Renderer } from './astro';
export interface SSRMetadata {
importedModules: Record<string, any>;
renderers: Renderer[];
}

View file

@ -0,0 +1,66 @@
import { pathToFileURL } from 'url';
interface ModuleInfo {
module: Record<string, any>,
specifier: string;
}
interface ComponentMetadata {
componentExport: string;
componentUrl: string
}
class HydrationMap {
public fileURL: URL;
private metadataCache: Map<any, ComponentMetadata | null>;
constructor(fileURL: string, public modules: ModuleInfo[], components: any[]) {
this.fileURL = pathToFileURL(fileURL);
this.metadataCache = new Map<any, ComponentMetadata | null>();
}
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);
}

View file

@ -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<string | number, any>): { hydrationDirective: [string, any] | null; props: Record<string | number, any> } {
let props: Record<string | number, any> = {};
let hydrationDirective: [string, any] | null = null;
interface ExtractedHydration {
hydration: {
directive: string;
value: string;
componentUrl: string;
componentExport: { value: string; };
} | null;
props: Record<string | number, any>
}
function extractHydrationDirectives(inputProps: Record<string | number, any>): 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, '');
}

View file

@ -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<string, any> = {};
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<string, any>) => {
@ -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<string> {
@ -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);

View file

@ -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 });

View file

@ -4,8 +4,10 @@ import Slotted from '../components/SlottedAPI.astro';
<html>
<head>
<!-- Test Astro.slots behavior. -->
<!-- IDs will exist because the slots are filled -->
<!--
Test Astro.slots behavior.
IDs will exist because the slots are filled
-->
</head>
<body>
<Slotted>

View file

@ -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"