Skip to content

Commit f5afaf2

Browse files
matthewpnatemoo-re
andauthored
Support re-exporting astro components containing client components (#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>
1 parent 411af7a commit f5afaf2

40 files changed

Lines changed: 434 additions & 242 deletions

.changeset/cyan-kids-sleep.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Significantly improved build performance
6+
7+
This change reflects in a significantly improved build performance, especially on larger sites.
8+
9+
With this change Astro is not building everything by statically analyzing `.astro` files. This means it no longer needs to dynamically *run* your code in order to know what JavaScript needs to be built.
10+
11+
With one particular large site we found it to build __32%__ faster.

.changeset/unlucky-hairs-camp.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@astrojs/lit': minor
3+
---
4+
5+
Conform to Constructor based rendering
6+
7+
This changes `@astrojs/lit` to conform to the way rendering happens in all other frameworks. Instead of using the tag name `<my-element client:load>` you use the imported constructor function, `<MyElement client:load>` like you would do with any other framework.
8+
9+
Support for `tag-name` syntax had to be removed due to the fact that it was a runtime feature that was not statically analyzable. To improve build performance, we have removed all runtime based component discovery. Using the imported Constructor name allows Astro to discover what components need to be built and bundled for production without ever running your file.

packages/astro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"test:e2e:match": "playwright test -g"
7979
},
8080
"dependencies": {
81-
"@astrojs/compiler": "^0.15.2",
81+
"@astrojs/compiler": "^0.16.1",
8282
"@astrojs/language-server": "^0.13.4",
8383
"@astrojs/markdown-remark": "^0.11.2",
8484
"@astrojs/prism": "0.4.1",

packages/astro/src/core/build/generate.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ function* throttle(max: number, inPaths: string[]) {
5656
}
5757
}
5858

59+
function shouldSkipDraft(pageModule: ComponentInstance, astroConfig: AstroConfig): boolean {
60+
return (
61+
// Drafts are disabled
62+
!astroConfig.markdown.drafts &&
63+
// This is a draft post
64+
('frontmatter' in pageModule && (pageModule as any).frontmatter.draft === true)
65+
);
66+
}
67+
5968
// Gives back a facadeId that is relative to the root.
6069
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
6170
export function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig): string {
@@ -124,6 +133,11 @@ async function generatePage(
124133
);
125134
}
126135

136+
if(shouldSkipDraft(pageModule, opts.astroConfig)) {
137+
info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
138+
return;
139+
}
140+
127141
const generationOptions: Readonly<GeneratePathOptions> = {
128142
pageData,
129143
internals,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { GetModuleInfo, ModuleInfo, OutputChunk } from 'rollup';
2+
import { resolvedPagesVirtualModuleId } from '../app/index.js';
3+
4+
// This walks up the dependency graph and yields out each ModuleInfo object.
5+
export function* walkParentInfos(
6+
id: string,
7+
ctx: { getModuleInfo: GetModuleInfo },
8+
seen = new Set<string>()
9+
): Generator<ModuleInfo, void, unknown> {
10+
seen.add(id);
11+
const info = ctx.getModuleInfo(id);
12+
if (info) {
13+
yield info;
14+
}
15+
const importers = (info?.importers || []).concat(info?.dynamicImporters || []);
16+
for (const imp of importers) {
17+
if (seen.has(imp)) {
18+
continue;
19+
}
20+
yield* walkParentInfos(imp, ctx, seen);
21+
}
22+
}
23+
24+
// This function walks the dependency graph, going up until it finds a page component.
25+
// This could be a .astro page or a .md page.
26+
export function* getTopLevelPages(
27+
id: string,
28+
ctx: { getModuleInfo: GetModuleInfo }
29+
): Generator<string, void, unknown> {
30+
for (const info of walkParentInfos(id, ctx)) {
31+
const importers = (info?.importers || []).concat(info?.dynamicImporters || []);
32+
if (importers.length <= 2 && importers[0] === resolvedPagesVirtualModuleId) {
33+
yield info.id;
34+
}
35+
}
36+
}

packages/astro/src/core/build/index.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,6 @@ class AstroBuilder {
114114
ssr: isBuildingToSSR(this.config),
115115
});
116116

117-
// Filter pages by using conditions based on their frontmatter.
118-
Object.entries(allPages).forEach(([page, data]) => {
119-
if ('frontmatter' in data.preload[1]) {
120-
// TODO: add better type inference to data.preload[1]
121-
const frontmatter = (data.preload[1] as any).frontmatter;
122-
if (Boolean(frontmatter.draft) && !this.config.markdown.drafts) {
123-
debug('build', timerMessage(`Skipping draft page ${page}`, this.timer.loadStart));
124-
delete allPages[page];
125-
}
126-
}
127-
});
128-
129117
debug('build', timerMessage('All pages loaded', this.timer.loadStart));
130118

131119
// The names of each pages

