Work on removing vite-postprocess

This commit is contained in:
Matthew Phillips 2021-12-20 16:03:57 -05:00
parent e07c1cbd7e
commit d22734c6e8
11 changed files with 283 additions and 145 deletions

View file

@ -0,0 +1,24 @@
<template>
<div id="vue" class="counter">
<button @click="subtract()">-</button>
<pre>{{ count }}</pre>
<button @click="add()">+</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0)
const add = () => count.value = count.value + 1;
const subtract = () => count.value = count.value - 1;
return {
count,
add,
subtract
}
}
}
</script>

View file

@ -2,6 +2,7 @@
import imgUrl from '../images/penguin.jpg';
import grayscaleUrl from '../images/random.jpg?grayscale=true';
import Greeting from '../components/Greeting.vue';
import Counter from '../components/Counter.vue';
---
<html>
@ -28,5 +29,10 @@ import Greeting from '../components/Greeting.vue';
<h1>ImageTools</h1>
<img src={grayscaleUrl} />
</section>
<section>
<h1>Hydrated component</h1>
<Counter client:idle />
</section>
</body>
</html>

View file

@ -371,5 +371,6 @@ export interface SSRResult {
scripts: Set<SSRElement>;
links: Set<SSRElement>;
createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
resolve: (s: string) => Promise<string>;
_metadata: SSRMetadata;
}

View file

