Move hydration to the compiler (#1547)
* Move hydration to the compiler * Move extracting url, export to util fn
This commit is contained in:
parent
e342273d85
commit
35741b6033
8 changed files with 132 additions and 90 deletions
|
@ -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",
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Astro as AstroGlobal } from './astro-file';
|
|||
import { Renderer } from './astro';
|
||||
|
||||
export interface SSRMetadata {
|
||||
importedModules: Record<string, any>;
|
||||
renderers: Renderer[];
|
||||
}
|
||||
|
||||
|
|
66
packages/astro/src/internal/hydration-map.ts
Normal file
66
packages/astro/src/internal/hydration-map.ts
Normal 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);
|
||||
}
|
|
@ -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, '');
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in a new issue