Skip to content
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

feat: hybrid output #6991

Merged
merged 54 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
99c3bfa
update config schema
MoustaphaDev Apr 27, 2023
e4c614d
adapt default route `prerender` value
MoustaphaDev Apr 27, 2023
1b05568
adapt error message for hybrid output
MoustaphaDev Apr 27, 2023
e9408cf
core hybrid output support
MoustaphaDev Apr 28, 2023
ba0c6cd
add JSDocs for hybrid output
MoustaphaDev Apr 28, 2023
06a8f69
dev server hybrid output support
MoustaphaDev Apr 29, 2023
6d509b5
defer hybrid output check
MoustaphaDev Apr 29, 2023
2d7263e
update endpoint request warning
MoustaphaDev Apr 29, 2023
96cffb7
support `output=hybrid` in integrations
MoustaphaDev Apr 29, 2023
70c60c9
put constant variable out of for loop
MoustaphaDev Apr 30, 2023
3a4541d
revert: reapply back ssr plugin in ssr mode
MoustaphaDev Apr 30, 2023
2788683
change `prerender` option default
MoustaphaDev Apr 30, 2023
157c4e0
apply `prerender` by default in hybrid mode
MoustaphaDev Apr 30, 2023
0fa2ded
simplfy conditional
MoustaphaDev Apr 30, 2023
459b9d3
Merge branch 'main' of https://github.com/withastro/astro into mk-fea…
MoustaphaDev May 7, 2023
b3c6e24
update config schema
MoustaphaDev May 10, 2023
21f7a41
add `isHybridOutput` helper
MoustaphaDev May 12, 2023
f18245c
more readable prerender condition
MoustaphaDev May 12, 2023
3496afa
set default prerender value if no export is found
MoustaphaDev May 12, 2023
cc4fd2d
Merge branch 'main' of https://github.com/withastro/astro into mk-fea…
MoustaphaDev May 12, 2023
b44a926
only add `pagesVirtualModuleId` ro rollup input in `output=static`
MoustaphaDev May 12, 2023
b756753
don't export vite plugin
MoustaphaDev May 12, 2023
0b14c61
remove unneeded check
MoustaphaDev May 12, 2023
aaa9896
don't prerender when it shouldn't
MoustaphaDev May 12, 2023
ef2ffb1
extract fallback `prerender` meta
MoustaphaDev May 12, 2023
7ca73e8
pass missing argument to function
MoustaphaDev May 12, 2023
8a64125
test: update cloudflare integration tests
MoustaphaDev May 13, 2023
e941ad7
test: update tests of vercel integration
MoustaphaDev May 13, 2023
5e54411
test: update tests of node integration
MoustaphaDev May 13, 2023
5f7c217
test: update tests of netlify func integration
MoustaphaDev May 13, 2023
7ba6bcb
test: update tests of netlify edge integration
MoustaphaDev May 13, 2023
e2c582a
Merge branch 'main' of https://github.com/withastro/astro into mk-fea…
MoustaphaDev May 13, 2023
5efc41e
throw when `hybrid` mode is malconfigured
MoustaphaDev May 13, 2023
f104d5d
Merge branch 'main' of https://github.com/withastro/astro into mk-fea…
MoustaphaDev May 15, 2023
fb02e34
update node integraiton `output` warning
MoustaphaDev May 15, 2023
109fd60
test(WIP): skip node prerendering tests for now
MoustaphaDev May 15, 2023
2b9a37e
remove non-existant import
MoustaphaDev May 15, 2023
0e82f30
Merge branch 'main' of https://github.com/withastro/astro into mk-fea…
MoustaphaDev May 15, 2023
fa2f92c
test: bring back prerendering tests
MoustaphaDev May 15, 2023
0478cea
remove outdated comments
MoustaphaDev May 15, 2023
ae079eb
test: refactor test to support windows paths
MoustaphaDev May 15, 2023
79fdfd6
remove outdated comments
MoustaphaDev May 15, 2023
cdb5816
apply sarah review
MoustaphaDev May 15, 2023
b818f79
docs: `experiment.hybridOutput` jsodcs
MoustaphaDev May 15, 2023
d5bf324
test: prevent import from being cached
MoustaphaDev May 15, 2023
a1f1558
refactor: extract hybrid output check to function
MoustaphaDev May 15, 2023
bada941
add `hybrid` to output warning in adapter hooks
MoustaphaDev May 15, 2023
091e9c8
chore: changeset
MoustaphaDev May 15, 2023
037efbc
add `.js` extension to import
MoustaphaDev May 15, 2023
e6bd737
chore: use spaces instead of tabs for gh formating
MoustaphaDev May 15, 2023
e595d21
Merge branch 'main' of https://github.com/withastro/astro into mk-fea…
MoustaphaDev May 17, 2023
c408482
resolve merge conflict
MoustaphaDev May 17, 2023
3f340a3
chore: move test to another file for consitency
MoustaphaDev May 17, 2023
eb55a43
Merge branch 'main' into mk-feat/output-hybrid
matthewp May 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .changeset/mighty-shoes-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'astro': minor
'@astrojs/cloudflare': patch
'@astrojs/netlify': patch
'@astrojs/vercel': patch
'@astrojs/image': patch
'@astrojs/deno': patch
'@astrojs/node': patch
Comment on lines +3 to +8
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marked these as 'patch' because I updated the warning we get when using these adapters with an unexpected output mode.