@ -14,6 +14,8 @@ export interface BuildInternals {
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
facadeIdToAssetsMap: Map<string, string[]>;
entrySpecifierToBundleMap: Map<string, string>;
}
/**
@ -41,5 +43,6 @@ export function createBuildInternals(): BuildInternals {
astroStyleMap,
astroPageStyleMap,
facadeIdToAssetsMap,
entrySpecifierToBundleMap: new Map<string, string>(),
};
}

View file

@ -1,6 +1,6 @@
import type { OutputChunk, PreRenderedChunk, RollupOutput } from 'rollup';
import type { Plugin as VitePlugin } from '../vite';
import type { AstroConfig, RouteCache } from '../../@types/astro';
import type { AstroConfig, RouteCache, SSRElement } from '../../@types/astro';
import type { AllPagesData } from './types';
import type { LogOptions } from '../logger';
import type { ViteConfigWithSSR } from '../create-vite';
@ -14,7 +14,9 @@ import vite from '../vite.js';
import { debug, info, error } from '../../core/logger.js';
import { createBuildInternals } from '../../core/build/internal.js';
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
import { renderComponent, getParamsAndProps } from '../ssr/index.js';
import { getParamsAndProps } from '../ssr/index.js';
import { createResult } from '../ssr/result.js';
import { renderPage } from '../../runtime/server/index.js';
export interface StaticBuildOptions {
allPages: AllPagesData;
@ -38,10 +40,17 @@ export async function staticBuild(opts: StaticBuildOptions) {
for (const [component, pageData] of Object.entries(allPages)) {
const [renderers, mod] = pageData.preload;
// Hydrated components are statically identified.
for (const path of mod.$$metadata.getAllHydratedComponentPaths()) {
// Note that this part is not yet implemented in the static build.
//jsInput.add(path);
const topLevelImports = new Set([
// Any component that gets hydrated
...mod.$$metadata.hydratedComponentPaths(),
// Any hydration directive like astro/client/idle.js
...mod.$$metadata.hydrationDirectiveSpecifiers(),
// The client path for each renderer
...renderers.filter(renderer => !!renderer.source).map(renderer => renderer.source!),
]);
for(const specifier of topLevelImports) {
jsInput.add(specifier);
}
let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname;
@ -79,7 +88,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
target: 'es2020', // must match an esbuild target
},
plugins: [
vitePluginNewBuild(),
vitePluginNewBuild(input, internals),
rollupPluginAstroBuildCSS({
internals,
}),
@ -124,6 +133,7 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
linkIds,
Component,
};
@ -136,13 +146,14 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
interface GeneratePathOptions {
pageData: PageBuildData;
internals: BuildInternals;
linkIds: string[];
Component: AstroComponentFactory;
}
async function generatePath(path: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
const { astroConfig, logging, origin, routeCache } = opts;
const { Component, linkIds, pageData } = gopts;
const { Component, internals, linkIds, pageData } = gopts;
const [renderers, mod] = pageData.preload;
@ -151,14 +162,33 @@ async function generatePath(path: string, opts: StaticBuildOptions, gopts: Gener
route: pageData.route,
routeCache,
logging,
pathname: path,
pathname,
mod,
});
info(logging, 'generate', `Generating: ${path}`);
info(logging, 'generate', `Generating: ${pathname}`);
const html = await renderComponent(renderers, Component, astroConfig, path, origin, params, pageProps, linkIds);
const outFolder = new URL('.' + path + '/', astroConfig.dist);
const result = createResult({ astroConfig, origin, params, pathname, renderers });
result.links = new Set<SSRElement>(
linkIds.map((href) => ({
props: {
rel: 'stylesheet',
href,
},
children: '',
}))
);
result.resolve = async (specifier: string) => {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
if(typeof hashedFilePath !== 'string') {
throw new Error(`Cannot find the built path for ${specifier}`);
}
console.log("WE GOT", hashedFilePath)
return hashedFilePath;
};
let html = await renderPage(result, Component, pageProps, null);
const outFolder = new URL('.' + pathname + '/', astroConfig.dist);
const outFile = new URL('./index.html', outFolder);
await fs.promises.mkdir(outFolder, { recursive: true });
await fs.promises.writeFile(outFile, html, 'utf-8');
@ -167,7 +197,7 @@ async function generatePath(path: string, opts: StaticBuildOptions, gopts: Gener
}
}
export function vitePluginNewBuild(): VitePlugin {
export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals): VitePlugin {
return {
name: '@astro/rollup-plugin-new-build',
@ -191,5 +221,26 @@ export function vitePluginNewBuild(): VitePlugin {
});
return outputOptions;
},
async generateBundle(_options, bundle) {
const promises = [];
const mapping = new Map<string, string>();
for(const specifier of input) {
promises.push(this.resolve(specifier).then(result => {
if(result) {
mapping.set(result.id, specifier);
}
}));
}
await Promise.all(promises);
for(const [, chunk] of Object.entries(bundle)) {
if(chunk.type === 'chunk' && chunk.facadeModuleId && mapping.has(chunk.facadeModuleId)) {
const specifier = mapping.get(chunk.facadeModuleId)!;
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
}
}
console.log(internals.entrySpecifierToBundleMap);
}
};
}

View file

@ -2,8 +2,6 @@ import type { BuildResult } from 'esbuild';
import type vite from '../vite';
import type {
AstroConfig,
AstroGlobal,
AstroGlobalPartial,
ComponentInstance,
GetStaticPathsResult,
Params,
@ -14,7 +12,6 @@ import type {
RuntimeMode,
SSRElement,
SSRError,
SSRResult,
} from '../../@types/astro';
import type { LogOptions } from '../logger';
import type { AstroComponentFactory } from '../../runtime/server/index';
@ -23,12 +20,13 @@ import eol from 'eol';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { renderPage, renderSlot } from '../../runtime/server/index.js';
import { canonicalURL as getCanonicalURL, codeFrame, resolveDependency } from '../util.js';
import { renderPage } from '../../runtime/server/index.js';
import { codeFrame, resolveDependency } from '../util.js';
import { getStylesForURL } from './css.js';
import { injectTags } from './html.js';
import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
import { createResult } from './result.js';
const svelteStylesRE = /svelte\?svelte&type=style/;
@ -139,6 +137,7 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions)
return [renderers, mod];
}
// TODO REMOVE
export async function renderComponent(
renderers: Renderer[],
Component: AstroComponentFactory,
@ -149,7 +148,8 @@ export async function renderComponent(
pageProps: Props,
links: string[] = []
): Promise<string> {
const _links = new Set<SSRElement>(
const result = createResult({ astroConfig, origin, params, pathname, renderers });
result.links = new Set<SSRElement>(
links.map((href) => ({
props: {
rel: 'stylesheet',
@ -158,50 +158,6 @@ export async function renderComponent(
children: '',
}))
);
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
links: _links,
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
return {
__proto__: astroGlobal,
props,
request: {
canonicalURL,
params,
url,
},
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
// This is used for <Markdown> but shouldn't be used publicly
privateRenderSlotDoNotUse(slotName: string) {
return renderSlot(result, slots ? slots[slotName] : null);
},
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
let mdRender = astroConfig.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(mdRender)) {
renderOpts = mdRender[1];
mdRender = mdRender[0];
}
if (typeof mdRender === 'string') {
({ default: mdRender } = await import(mdRender));
}
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
_metadata: {
renderers,
pathname,
experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
},
};
let html = await renderPage(result, Component, pageProps, null);
@ -292,57 +248,10 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
links: new Set<SSRElement>(),
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
return {
__proto__: astroGlobal,
props,
request: {
canonicalURL,
params,
url,
},
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
// This is used for <Markdown> but shouldn't be used publicly
privateRenderSlotDoNotUse(slotName: string) {
return renderSlot(result, slots ? slots[slotName] : null);
},
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
let mdRender = astroConfig.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(mdRender)) {
renderOpts = mdRender[1];
mdRender = mdRender[0];
}
// ['rehype-toc', opts]
if (typeof mdRender === 'string') {
({ default: mdRender } = await import(mdRender));
}
// [import('rehype-toc'), opts]
else if (mdRender instanceof Promise) {
({ default: mdRender } = await mdRender);
}
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
_metadata: {
renderers,
pathname,
experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
},
const result = createResult({ astroConfig, origin, params, pathname, renderers });
result.resolve = async (s: string) => {
const [, path] = await viteServer.moduleGraph.resolveUrl(s);
return path;
};
let html = await renderPage(result, Component, pageProps, null);
@ -389,7 +298,8 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
// run transformIndexHtml() in dev to run Vite dev transformations
if (mode === 'development') {
const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
console.log("TRANFORM", relativeURL, html);
//html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
}
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)

View file

@ -0,0 +1,83 @@
import type {
AstroConfig,
AstroGlobal,
AstroGlobalPartial,
Params,
Renderer,
SSRElement,
SSRResult,
} from '../../@types/astro';
import { canonicalURL as getCanonicalURL } from '../util.js';
import { renderSlot } from '../../runtime/server/index.js';
export interface CreateResultArgs {
astroConfig: AstroConfig;
origin: string;
params: Params;
pathname: string;
renderers: Renderer[];
}
export function createResult(args: CreateResultArgs): SSRResult {
const { astroConfig, origin, params, pathname, renderers } = args;
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
links: new Set<SSRElement>(),
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
return {
__proto__: astroGlobal,
props,
request: {
canonicalURL,
params,
url,
},
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
// This is used for <Markdown> but shouldn't be used publicly
privateRenderSlotDoNotUse(slotName: string) {
return renderSlot(result, slots ? slots[slotName] : null);
},
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
let mdRender = astroConfig.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(mdRender)) {
renderOpts = mdRender[1];
mdRender = mdRender[0];
}
// ['rehype-toc', opts]
if (typeof mdRender === 'string') {
({ default: mdRender } = await import(mdRender));
}
// [import('rehype-toc'), opts]
else if (mdRender instanceof Promise) {
({ default: mdRender } = await mdRender);
}
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
// This is a stub and will be implemented by dev and build.
async resolve(s: string): Promise<string> {
return '';
},
_metadata: {
renderers,
pathname,
experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
},
};
return result;
}

View file

@ -1,8 +1,8 @@
import type { AstroComponentMetadata } from '../../@types/astro';
import type { SSRElement } from '../../@types/astro';
import type { SSRElement, SSRResult } from '../../@types/astro';
import { valueToEstree } from 'estree-util-value-to-estree';
import * as astring from 'astring';
import { serializeListValue } from './util.js';
import { hydrationSpecifier, serializeListValue } from './util.js';
const { generate, GENERATOR } = astring;
@ -69,6 +69,9 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
extracted.hydration.componentExport.value = value;
break;
}
case 'client:component-hydration': {
break;
}
default: {
extracted.hydration.directive = key.split(':')[1];
extracted.hydration.value = value;
@ -98,13 +101,14 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
interface HydrateScriptOptions {
renderer: any;
result: SSRResult;
astroId: string;
props: Record<string | number, any>;
}
/** For hydrated components, generate a <script type="module"> to load the component */
export async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>): Promise<SSRElement> {
const { renderer, astroId, props } = scriptOptions;
const { renderer, result, astroId, props } = scriptOptions;
const { hydrate, componentUrl, componentExport } = metadata;
if (!componentExport) {
@ -117,16 +121,17 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
}
hydrationSource += renderer.source
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${renderer.source}")]);
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${await result.resolve(renderer.source)}")]);
return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children);
`
: `await import("${componentUrl}");
return () => {};
`;
const hydrationScript = {
props: { type: 'module', 'data-astro-component-hydration': true },
children: `import setup from 'astro/client/${hydrate}.js';
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => {
${hydrationSource}
});

