Move hydration to the compiler (#1547)
* Move hydration to the compiler * Move extracting url, export to util fn
This commit is contained in:
parent
215f46aa01
commit
b418ae31b8
8 changed files with 132 additions and 90 deletions
|
@ -40,7 +40,7 @@
|
||||||
"test": "mocha --parallel --timeout 15000"
|
"test": "mocha --parallel --timeout 15000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^0.1.9",
|
"@astrojs/compiler": "^0.1.12",
|
||||||
"@astrojs/language-server": "^0.7.16",
|
"@astrojs/language-server": "^0.7.16",
|
||||||
"@astrojs/markdown-remark": "^0.3.1",
|
"@astrojs/markdown-remark": "^0.3.1",
|
||||||
"@astrojs/markdown-support": "0.3.1",
|
"@astrojs/markdown-support": "0.3.1",
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Astro as AstroGlobal } from './astro-file';
|
||||||
import { Renderer } from './astro';
|
import { Renderer } from './astro';
|
||||||
|
|
||||||
export interface SSRMetadata {
|
export interface SSRMetadata {
|
||||||
importedModules: Record<string, any>;
|
|
||||||
renderers: Renderer[];
|
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 { valueToEstree } from 'estree-util-value-to-estree';
|
||||||
import * as astring from 'astring';
|
import * as astring from 'astring';
|
||||||
import shorthash from 'shorthash';
|
import shorthash from 'shorthash';
|
||||||
|
export { createHydrationMap } from './hydration-map.js';
|
||||||
|
|
||||||
const { generate, GENERATOR } = astring;
|
const { generate, GENERATOR } = astring;
|
||||||
|
|
||||||
|
@ -83,17 +84,51 @@ export function createComponent(cb: AstroComponentFactory) {
|
||||||
return cb;
|
return cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractHydrationDirectives(inputProps: Record<string | number, any>): { hydrationDirective: [string, any] | null; props: Record<string | number, any> } {
|
interface ExtractedHydration {
|
||||||
let props: Record<string | number, any> = {};
|
hydration: {
|
||||||
let hydrationDirective: [string, any] | null = null;
|
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)) {
|
for (const [key, value] of Object.entries(inputProps)) {
|
||||||
if (key.startsWith('client:')) {
|
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 {
|
} else {
|
||||||
props[key] = value;
|
extracted.props[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { hydrationDirective, props };
|
return extracted;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HydrateScriptOptions {
|
interface HydrateScriptOptions {
|
||||||
|
@ -157,32 +192,15 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
|
||||||
if (Component == null) {
|
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?`);
|
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 { hydration, props } = extractHydrationDirectives(_props);
|
||||||
// }
|
|
||||||
const { hydrationDirective, props } = extractHydrationDirectives(_props);
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (hydrationDirective) {
|
if (hydration) {
|
||||||
metadata.hydrate = hydrationDirective[0] as AstroComponentMetadata['hydrate'];
|
metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
|
||||||
metadata.hydrateArgs = hydrationDirective[1];
|
metadata.hydrateArgs = hydration.value;
|
||||||
}
|
metadata.componentExport = hydration.componentExport;
|
||||||
|
metadata.componentUrl = hydration.componentUrl;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = null;
|
let renderer = null;
|
||||||
|
@ -207,7 +225,7 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
|
||||||
html = html + polyfillScripts;
|
html = html + polyfillScripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hydrationDirective) {
|
if (!hydration) {
|
||||||
return html.replace(/\<\/?astro-fragment\>/g, '');
|
return html.replace(/\<\/?astro-fragment\>/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,49 +80,6 @@ async function resolveRenderers(viteServer: ViteDevServer, ids: string[]): Promi
|
||||||
return renderers;
|
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. */
|
/** Create the Astro.fetchContent() runtime function. */
|
||||||
function createFetchContentFn(url: URL) {
|
function createFetchContentFn(url: URL) {
|
||||||
const fetchContent = (importMetaGlobResult: Record<string, any>) => {
|
const fetchContent = (importMetaGlobResult: Record<string, any>) => {
|
||||||
|
@ -145,7 +102,7 @@ function createFetchContentFn(url: URL) {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
};
|
};
|
||||||
return fetchContent;
|
return fetchContent;
|
||||||
};
|
}
|
||||||
|
|
||||||
/** use Vite to SSR */
|
/** use Vite to SSR */
|
||||||
export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
|
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
|
// 1.5. load module
|
||||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
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
|
// 2. handle dynamic routes
|
||||||
let params: Params = {};
|
let params: Params = {};
|
||||||
let pageProps: Props = {};
|
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);
|
let html = await renderPage(result, Component, {}, null);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { TransformResult } from '@astrojs/compiler';
|
import type { TransformResult } from '@astrojs/compiler';
|
||||||
import type { Plugin } from 'vite';
|
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 esbuild from 'esbuild';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -28,7 +28,11 @@ export default function astro({ config, devServer }: AstroPluginOptions): Plugin
|
||||||
try {
|
try {
|
||||||
// 1. Transform from `.astro` to valid `.ts`
|
// 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.
|
// 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`
|
// 2. Compile `.ts` to `.js`
|
||||||
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'external', sourcefile: id });
|
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>
|
<html>
|
||||||
<head>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Slotted>
|
<Slotted>
|
||||||
|
|
|
@ -106,10 +106,10 @@
|
||||||
"@algolia/logger-common" "4.10.5"
|
"@algolia/logger-common" "4.10.5"
|
||||||
"@algolia/requester-common" "4.10.5"
|
"@algolia/requester-common" "4.10.5"
|
||||||
|
|
||||||
"@astrojs/compiler@^0.1.9":
|
"@astrojs/compiler@^0.1.12":
|
||||||
version "0.1.9"
|
version "0.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.1.9.tgz#079f8618f4281f07421c961aa2161cb3ab771b4d"
|
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.1.12.tgz#e20fd240044505bea509e62c10f6e2a725e56e7a"
|
||||||
integrity sha512-cFdAsjLUG9q5mwyXexKHZIWSMKacQgLkzQJuHm5kqCd6u+Njl+/iXbxsTAAkNu5L6MNM/kipezfthHppC5TRgA==
|
integrity sha512-oBPK6Rw9K+En1rSB8/15YXF7fC4QhJKwzGaQ8a7AFMga8hJqQwY8rReiwmatAe/6lYNUKJjeLOLO3nE2lGXhzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript "^4.3.5"
|
typescript "^4.3.5"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue