Add private addPageExtension
hook (#3628)
* feat: add private `addPageExtensions` hook * chore: remove renderer binding Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
9502fbf4a9
commit
8e3e4894c9
15 changed files with 134 additions and 41 deletions
|
@ -726,6 +726,7 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
|
||||||
// that is different from the user-exposed configuration.
|
// that is different from the user-exposed configuration.
|
||||||
// TODO: Create an AstroConfig class to manage this, long-term.
|
// TODO: Create an AstroConfig class to manage this, long-term.
|
||||||
_ctx: {
|
_ctx: {
|
||||||
|
pageExtensions: string[];
|
||||||
injectedRoutes: InjectedRoute[];
|
injectedRoutes: InjectedRoute[];
|
||||||
adapter: AstroAdapter | undefined;
|
adapter: AstroAdapter | undefined;
|
||||||
renderers: AstroRenderer[];
|
renderers: AstroRenderer[];
|
||||||
|
|
|
@ -57,23 +57,30 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const [renderers, mod] = pageData.preload;
|
const [renderers, mod] = pageData.preload;
|
||||||
const metadata = mod.$$metadata;
|
const metadata = mod.$$metadata;
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
const topLevelImports = new Set([
|
const topLevelImports = new Set([
|
||||||
// Any component that gets hydrated
|
|
||||||
// 'components/Counter.jsx'
|
|
||||||
// { 'components/Counter.jsx': 'counter.hash.js' }
|
|
||||||
...metadata.hydratedComponentPaths(),
|
|
||||||
// Client-only components
|
|
||||||
...clientOnlys,
|
|
||||||
// The client path for each renderer
|
// The client path for each renderer
|
||||||
...renderers
|
...renderers
|
||||||
.filter((renderer) => !!renderer.clientEntrypoint)
|
.filter((renderer) => !!renderer.clientEntrypoint)
|
||||||
.map((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
|
// Add hoisted scripts
|
||||||
const hoistedScripts = new Set(metadata.hoistedScriptPaths());
|
const hoistedScripts = new Set(metadata.hoistedScriptPaths());
|
||||||
if (hoistedScripts.size) {
|
if (hoistedScripts.size) {
|
||||||
|
@ -99,6 +106,7 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts);
|
internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const specifier of topLevelImports) {
|
for (const specifier of topLevelImports) {
|
||||||
jsInput.add(specifier);
|
jsInput.add(specifier);
|
||||||
|
|
|
@ -338,7 +338,7 @@ export async function validateConfig(
|
||||||
// First-Pass Validation
|
// First-Pass Validation
|
||||||
const result = {
|
const result = {
|
||||||
...(await AstroConfigRelativeSchema.parseAsync(userConfig)),
|
...(await AstroConfigRelativeSchema.parseAsync(userConfig)),
|
||||||
_ctx: { scripts: [], renderers: [], injectedRoutes: [], adapter: undefined },
|
_ctx: { pageExtensions: [], scripts: [], renderers: [], injectedRoutes: [], adapter: undefined },
|
||||||
};
|
};
|
||||||
// Final-Pass Validation (perform checks that require the full config object)
|
// Final-Pass Validation (perform checks that require the full config object)
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger/core.js';
|
import type { LogOptions } from '../logger/core.js';
|
||||||
|
|
||||||
import { renderHead, renderPage } from '../../runtime/server/index.js';
|
import { renderHead, renderPage, renderComponent } from '../../runtime/server/index.js';
|
||||||
import { getParams } from '../routing/params.js';
|
import { getParams } from '../routing/params.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||||
|
@ -126,8 +126,6 @@ export async function render(
|
||||||
const Component = await mod.default;
|
const Component = await mod.default;
|
||||||
if (!Component)
|
if (!Component)
|
||||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof 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})`);
|
|
||||||
|
|
||||||
const result = createResult({
|
const result = createResult({
|
||||||
links,
|
links,
|
||||||
|
@ -146,7 +144,17 @@ export async function render(
|
||||||
ssr,
|
ssr,
|
||||||
});
|
});
|
||||||
|
|
||||||
let page = await renderPage(result, Component, pageProps, null);
|
let page: Awaited<ReturnType<typeof renderPage>>;
|
||||||
|
if (!Component.isAstroComponentFactory) {
|
||||||
|
const props: Record<string, any> = { ...(pageProps ?? {}), 'server:root': true };
|
||||||
|
const html = await renderComponent(result, Component.name, Component, props, null);
|
||||||
|
page = {
|
||||||
|
type: 'html',
|
||||||
|
html: html.toString()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
page = await renderPage(result, Component, pageProps, null);
|
||||||
|
}
|
||||||
|
|
||||||
if (page.type === 'response') {
|
if (page.type === 'response') {
|
||||||
return page;
|
return page;
|
||||||
|
|
|
@ -165,7 +165,7 @@ export function createRouteManifest(
|
||||||
): ManifestData {
|
): ManifestData {
|
||||||
const components: string[] = [];
|
const components: string[] = [];
|
||||||
const routes: RouteData[] = [];
|
const routes: RouteData[] = [];
|
||||||
const validPageExtensions: Set<string> = new Set(['.astro', '.md']);
|
const validPageExtensions: Set<string> = new Set(['.astro', '.md', ...config._ctx.pageExtensions]);
|
||||||
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
||||||
|
|
||||||
function walk(dir: string, parentSegments: RoutePart[][], parentParams: string[]) {
|
function walk(dir: string, parentSegments: RoutePart[][], parentParams: string[]) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import type { ViteDevServer } from 'vite';
|
import type { ViteDevServer } from 'vite';
|
||||||
import { AstroConfig, AstroRenderer, BuildConfig, RouteData } from '../@types/astro.js';
|
import { AstroConfig, AstroIntegration, AstroRenderer, BuildConfig, RouteData } from '../@types/astro.js';
|
||||||
import ssgAdapter from '../adapter-ssg/index.js';
|
import ssgAdapter from '../adapter-ssg/index.js';
|
||||||
import type { SerializedSSRManifest } from '../core/app/types';
|
import type { SerializedSSRManifest } from '../core/app/types';
|
||||||
import type { PageBuildData } from '../core/build/types';
|
import type { PageBuildData } from '../core/build/types';
|
||||||
|
@ -8,6 +8,8 @@ import { mergeConfig } from '../core/config.js';
|
||||||
import type { ViteConfigWithSSR } from '../core/create-vite.js';
|
import type { ViteConfigWithSSR } from '../core/create-vite.js';
|
||||||
import { isBuildingToSSR } from '../core/util.js';
|
import { isBuildingToSSR } from '../core/util.js';
|
||||||
|
|
||||||
|
type Hooks<Hook extends keyof AstroIntegration['hooks'], Fn = AstroIntegration['hooks'][Hook]> = Fn extends (...args: any) => any ? Parameters<Fn>[0] : never;
|
||||||
|
|
||||||
export async function runHookConfigSetup({
|
export async function runHookConfigSetup({
|
||||||
config: _config,
|
config: _config,
|
||||||
command,
|
command,
|
||||||
|
@ -34,7 +36,7 @@ export async function runHookConfigSetup({
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
if (integration?.hooks?.['astro:config:setup']) {
|
if (integration?.hooks?.['astro:config:setup']) {
|
||||||
await integration.hooks['astro:config:setup']({
|
const hooks: Hooks<'astro:config:setup'> = {
|
||||||
config: updatedConfig,
|
config: updatedConfig,
|
||||||
command,
|
command,
|
||||||
addRenderer(renderer: AstroRenderer) {
|
addRenderer(renderer: AstroRenderer) {
|
||||||
|
@ -49,7 +51,17 @@ export async function runHookConfigSetup({
|
||||||
injectRoute: (injectRoute) => {
|
injectRoute: (injectRoute) => {
|
||||||
updatedConfig._ctx.injectedRoutes.push(injectRoute);
|
updatedConfig._ctx.injectedRoutes.push(injectRoute);
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
// Semi-private `addPageExtension` hook
|
||||||
|
Object.defineProperty(hooks, 'addPageExtension', {
|
||||||
|
value: (...input: (string|string[])[]) => {
|
||||||
|
const exts = (input.flat(Infinity) as string[]).map(ext => `.${ext.replace(/^\./, '')}`);
|
||||||
|
updatedConfig._ctx.pageExtensions.push(...exts);
|
||||||
|
},
|
||||||
|
writable: false,
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
await integration.hooks['astro:config:setup'](hooks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return updatedConfig;
|
return updatedConfig;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { serializeListValue } from './util.js';
|
||||||
const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only'];
|
const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only'];
|
||||||
|
|
||||||
interface ExtractedProps {
|
interface ExtractedProps {
|
||||||
|
isPage: boolean;
|
||||||
hydration: {
|
hydration: {
|
||||||
directive: string;
|
directive: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -24,10 +25,16 @@ interface ExtractedProps {
|
||||||
// Finds these special props and removes them from what gets passed into the component.
|
// Finds these special props and removes them from what gets passed into the component.
|
||||||
export function extractDirectives(inputProps: Record<string | number, any>): ExtractedProps {
|
export function extractDirectives(inputProps: Record<string | number, any>): ExtractedProps {
|
||||||
let extracted: ExtractedProps = {
|
let extracted: ExtractedProps = {
|
||||||
|
isPage: false,
|
||||||
hydration: null,
|
hydration: null,
|
||||||
props: {},
|
props: {},
|
||||||
};
|
};
|
||||||
for (const [key, value] of Object.entries(inputProps)) {
|
for (const [key, value] of Object.entries(inputProps)) {
|
||||||
|
if (key.startsWith('server:')) {
|
||||||
|
if (key === 'server:root') {
|
||||||
|
extracted.isPage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (key.startsWith('client:')) {
|
if (key.startsWith('client:')) {
|
||||||
if (!extracted.hydration) {
|
if (!extracted.hydration) {
|
||||||
extracted.hydration = {
|
extracted.hydration = {
|
||||||
|
|
|
@ -181,7 +181,7 @@ export async function renderComponent(
|
||||||
const { renderers } = result._metadata;
|
const { renderers } = result._metadata;
|
||||||
const metadata: AstroComponentMetadata = { displayName };
|
const metadata: AstroComponentMetadata = { displayName };
|
||||||
|
|
||||||
const { hydration, props } = extractDirectives(_props);
|
const { hydration, isPage, props } = extractDirectives(_props);
|
||||||
let html = '';
|
let html = '';
|
||||||
let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
|
let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
|
||||||
let needsDirectiveScript =
|
let needsDirectiveScript =
|
||||||
|
@ -317,6 +317,9 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hydration) {
|
if (!hydration) {
|
||||||
|
if (isPage) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
return markHTMLString(html.replace(/\<\/?astro-fragment\>/g, ''));
|
return markHTMLString(html.replace(/\<\/?astro-fragment\>/g, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6
packages/astro/test/fixtures/integration-add-page-extension/astro.config.mjs
vendored
Normal file
6
packages/astro/test/fixtures/integration-add-page-extension/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineConfig } from 'rollup'
|
||||||
|
import test from './integration.js'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [test()]
|
||||||
|
})
|
10
packages/astro/test/fixtures/integration-add-page-extension/integration.js
vendored
Normal file
10
packages/astro/test/fixtures/integration-add-page-extension/integration.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export default function() {
|
||||||
|
return {
|
||||||
|
name: '@astrojs/test-integration',
|
||||||
|
hooks: {
|
||||||
|
'astro:config:setup': ({ addPageExtension }) => {
|
||||||
|
addPageExtension('.mjs')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
packages/astro/test/fixtures/integration-add-page-extension/package.json
vendored
Normal file
9
packages/astro/test/fixtures/integration-add-page-extension/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@test/integration-add-page-extension",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
1
packages/astro/test/fixtures/integration-add-page-extension/src/components/test.astro
vendored
Normal file
1
packages/astro/test/fixtures/integration-add-page-extension/src/components/test.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>Hello world!</h1>
|
3
packages/astro/test/fixtures/integration-add-page-extension/src/pages/test.mjs
vendored
Normal file
3
packages/astro/test/fixtures/integration-add-page-extension/src/pages/test.mjs
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// Convulted test case, rexport astro file from new `.mjs` page
|
||||||
|
import Test from '../components/test.astro';
|
||||||
|
export default Test;
|
19
packages/astro/test/integration-add-page-extension.test.js
Normal file
19
packages/astro/test/integration-add-page-extension.test.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
|
describe('Integration addPageExtension', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({ root: './fixtures/integration-add-page-extension/' });
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports .mjs files', async () => {
|
||||||
|
const html = await fixture.readFile('/test/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('h1').text()).to.equal('Hello world!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1388,6 +1388,12 @@ importers:
|
||||||
'@fontsource/montserrat': 4.5.11
|
'@fontsource/montserrat': 4.5.11
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/integration-add-page-extension:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/legacy-build:
|
packages/astro/test/fixtures/legacy-build:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/vue': workspace:*
|
'@astrojs/vue': workspace:*
|
||||||
|
|
Loading…
Reference in a new issue