---

Enable experimental support for hybrid SSR with pre-rendering enabled by default

__astro.config.mjs__
```js
import { defineConfig } from 'astro/config';
export defaultdefineConfig({
output: 'hybrid',
experimental: {
hybridOutput: true,
},
})
```
Then add `export const prerender = false` to any page or endpoint you want to opt-out of pre-rendering.

__src/pages/contact.astro__
```astro
---
export const prerender = false

if (Astro.request.method === 'POST') {
// handle form submission
}
---
<form method="POST">
<input type="text" name="name" />
<input type="email" name="email" />
<button type="submit">Submit</button>
</form>
```
47 changes: 43 additions & 4 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ export interface AstroUserConfig {
/**
* @docs
* @name output
* @type {('static' | 'server')}
* @type {('static' | 'server' | 'hybrid')}
* @default `'static'`
* @see adapter
* @description
Expand All @@ -566,6 +566,7 @@ export interface AstroUserConfig {
*
* - 'static' - Building a static site to be deploy to any static host.
* - 'server' - Building an app to be deployed to a host supporting SSR (server-side rendering).
* - 'hybrid' - Building a static site with a few server-side rendered pages.
*
* ```js
* import { defineConfig } from 'astro/config';
Expand All @@ -575,7 +576,7 @@ export interface AstroUserConfig {
* })
* ```
*/
output?: 'static' | 'server';
output?: 'static' | 'server' | 'hybrid';

/**
* @docs
Expand Down Expand Up @@ -616,14 +617,14 @@ export interface AstroUserConfig {
* @type {string}
* @default `'./dist/client'`
* @description
* Controls the output directory of your client-side CSS and JavaScript when `output: 'server'` only.
* Controls the output directory of your client-side CSS and JavaScript when `output: 'server'` or `output: 'hybrid'` only.
* `outDir` controls where the code is built to.
*
* This value is relative to the `outDir`.
*
* ```js
* {
* output: 'server',
* output: 'server', // or 'hybrid'
* build: {
* client: './client'
* }
Expand Down Expand Up @@ -1121,6 +1122,44 @@ export interface AstroUserConfig {
* ```
*/
middleware?: boolean;

/**
* @docs
* @name experimental.hybridOutput
* @type {boolean}
* @default `false`
* @version 2.5.0
* @description
* Enable experimental support for hybrid SSR with pre-rendering enabled by default.
*
* To enable this feature, first set `experimental.hybridOutput` to `true` in your Astro config, and set `output` to `hybrid`.
*
* ```js
* {
* output: 'hybrid',
* experimental: {
* hybridOutput: true,
* },
* }
* ```
* Then add `export const prerender = false` to any page or endpoint you want to opt-out of pre-rendering.
* ```astro
* ---
* // pages/contact.astro
* export const prerender = false
*
* if (Astro.request.method === 'POST') {
* // handle form submission
* }
* ---
* <form method="POST">
* <input type="text" name="name" />
* <input type="email" name="email" />
* <button type="submit">Submit</button>
* </form>
* ```
*/
hybridOutput?: boolean;
};

// Legacy options to be removed
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/assets/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { prependForwardSlash } from '../core/path.js';
import { getConfiguredImageService, isESMImportedImage } from './internal.js';
import type { LocalImageService } from './services/service.js';
import type { ImageTransform } from './types.js';
import { isHybridOutput } from '../prerender/utils.js';