packages/astro/src/core/build/internal.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { RenderedChunk } from 'rollup';
1+
import type { OutputChunk, RenderedChunk } from 'rollup';
22
import type { PageBuildData, ViteID } from './types';
33

44
import { prependForwardSlash } from '../path.js';
@@ -31,6 +31,27 @@ export interface BuildInternals {
3131
* A map for page-specific information by a client:only component
3232
*/
3333
pagesByClientOnly: Map<string, Set<PageBuildData>>;
34+
35+
/**
36+
* A list of hydrated components that are discovered during the SSR build
37+
* These will be used as the top-level entrypoints for the client build.
38+
*/
39+
discoveredHydratedComponents: Set<string>;
40+
/**
41+
* A list of client:only components that are discovered during the SSR build
42+
* These will be used as the top-level entrypoints for the client build.
43+
*/
44+
discoveredClientOnlyComponents: Set<string>;
45+
/**
46+
* A list of hoisted scripts that are discovered during the SSR build
47+
* These will be used as the top-level entrypoints for the client build.
48+
*/
49+
discoveredScripts: Set<string>;
50+
51+
// A list of all static files created during the build. Used for SSR.
52+
staticFiles: Set<string>;
53+
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
54+
ssrEntryChunk?: OutputChunk;
3455
}
3556

