-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support re-exporting astro components containing client components #3625
Changes from 19 commits
00a2309
e19994a
599447c
c562eeb
6fa2868
b21c957
e171202
ebfd5cf
6094ed3
243f7ae
0c3a85f
4e4ff19
7f7a82b
04288d4
06c2d75
6ca7a81
79b5118
e47dfb5
2dc17d0
1476acc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
'@astrojs/lit': minor | ||
--- | ||
|
||
Conform to Constructor based rendering | ||
|
||
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. | ||
|
||
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. |
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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,30 +71,18 @@ export async function collectPagesData( | |
css: new Set(), | ||
hoistedScript: undefined, | ||
scripts: new Set(), | ||
preload: await ssrPreload({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This removes the "preload" step which was the expensive part of the build that we no longer need to do. Metadata is extracted via the compiler instead. |
||
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: | ||
|
@@ -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() | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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> = {}; | ||
|
||
|
@@ -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); | ||
} | ||
|
@@ -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( | ||
|
@@ -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, | ||
]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are all of the client pieces to build; hydrated and client:only components, renderers, and hoisted scripts. |
||
|
||
// 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 { | ||
|
@@ -146,6 +89,9 @@ export async function staticBuild(opts: StaticBuildOptions) { | |
await cleanSsrOutput(opts); | ||
} | ||
} else { | ||
// Inject the manifest | ||
await injectManifest(opts, internals) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The SSR manifest is injected after both builds, so that we can build a full manifest containing all of the asset information. |
||
|
||
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`); | ||
await ssrMoveAssets(opts); | ||
} | ||
|
@@ -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, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this break
drafts
support? Didn't see where this was handled elsewhere.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I was thinking this was handled in the markdown plugin but I don't see the code for it. There are tests which are passing though. Will investigate more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test was just wrong, so this is broken in this branch. Will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 2dc17d0