Skip to content

Commit

Permalink
Support re-exporting astro components containing client components (w…
Browse files Browse the repository at this point in the history
…ithastro#3625)

* Support re-exporting astro components containing client components

* Include metadata for markdown too

* Fix ssr, probably

* Inject post-build

* Remove tagName custom element test

* Allows using the constructor for lit elements

* Fix hoisted script scanning

* Pass through plugin context

* Get edge functions working in the edge tests

* Fix types for the edge function integration

* Upgrade the compiler

* Upgrade compiler version

* Better release notes for lit

* Update .changeset/unlucky-hairs-camp.md

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Properly test that the draft was not rendered

* Prevent from rendering draft posts

* Add a changeset about the build perf improvement.

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
  • Loading branch information
matthewp and natemoo-re authored Jun 21, 2022
1 parent 099c4d5 commit 31110e3
Show file tree
Hide file tree
Showing 30 changed files with 361 additions and 233 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^0.15.2",
"@astrojs/compiler": "^0.16.1",
"@astrojs/language-server": "^0.13.4",
"@astrojs/markdown-remark": "^0.11.2",
"@astrojs/prism": "0.4.1",
Expand Down
14 changes: 14 additions & 0 deletions src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ function* throttle(max: number, inPaths: string[]) {
}
}

function shouldSkipDraft(pageModule: ComponentInstance, astroConfig: AstroConfig): boolean {
return (
// Drafts are disabled
!astroConfig.markdown.drafts &&
// This is a draft post
('frontmatter' in pageModule && (pageModule as any).frontmatter.draft === true)
);
}

// Gives back a facadeId that is relative to the root.
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
export function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig): string {
Expand Down Expand Up @@ -124,6 +133,11 @@ async function generatePage(
);
}

if(shouldSkipDraft(pageModule, opts.astroConfig)) {
info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
return;
}

const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
Expand Down
36 changes: 36 additions & 0 deletions src/core/build/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { GetModuleInfo, ModuleInfo, OutputChunk } from 'rollup';
import { resolvedPagesVirtualModuleId } from '../app/index.js';

// This walks up the dependency graph and yields out each ModuleInfo object.
export function* walkParentInfos(
id: string,
ctx: { getModuleInfo: GetModuleInfo },
seen = new Set<string>()
): Generator<ModuleInfo, void, unknown> {
seen.add(id);
const info = ctx.getModuleInfo(id);
if (info) {
yield info;
}
const importers = (info?.importers || []).concat(info?.dynamicImporters || []);
for (const imp of importers) {
if (seen.has(imp)) {
continue;
}
yield* walkParentInfos(imp, ctx, seen);
}
}

// This function walks the dependency graph, going up until it finds a page component.
// This could be a .astro page or a .md page.
export function* getTopLevelPages(
id: string,
ctx: { getModuleInfo: GetModuleInfo }
): Generator<string, void, unknown> {
for (const info of walkParentInfos(id, ctx)) {
const importers = (info?.importers || []).concat(info?.dynamicImporters || []);
if (importers.length <= 2 && importers[0] === resolvedPagesVirtualModuleId) {
yield info.id;
}
}
}
12 changes: 0 additions & 12 deletions src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,6 @@ class AstroBuilder {
ssr: isBuildingToSSR(this.config),
});

// Filter pages by using conditions based on their frontmatter.
Object.entries(allPages).forEach(([page, data]) => {
if ('frontmatter' in data.preload[1]) {
// TODO: add better type inference to data.preload[1]
const frontmatter = (data.preload[1] as any).frontmatter;
if (Boolean(frontmatter.draft) && !this.config.markdown.drafts) {
debug('build', timerMessage(`Skipping draft page ${page}`, this.timer.loadStart));
delete allPages[page];
}
}
});

debug('build', timerMessage('All pages loaded', this.timer.loadStart));

// The names of each pages
Expand Down
28 changes: 27 additions & 1 deletion src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RenderedChunk } from 'rollup';
import type { OutputChunk, RenderedChunk } from 'rollup';
import type { PageBuildData, ViteID } from './types';