interface GenerationDataUncached {
cached: false;
Expand Down Expand Up @@ -46,7 +47,7 @@ export async function generateImage(
}

let serverRoot: URL, clientRoot: URL;
if (buildOpts.settings.config.output === 'server') {
if (buildOpts.settings.config.output === 'server' || isHybridOutput(buildOpts.settings.config)) {
serverRoot = buildOpts.settings.config.build.server;
clientRoot = buildOpts.settings.config.build.client;
} else {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { isLocalService, type ImageService } from './services/service.js';
import type { GetImageResult, ImageMetadata, ImageTransform } from './types.js';
import { isHybridOutput } from '../prerender/utils.js';

export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
return typeof src === 'object';
Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import type {
StylesheetAsset,
} from './types';
import { getTimeStat } from './util.js';
import { isHybridOutput } from '../../prerender/utils.js';

function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
return (
Expand Down Expand Up @@ -89,7 +90,7 @@ export function chunkIsPage(

export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
const timer = performance.now();
const ssr = opts.settings.config.output === 'server';
const ssr = opts.settings.config.output === 'server' || isHybridOutput(opts.settings.config); // hybrid mode is essentially SSR with prerender by default
const serverEntry = opts.buildConfig.serverEntry;
const outFolder = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);

Expand Down Expand Up @@ -227,7 +228,7 @@ async function getPathsForRoute(
route: pageData.route,
isValidate: false,
logging: opts.logging,
ssr: opts.settings.config.output === 'server',
ssr: opts.settings.config.output === 'server' || isHybridOutput(opts.settings.config),
MoustaphaDev marked this conversation as resolved.
Show resolved Hide resolved
})
.then((_result) => {
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
Expand Down Expand Up @@ -403,7 +404,7 @@ async function generatePath(
}
}

const ssr = settings.config.output === 'server';
const ssr = settings.config.output === 'server' || isHybridOutput(settings.config);
const url = getUrlForPath(
pathname,
opts.settings.config.base,
Expand Down
13 changes: 9 additions & 4 deletions packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { AstroTelemetry } from '@astrojs/telemetry';
import type { AstroSettings, BuildConfig, ManifestData, RuntimeMode } from '../../@types/astro';
import type { LogOptions } from '../logger/core';
import type {
AstroConfig,
AstroSettings,
BuildConfig,
ManifestData,
RuntimeMode,
} from '../../@types/astro';

import fs from 'fs';
import * as colors from 'kleur/colors';
Expand All @@ -14,7 +19,7 @@ import {
runHookConfigSetup,
} from '../../integrations/index.js';
import { createVite } from '../create-vite.js';
import { debug, info, levels, timerMessage } from '../logger/core.js';
import { debug, info, levels, timerMessage, warn, type LogOptions } from '../logger/core.js';
import { printHelp } from '../messages.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { RouteCache } from '../render/route-cache.js';
Expand Down Expand Up @@ -233,7 +238,7 @@ class AstroBuilder {
logging: LogOptions;
timeStart: number;
pageCount: number;
buildMode: 'static' | 'server';
buildMode: AstroConfig['output'];
}) {
const total = getTimeStat(timeStart, performance.now());

Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/build/plugins/plugin-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../inter
import type { AstroBuildPlugin } from '../plugin';
import type { StaticBuildOptions } from '../types';

export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
return {
name: '@astro/plugin-build-pages',

options(options) {
if (opts.settings.config.output === 'static' || hasPrerenderedPages(internals)) {
Copy link
Member Author

@MoustaphaDev MoustaphaDev May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vitePluginPage runs before vitePluginPrerender which is responsible for parsing the prerender metadata of astro files, so there's no way to know if there are prerendered pages or not from this stage.
This would always return undefined so I removed it.

if (opts.settings.config.output === 'static') {
return addRollupInput(options, [pagesVirtualModuleId]);
}
},
Expand Down
6 changes: 2 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types';
import { extendManualChunks } from './util.js';

export function vitePluginPrerender(
opts: StaticBuildOptions,
internals: BuildInternals
): VitePlugin {
function vitePluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
return {
name: 'astro:rollup-plugin-prerender',

Expand All @@ -26,6 +23,7 @@ export function vitePluginPrerender(
pageInfo.route.prerender = true;
return 'prerender';
}
pageInfo.route.prerender = false;
// dynamic pages should all go in their own chunk in the pages/* directory
return `pages/all`;
}
Expand Down
9 changes: 5 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Plugin as VitePlugin } from 'vite';
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
import type { BuildInternals } from '../internal.js';
import type { StaticBuildOptions } from '../types';

import glob from 'fast-glob';
Expand All @@ -13,15 +12,16 @@ import { joinPaths, prependForwardSlash } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js';
import { cssOrder, eachPageData, mergeInlineCss } from '../internal.js';
import { cssOrder, eachPageData, mergeInlineCss, type BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin';
import { isHybridOutput } from '../../../prerender/utils.js';

export const virtualModuleId = '@astrojs-ssr-virtual-entry';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');

export function vitePluginSSR(
function vitePluginSSR(
internals: BuildInternals,
adapter: AstroAdapter,
config: AstroConfig
Expand Down Expand Up @@ -249,7 +249,8 @@ export function pluginSSR(
options: StaticBuildOptions,
internals: BuildInternals
): AstroBuildPlugin {
const ssr = options.settings.config.output === 'server';
const ssr =
options.settings.config.output === 'server' || isHybridOutput(options.settings.config);
return {
build: 'ssr',
hooks: {
Expand Down
16 changes: 9 additions & 7 deletions packages/astro/src/core/build/static-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.
import { registerAllPlugins } from './plugins/index.js';
import type { PageBuildData, StaticBuildOptions } from './types';
import { getTimeStat } from './util.js';
import { isHybridOutput } from '../../prerender/utils.js';

export async function viteBuild(opts: StaticBuildOptions) {
const { allPages, settings } = opts;
Expand Down Expand Up @@ -111,15 +112,16 @@ export async function viteBuild(opts: StaticBuildOptions) {

export async function staticBuild(opts: StaticBuildOptions, internals: BuildInternals) {
const { settings } = opts;
switch (settings.config.output) {
case 'static': {
const hybridOutput = isHybridOutput(settings.config);
switch (true) {
case settings.config.output === 'static': {
settings.timer.start('Static generate');
await generatePages(opts, internals);
await cleanServerOutput(opts);
settings.timer.end('Static generate');
return;
}
case 'server': {
case settings.config.output === 'server' || hybridOutput: {
settings.timer.start('Server generate');
await generatePages(opts, internals);
await cleanStaticOutput(opts, internals);
Expand All @@ -138,7 +140,7 @@ async function ssrBuild(
container: AstroBuildPluginContainer
) {
const { settings, viteConfig } = opts;
const ssr = settings.config.output === 'server';
const ssr = settings.config.output === 'server' || isHybridOutput(settings.config);
const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(settings.config.outDir);

const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
Expand Down Expand Up @@ -207,7 +209,7 @@ async function clientBuild(
) {
const { settings, viteConfig } = opts;
const timer = performance.now();
const ssr = settings.config.output === 'server';
const ssr = settings.config.output === 'server' || isHybridOutput(settings.config);
const out = ssr ? opts.buildConfig.client : getOutDirWithinCwd(settings.config.outDir);

// Nothing to do if there is no client-side JS.
Expand Down Expand Up @@ -273,7 +275,7 @@ async function runPostBuildHooks(
const buildConfig = container.options.settings.config.build;
for (const [fileName, mutation] of mutations) {
const root =
config.output === 'server'
config.output === 'server' || isHybridOutput(config)
? mutation.build === 'server'
? buildConfig.server
: buildConfig.client
Expand All @@ -294,7 +296,7 @@ async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInter
if (pageData.route.prerender)
allStaticFiles.add(internals.pageToBundleMap.get(pageData.moduleSpecifier));
}
const ssr = opts.settings.config.output === 'server';
const ssr = opts.settings.config.output === 'server' || isHybridOutput(opts.settings.config);
const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
// The SSR output is all .mjs files, the client output is not.
const files = await glob('**/*.mjs', {
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { LogOptions } from '../logger/core.js';
import { arraify, isObject, isURL } from '../util.js';
import { createRelativeSchema } from './schema.js';
import { loadConfigWithVite } from './vite-load.js';
import { isHybridMalconfigured } from '../../prerender/utils.js';

export const LEGACY_ASTRO_CONFIG_KEYS = new Set([
'projectRoot',
Expand Down Expand Up @@ -223,6 +224,12 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
}
const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd);

if (isHybridMalconfigured(astroConfig)) {
throw new Error(
`The "output" config option must be set to "hybrid" and "experimental.hybridOutput" must be set to true to use the hybrid output mode. Falling back to "static" output mode.`
);
}

return {
astroConfig,
userConfig,
Expand Down
Loading