View file

@ -249,7 +249,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
// Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head.
// INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point.
result.scripts.add(await generateHydrateScript({ renderer, astroId, props }, metadata as Required<AstroComponentMetadata>));
result.scripts.add(await generateHydrateScript({ renderer, result, astroId, props }, metadata as Required<AstroComponentMetadata>));
return `<astro-root uid="${astroId}">${html ?? ''}</astro-root>`;
}

View file

@ -1,3 +1,5 @@
import { hydrationSpecifier } from './util.js';
interface ModuleInfo {
module: Record<string, any>;
specifier: string;
@ -8,10 +10,27 @@ interface ComponentMetadata {
componentUrl: string;
}
interface CreateMetadataOptions {
modules: ModuleInfo[];
hydratedComponents: any[];
hydrationDirectives: Set<string>;
hoisted: any[];
}
export class Metadata {
public fileURL: URL;
public modules: ModuleInfo[];
public hoisted: any[];
public hydratedComponents: any[];
public hydrationDirectives: Set<string>;
private metadataCache: Map<any, ComponentMetadata | null>;
constructor(fileURL: string, public modules: ModuleInfo[], public hydratedComponents: any[], public hoisted: any[]) {
constructor(fileURL: string, opts: CreateMetadataOptions) {
this.modules = opts.modules;
this.hoisted = opts.hoisted;
this.hydratedComponents = opts.hydratedComponents;
this.hydrationDirectives = opts.hydrationDirectives;
this.fileURL = new URL(fileURL);
this.metadataCache = new Map<any, ComponentMetadata | null>();
}
@ -30,24 +49,50 @@ export class Metadata {
return metadata?.componentExport || null;
}
// Recursively collect all of the hydrated components' paths.
getAllHydratedComponentPaths(): Set<string> {
const paths = new Set<string>();
for (const component of this.hydratedComponents) {
const path = this.getPath(component);
if (path) {
paths.add(path);
}
}
for (const { module: mod } of this.modules) {
if (typeof mod.$$metadata !== 'undefined') {
for (const path of mod.$$metadata.getAllHydratedComponentPaths()) {
paths.add(path);
/**
* Gets the paths of all hydrated components within this component
* and children components.
*/
*hydratedComponentPaths() {
const found = new Set<string>();
for(const metadata of this.deepMetadata()) {
for (const component of metadata.hydratedComponents) {
const path = this.getPath(component);
if(path && !found.has(path)) {
found.add(path);
yield path;
}
}
}
}
/**
* Gets all of the hydration specifiers used within this component.
*/
*hydrationDirectiveSpecifiers() {
for(const directive of this.hydrationDirectives) {
yield hydrationSpecifier(directive);
}
}
private *deepMetadata(): Generator<Metadata, void, unknown> {
// Yield self
yield this;
// Keep a Set of metadata objects so we only yield them out once.
const seen = new Set<Metadata>();
for (const { module: mod } of this.modules) {
if (typeof mod.$$metadata !== 'undefined') {
const md = mod.$$metadata as Metadata;
// Call children deepMetadata() which will yield the child metadata
// and any of its children metadatas
for(const childMetdata of md.deepMetadata()) {
if(!seen.has(childMetdata)) {
seen.add(childMetdata);
yield childMetdata;
}
}
}
}
return paths;
}
private getComponentMetadata(Component: any): ComponentMetadata | null {
@ -83,12 +128,6 @@ export class Metadata {
}
}
interface CreateMetadataOptions {
modules: ModuleInfo[];
hydratedComponents: any[];
hoisted: any[];
}
export function createMetadata(fileURL: string, options: CreateMetadataOptions) {
return new Metadata(fileURL, options.modules, options.hydratedComponents, options.hoisted);
return new Metadata(fileURL, options);
}

View file

@ -1,3 +1,10 @@
function formatList(values: string[]): string {
if (values.length === 1) {
return values[0];
}
return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
}
export function serializeListValue(value: any) {
const hash: Record<string, any> = {};
@ -27,3 +34,12 @@ export function serializeListValue(value: any) {
}
}
}
/**
* Get the import specifier for a given hydration directive.
* @param hydrate The hydration directive such as `idle` or `visible`
* @returns
*/
export function hydrationSpecifier(hydrate: string) {
return `astro/client/${hydrate}.js`;
}