import { prependForwardSlash } from '../path.js';
Expand Down Expand Up @@ -31,6 +31,27 @@ export interface BuildInternals {
* A map for page-specific information by a client:only component
*/
pagesByClientOnly: Map<string, Set<PageBuildData>>;

/**
* A list of hydrated components that are discovered during the SSR build
* These will be used as the top-level entrypoints for the client build.
*/
discoveredHydratedComponents: Set<string>;
/**
* A list of client:only components that are discovered during the SSR build
* These will be used as the top-level entrypoints for the client build.
*/
discoveredClientOnlyComponents: Set<string>;
/**
* A list of hoisted scripts that are discovered during the SSR build
* These will be used as the top-level entrypoints for the client build.
*/
discoveredScripts: Set<string>;

// A list of all static files created during the build. Used for SSR.
staticFiles: Set<string>;
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
ssrEntryChunk?: OutputChunk;
}

/**
Expand Down Expand Up @@ -64,6 +85,11 @@ export function createBuildInternals(): BuildInternals {
pagesByComponent: new Map(),
pagesByViteID: new Map(),
pagesByClientOnly: new Map(),

discoveredHydratedComponents: new Set(),
discoveredClientOnlyComponents: new Set(),
discoveredScripts: new Set(),
staticFiles: new Set(),
};
}

Expand Down
41 changes: 12 additions & 29 deletions src/core/build/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,30 +71,18 @@ export async function collectPagesData(
css: new Set(),
hoistedScript: undefined,
scripts: new Set(),
preload: await ssrPreload({
astroConfig,
filePath: new URL(`./${route.component}`, astroConfig.root),
viteServer,
})
.then((routes) => {
clearInterval(routeCollectionLogTimeout);
if (buildMode === 'static') {
const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
debug(
'build',
`├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.yellow(html)}`
);
} else {
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}`);
}
return routes;
})
.catch((err) => {
clearInterval(routeCollectionLogTimeout);
debug('build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`);
throw err;
}),
};

clearInterval(routeCollectionLogTimeout);
if (buildMode === 'static') {
const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
debug(
'build',
`├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.yellow(html)}`
);
} else {
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}`);
}
continue;
}
// dynamic route:
Expand Down Expand Up @@ -144,12 +132,7 @@ export async function collectPagesData(
moduleSpecifier: '',
css: new Set(),
hoistedScript: undefined,
scripts: new Set(),
preload: await ssrPreload({
astroConfig,
filePath: new URL(`./${route.component}`, astroConfig.root),
viteServer,
}),
scripts: new Set()
};
}

Expand Down
87 changes: 17 additions & 70 deletions src/core/build/static-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import * as vite from 'vite';
import {
BuildInternals,
createBuildInternals,
trackClientOnlyPageDatas,
} from '../../core/build/internal.js';
import { prependForwardSlash } from '../../core/path.js';
import { emptyDir, removeDir } from '../../core/util.js';
Expand All @@ -23,24 +22,21 @@ import { getTimeStat } from './util.js';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
import { vitePluginInternals } from './vite-plugin-internals.js';
import { vitePluginPages } from './vite-plugin-pages.js';
import { vitePluginSSR } from './vite-plugin-ssr.js';
import { vitePluginSSR, injectManifest } from './vite-plugin-ssr.js';
import { vitePluginAnalyzer } from './vite-plugin-analyzer.js';

export async function staticBuild(opts: StaticBuildOptions) {
const { allPages, astroConfig } = opts;

// The pages to be built for rendering purposes.
const pageInput = new Set<string>();

// The JavaScript entrypoints.
const jsInput = new Set<string>();

// A map of each page .astro file, to the PageBuildData which contains information
// about that page, such as its paths.
const facadeIdToPageDataMap = new Map<string, PageBuildData>();

// Build internals needed by the CSS plugin
const internals = createBuildInternals();
const uniqueHoistedIds = new Map<string, string>();

const timer: Record<string, number> = {};

Expand All @@ -53,66 +49,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Track the page data in internals
trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);

if (pageData.route.type === 'page') {
const [renderers, mod] = pageData.preload;
const metadata = mod.$$metadata;

const topLevelImports = new Set([
// The client path for each renderer
...renderers
.filter((renderer) => !!renderer.clientEntrypoint)
.map((renderer) => renderer.clientEntrypoint!),
]);

if (metadata) {
// Any component that gets hydrated
// 'components/Counter.jsx'
// { 'components/Counter.jsx': 'counter.hash.js' }
for (const hydratedComponentPath of metadata.hydratedComponentPaths()) {
topLevelImports.add(hydratedComponentPath);
}

// Track client:only usage so we can map their CSS back to the Page they are used in.
const clientOnlys = Array.from(metadata.clientOnlyComponentPaths());
trackClientOnlyPageDatas(internals, pageData, clientOnlys);

// Client-only components
for (const clientOnly of clientOnlys) {
topLevelImports.add(clientOnly);
}

// Add hoisted scripts
const hoistedScripts = new Set(metadata.hoistedScriptPaths());
if (hoistedScripts.size) {
const uniqueHoistedId = JSON.stringify(Array.from(hoistedScripts).sort());
let moduleId: string;

// If we're already tracking this set of hoisted scripts, get the unique id
if (uniqueHoistedIds.has(uniqueHoistedId)) {
moduleId = uniqueHoistedIds.get(uniqueHoistedId)!;
} else {
// Otherwise, create a unique id for this set of hoisted scripts
moduleId = `/astro/hoisted.js?q=${uniqueHoistedIds.size}`;
uniqueHoistedIds.set(uniqueHoistedId, moduleId);
}
topLevelImports.add(moduleId);

// Make sure to track that this page uses this set of hoisted scripts
if (internals.hoistedScriptIdToPagesMap.has(moduleId)) {
const pages = internals.hoistedScriptIdToPagesMap.get(moduleId);
pages!.add(astroModuleId);
} else {
internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId]));
internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts);
}
}
}

for (const specifier of topLevelImports) {
jsInput.add(specifier);
}
}

pageInput.add(astroModuleId);
facadeIdToPageDataMap.set(fileURLToPath(astroModuleURL), pageData);
}
Expand All @@ -122,10 +58,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
// condition, so we are doing it ourselves
emptyDir(astroConfig.outDir, new Set('.git'));

timer.clientBuild = performance.now();
// Run client build first, so the assets can be fed into the SSR rendered version.
await clientBuild(opts, internals, jsInput);

// Build your project (SSR application code, assets, client JS, etc.)
timer.ssr = performance.now();
info(
Expand All @@ -138,6 +70,17 @@ export async function staticBuild(opts: StaticBuildOptions) {
const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput;
info(opts.logging, 'build', dim(`Completed in ${getTimeStat(timer.ssr, performance.now())}.`));

const clientInput = new Set<string>([
...internals.discoveredHydratedComponents,
...internals.discoveredClientOnlyComponents,
...astroConfig._ctx.renderers.map(r => r.clientEntrypoint).filter(a => a) as string[],
...internals.discoveredScripts,
]);

// Run client build first, so the assets can be fed into the SSR rendered version.
timer.clientBuild = performance.now();
await clientBuild(opts, internals, clientInput);

timer.generate = performance.now();
if (opts.buildConfig.staticMode) {
try {
Expand All @@ -146,6 +89,9 @@ export async function staticBuild(opts: StaticBuildOptions) {
await cleanSsrOutput(opts);
}
} else {
// Inject the manifest
await injectManifest(opts, internals)

info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
await ssrMoveAssets(opts);
}
Expand Down Expand Up @@ -198,6 +144,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
// SSR needs to be last
isBuildingToSSR(opts.astroConfig) &&
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
vitePluginAnalyzer(opts.astroConfig, internals)
],
publicDir: ssr ? false : viteConfig.publicDir,
root: viteConfig.root,
Expand Down
2 changes: 0 additions & 2 deletions src/core/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
} from '../../@types/astro';
import type { ViteConfigWithSSR } from '../create-vite';
import type { LogOptions } from '../logger/core';
import type { ComponentPreload } from '../render/dev/index';
import type { RouteCache } from '../render/route-cache';

export type ComponentPath = string;
Expand All @@ -17,7 +16,6 @@ export type ViteID = string;
export interface PageBuildData {
component: ComponentPath;
paths: string[];
preload: ComponentPreload;
route: RouteData;
moduleSpecifier: string;
css: Set<string>;
Expand Down
Loading

0 comments on commit 31110e3

Please sign in to comment.