3657
/**
@@ -64,6 +85,11 @@ export function createBuildInternals(): BuildInternals {
6485
pagesByComponent: new Map(),
6586
pagesByViteID: new Map(),
6687
pagesByClientOnly: new Map(),
88+
89+
discoveredHydratedComponents: new Set(),
90+
discoveredClientOnlyComponents: new Set(),
91+
discoveredScripts: new Set(),
92+
staticFiles: new Set(),
6793
};
6894
}
6995

packages/astro/src/core/build/page-data.ts

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,18 @@ export async function collectPagesData(
7171
css: new Set(),
7272
hoistedScript: undefined,
7373
scripts: new Set(),
74-
preload: await ssrPreload({
75-
astroConfig,
76-
filePath: new URL(`./${route.component}`, astroConfig.root),
77-
viteServer,
78-
})
79-
.then((routes) => {
80-
clearInterval(routeCollectionLogTimeout);
81-
if (buildMode === 'static') {
82-
const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
83-
debug(
84-
'build',
85-
`├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.yellow(html)}`
86-
);
87-
} else {
88-
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}`);
89-
}
90-
return routes;
91-
})
92-
.catch((err) => {
93-
clearInterval(routeCollectionLogTimeout);
94-
debug('build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`);
95-
throw err;
96-
}),
9774
};
75+
76+
clearInterval(routeCollectionLogTimeout);
77+
if (buildMode === 'static') {
78+
const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
79+
debug(
80+
'build',
81+
`├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.yellow(html)}`
82+
);
83+
} else {
84+
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}`);
85+
}
9886
continue;
9987
}
10088
// dynamic route:
@@ -144,12 +132,7 @@ export async function collectPagesData(
144132
moduleSpecifier: '',
145133
css: new Set(),
146134
hoistedScript: undefined,
147-
scripts: new Set(),
148-
preload: await ssrPreload({
149-
astroConfig,
150-
filePath: new URL(`./${route.component}`, astroConfig.root),
151-
viteServer,
152-
}),
135+
scripts: new Set()
153136
};
154137
}
155138

packages/astro/src/core/build/static-build.ts

Lines changed: 17 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as vite from 'vite';
77
import {
88
BuildInternals,
99
createBuildInternals,
10-
trackClientOnlyPageDatas,
1110
} from '../../core/build/internal.js';
1211
import { prependForwardSlash } from '../../core/path.js';
1312
import { emptyDir, removeDir } from '../../core/util.js';
@@ -23,24 +22,21 @@ import { getTimeStat } from './util.js';
2322
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
2423
import { vitePluginInternals } from './vite-plugin-internals.js';
2524
import { vitePluginPages } from './vite-plugin-pages.js';
26-
import { vitePluginSSR } from './vite-plugin-ssr.js';
25+
import { vitePluginSSR, injectManifest } from './vite-plugin-ssr.js';
26+
import { vitePluginAnalyzer } from './vite-plugin-analyzer.js';
2727

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

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

34-
// The JavaScript entrypoints.
35-
const jsInput = new Set<string>();
36-
3734
// A map of each page .astro file, to the PageBuildData which contains information
3835
// about that page, such as its paths.
3936
const facadeIdToPageDataMap = new Map<string, PageBuildData>();
4037

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

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

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

56-
if (pageData.route.type === 'page') {
57-
const [renderers, mod] = pageData.preload;
58-
const metadata = mod.$$metadata;
59-
60-
const topLevelImports = new Set([
61-
// The client path for each renderer
62-
...renderers
63-
.filter((renderer) => !!renderer.clientEntrypoint)
64-
.map((renderer) => renderer.clientEntrypoint!),
65-
]);
66-
67-
if (metadata) {
68-
// Any component that gets hydrated
69-
// 'components/Counter.jsx'
70-
// { 'components/Counter.jsx': 'counter.hash.js' }
71-
for (const hydratedComponentPath of metadata.hydratedComponentPaths()) {
72-
topLevelImports.add(hydratedComponentPath);
73-
}
74-
75-
// Track client:only usage so we can map their CSS back to the Page they are used in.
76-
const clientOnlys = Array.from(metadata.clientOnlyComponentPaths());
77-
trackClientOnlyPageDatas(internals, pageData, clientOnlys);
78-
79-
// Client-only components
80-
for (const clientOnly of clientOnlys) {
81-
topLevelImports.add(clientOnly);
82-
}
83-
84-
// Add hoisted scripts
85-
const hoistedScripts = new Set(metadata.hoistedScriptPaths());
86-
if (hoistedScripts.size) {
87-
const uniqueHoistedId = JSON.stringify(Array.from(hoistedScripts).sort());
88-
let moduleId: string;
89-
90-
// If we're already tracking this set of hoisted scripts, get the unique id
91-
if (uniqueHoistedIds.has(uniqueHoistedId)) {
92-
moduleId = uniqueHoistedIds.get(uniqueHoistedId)!;
93-
} else {
94-
// Otherwise, create a unique id for this set of hoisted scripts
95-
moduleId = `/astro/hoisted.js?q=${uniqueHoistedIds.size}`;
96-
uniqueHoistedIds.set(uniqueHoistedId, moduleId);
97-
}
98-
topLevelImports.add(moduleId);
99-
100-
// Make sure to track that this page uses this set of hoisted scripts
101-
if (internals.hoistedScriptIdToPagesMap.has(moduleId)) {
102-
const pages = internals.hoistedScriptIdToPagesMap.get(moduleId);
103-
pages!.add(astroModuleId);
104-
} else {
105-
internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId]));
106-
internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts);
107-
}
108-
}
109-
}
110-
111-
for (const specifier of topLevelImports) {
112-
jsInput.add(specifier);
113-
}
114-
}
115-
11652
pageInput.add(astroModuleId);
11753
facadeIdToPageDataMap.set(fileURLToPath(astroModuleURL), pageData);
11854
}
@@ -122,10 +58,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
12258
// condition, so we are doing it ourselves
12359
emptyDir(astroConfig.outDir, new Set('.git'));
12460

125-
timer.clientBuild = performance.now();
126-
// Run client build first, so the assets can be fed into the SSR rendered version.
127-
await clientBuild(opts, internals, jsInput);
128-
12961
// Build your project (SSR application code, assets, client JS, etc.)
13062
timer.ssr = performance.now();
13163
info(
@@ -138,6 +70,17 @@ export async function staticBuild(opts: StaticBuildOptions) {
13870
const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput;
13971
info(opts.logging, 'build', dim(`Completed in ${getTimeStat(timer.ssr, performance.now())}.`));
14072

73+
const clientInput = new Set<string>([
74+
...internals.discoveredHydratedComponents,
75+
...internals.discoveredClientOnlyComponents,
76+
...astroConfig._ctx.renderers.map(r => r.clientEntrypoint).filter(a => a) as string[],
77+
...internals.discoveredScripts,
78+
]);
79+
80+
// Run client build first, so the assets can be fed into the SSR rendered version.
81+
timer.clientBuild = performance.now();
82+
await clientBuild(opts, internals, clientInput);
83+
14184
timer.generate = performance.now();
14285
if (opts.buildConfig.staticMode) {
14386
try {
@@ -146,6 +89,9 @@ export async function staticBuild(opts: StaticBuildOptions) {
14689
await cleanSsrOutput(opts);
14790
}
14891
} else {
92+
// Inject the manifest
93+
await injectManifest(opts, internals)
94+
14995
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
15096
await ssrMoveAssets(opts);
15197
}
@@ -198,6 +144,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
198144
// SSR needs to be last
199145
isBuildingToSSR(opts.astroConfig) &&
200146
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
147+
vitePluginAnalyzer(opts.astroConfig, internals)
201148
],
202149
publicDir: ssr ? false : viteConfig.publicDir,
203150
root: viteConfig.root,

packages/astro/src/core/build/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
} from '../../@types/astro';
99
import type { ViteConfigWithSSR } from '../create-vite';
1010
import type { LogOptions } from '../logger/core';
11-
import type { ComponentPreload } from '../render/dev/index';
1211
import type { RouteCache } from '../render/route-cache';
1312

1413
export type ComponentPath = string;
@@ -17,7 +16,6 @@ export type ViteID = string;
1716
export interface PageBuildData {
1817
component: ComponentPath;
1918
paths: string[];
20-
preload: ComponentPreload;
2119
route: RouteData;
2220
moduleSpecifier: string;
2321
css: Set<string>;

0 commit comments

Comments
 (0)