feat: hybrid output (#6991)
* update config schema * adapt default route `prerender` value * adapt error message for hybrid output * core hybrid output support * add JSDocs for hybrid output * dev server hybrid output support * defer hybrid output check * update endpoint request warning * support `output=hybrid` in integrations * put constant variable out of for loop * revert: reapply back ssr plugin in ssr mode * change `prerender` option default * apply `prerender` by default in hybrid mode * simplfy conditional * update config schema * add `isHybridOutput` helper * more readable prerender condition * set default prerender value if no export is found * only add `pagesVirtualModuleId` ro rollup input in `output=static` * don't export vite plugin * remove unneeded check * don't prerender when it shouldn't * extract fallback `prerender` meta Extract the fallback `prerender` module meta out of the `scan` function. It shouldn't be its responsibility to handle that * pass missing argument to function * test: update cloudflare integration tests * test: update tests of vercel integration * test: update tests of node integration * test: update tests of netlify func integration * test: update tests of netlify edge integration * throw when `hybrid` mode is malconfigured * update node integraiton `output` warning * test(WIP): skip node prerendering tests for now * remove non-existant import * test: bring back prerendering tests * remove outdated comments * test: refactor test to support windows paths * remove outdated comments * apply sarah review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * docs: `experiment.hybridOutput` jsodcs * test: prevent import from being cached * refactor: extract hybrid output check to function * add `hybrid` to output warning in adapter hooks * chore: changeset * add `.js` extension to import * chore: use spaces instead of tabs for gh formating * resolve merge conflict * chore: move test to another file for consitency --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> Co-authored-by: Matthew Phillips <matthew@skypack.dev>
This commit is contained in:
parent
2b9230ed22
commit
719002ca5b
57 changed files with 671 additions and 175 deletions
39
.changeset/mighty-shoes-scream.md
Normal file
39
.changeset/mighty-shoes-scream.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
'astro': minor
|
||||
'@astrojs/cloudflare': patch
|
||||
'@astrojs/netlify': patch
|
||||
'@astrojs/vercel': patch
|
||||
'@astrojs/image': patch
|
||||
'@astrojs/deno': patch
|
||||
'@astrojs/node': patch
|
||||
---
|
||||
|
||||
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>
|
||||
```
|
|
@ -557,7 +557,7 @@ export interface AstroUserConfig {
|
|||
/**
|
||||
* @docs
|
||||
* @name output
|
||||
* @type {('static' | 'server')}
|
||||
* @type {('static' | 'server' | 'hybrid')}
|
||||
* @default `'static'`
|
||||
* @see adapter
|
||||
* @description
|
||||
|
@ -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';
|
||||
|
@ -575,7 +576,7 @@ export interface AstroUserConfig {
|
|||
* })
|
||||
* ```
|
||||
*/
|
||||
output?: 'static' | 'server';
|
||||
output?: 'static' | 'server' | 'hybrid';
|
||||
|
||||
/**
|
||||
* @docs
|
||||
|
@ -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'
|
||||
* }
|
||||
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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),
|
||||
})
|
||||
.then((_result) => {
|
||||
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
|
||||
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -233,7 +238,7 @@ class AstroBuilder {
|
|||
logging: LogOptions;
|
||||
timeStart: number;
|
||||
pageCount: number;
|
||||
buildMode: 'static' | 'server';
|
||||
buildMode: AstroConfig['output'];
|
||||
}) {
|
||||
const total = getTimeStat(timeStart, performance.now());
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
if (opts.settings.config.output === 'static') {
|
||||
return addRollupInput(options, [pagesVirtualModuleId]);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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', {
|
||||
|
|
|
@ -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',
|
||||
|
@ -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,
|
||||
|
|
|
@ -38,6 +38,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
legacy: {},
|
||||
experimental: {
|
||||
assets: false,
|
||||
hybridOutput: false,
|
||||
customClientDirecives: false,
|
||||
inlineStylesheets: 'never',
|
||||
middleware: false,
|
||||
|
@ -77,7 +78,7 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.trailingSlash),
|
||||
output: z
|
||||
.union([z.literal('static'), z.literal('server')])
|
||||
.union([z.literal('static'), z.literal('server'), z.literal('hybrid')])
|
||||
.optional()
|
||||
.default('static'),
|
||||
scopedStyleStrategy: z
|
||||
|
@ -205,6 +206,7 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.inlineStylesheets),
|
||||
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
|
||||
hybridOutput: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.hybridOutput),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
|
|
|
@ -8,6 +8,7 @@ import { getDefaultClientDirectives } from '../client-directive/index.js';
|
|||
import { createDefaultDevConfig } from './config.js';
|
||||
import { AstroTimer } from './timer.js';
|
||||
import { loadTSConfig } from './tsconfig.js';
|
||||
import { isHybridOutput } from '../../prerender/utils.js';
|
||||
|
||||
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||
return {
|
||||
|
@ -17,7 +18,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
|
|||
|
||||
adapter: undefined,
|
||||
injectedRoutes:
|
||||
config.experimental.assets && config.output === 'server'
|
||||
config.experimental.assets && (config.output === 'server' || isHybridOutput(config))
|
||||
? [{ pattern: '/_image', entryPoint: 'astro/assets/image-endpoint' }]
|
||||
: [],
|
||||
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
||||
|
|
|
@ -16,6 +16,7 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
|
|||
import { warn, type LogOptions } from '../logger/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { isValueSerializable } from '../render/core.js';
|
||||
import { isHybridOutput } from '../../prerender/utils.js';
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
@ -168,7 +169,7 @@ function isRedirect(statusCode: number) {
|
|||
}
|
||||
|
||||
export function throwIfRedirectNotAllowed(response: Response, config: AstroConfig) {
|
||||
if (config.output !== 'server' && isRedirect(response.status)) {
|
||||
if (config.output !== 'server' && !isHybridOutput(config) && isRedirect(response.status)) {
|
||||
throw new AstroError(AstroErrorData.StaticRedirectNotAvailable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export const AstroErrorData = {
|
|||
title: '`Astro.redirect` is not available in static mode.',
|
||||
code: 3001,
|
||||
message:
|
||||
"Redirects are only available when using `output: 'server'`. Update your Astro config if you need SSR features.",
|
||||
"Redirects are only available when using `output: 'server'` or `output: 'hybrid'`. Update your Astro config if you need SSR features.",
|
||||
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project for more information on how to enable SSR.',
|
||||
},
|
||||
/**
|
||||
|
@ -79,7 +79,7 @@ export const AstroErrorData = {
|
|||
title: '`Astro.clientAddress` is not available in static mode.',
|
||||
code: 3003,
|
||||
message:
|
||||
"`Astro.clientAddress` is only available when using `output: 'server'`. Update your Astro config if you need SSR features.",
|
||||
"`Astro.clientAddress` is only available when using `output: 'server'` or `output: 'hybrid'`. Update your Astro config if you need SSR features.",
|
||||
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project for more information on how to enable SSR.',
|
||||
},
|
||||
/**
|
||||
|
@ -390,7 +390,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
|||
NoAdapterInstalled: {
|
||||
title: 'Cannot use Server-side Rendering without an adapter.',
|
||||
code: 3017,
|
||||
message: `Cannot use \`output: 'server'\` without an adapter. Please install and configure the appropriate server adapter for your final deployment.`,
|
||||
message: `Cannot use \`output: 'server'\` or \`output: 'hybrid'\` without an adapter. Please install and configure the appropriate server adapter for your final deployment.`,
|
||||
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/ for more information.',
|
||||
},
|
||||
/**
|
||||
|
@ -416,10 +416,12 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
|||
InvalidPrerenderExport: {
|
||||
title: 'Invalid prerender export.',
|
||||
code: 3019,
|
||||
message: (prefix: string, suffix: string) => {
|
||||
message: (prefix: string, suffix: string, isHydridOuput: boolean) => {
|
||||
const defaultExpectedValue = isHydridOuput ? 'false' : 'true';
|
||||
let msg = `A \`prerender\` export has been detected, but its value cannot be statically analyzed.`;
|
||||
if (prefix !== 'const') msg += `\nExpected \`const\` declaration but got \`${prefix}\`.`;
|
||||
if (suffix !== 'true') msg += `\nExpected \`true\` value but got \`${suffix}\`.`;
|
||||
if (suffix !== 'true')
|
||||
msg += `\nExpected \`${defaultExpectedValue}\` value but got \`${suffix}\`.`;
|
||||
return msg;
|
||||
},
|
||||
hint: 'Mutable values declared at runtime are not supported. Please make sure to use exactly `export const prerender = true`.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { AstroSettings, RuntimeMode } from '../../../@types/astro';
|
||||
import { isHybridOutput } from '../../../prerender/utils.js';
|
||||
import type { LogOptions } from '../../logger/core.js';
|
||||
import type { ModuleLoader } from '../../module-loader/index';
|
||||
import type { Environment } from '../index';
|
||||
|
@ -29,7 +30,7 @@ export function createDevelopmentEnvironment(
|
|||
resolve: createResolve(loader, settings.config.root),
|
||||
routeCache: new RouteCache(logging, mode),
|
||||
site: settings.config.site,
|
||||
ssr: settings.config.output === 'server',
|
||||
ssr: settings.config.output === 'server' || isHybridOutput(settings.config),
|
||||
streaming: true,
|
||||
telemetry: Boolean(settings.forceDisableTelemetry),
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import { warn } from '../../logger/core.js';
|
|||
import { removeLeadingForwardSlash } from '../../path.js';
|
||||
import { resolvePages } from '../../util.js';
|
||||
import { getRouteGenerator } from './generator.js';
|
||||
import { isHybridOutput } from '../../../prerender/utils.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
interface Item {
|
||||
|
@ -226,6 +227,7 @@ export function createRouteManifest(
|
|||
]);
|
||||
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
||||
const localFs = fsMod ?? nodeFs;
|
||||
const isPrenderDefault = isHybridOutput(settings.config);
|
||||
|
||||
function walk(
|
||||
fs: typeof nodeFs,
|
||||
|
@ -322,7 +324,6 @@ export function createRouteManifest(
|
|||
const route = `/${segments
|
||||
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
|
||||
.join('/')}`.toLowerCase();
|
||||
|
||||
routes.push({
|
||||
route,
|
||||
type: item.isPage ? 'page' : 'endpoint',
|
||||
|
@ -332,7 +333,7 @@ export function createRouteManifest(
|
|||
component,
|
||||
generate,
|
||||
pathname: pathname || undefined,
|
||||
prerender: false,
|
||||
prerender: isPrenderDefault,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -408,7 +409,7 @@ export function createRouteManifest(
|
|||
component,
|
||||
generate,
|
||||
pathname: pathname || void 0,
|
||||
prerender: false,
|
||||
prerender: isPrenderDefault,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro';
|
|||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
|
||||
import type { ModuleLoader } from './module-loader';
|
||||
import { prependForwardSlash, removeTrailingForwardSlash } from './path.js';
|
||||
import { isHybridOutput } from '../prerender/utils.js';
|
||||
|
||||
/** Returns true if argument is an object of any prototype/class (but not null). */
|
||||
export function isObject(value: unknown): value is Record<string, any> {
|
||||
|
@ -138,7 +139,9 @@ export function isEndpoint(file: URL, settings: AstroSettings): boolean {
|
|||
}
|
||||
|
||||
export function isModeServerWithNoAdapter(settings: AstroSettings): boolean {
|
||||
return settings.config.output === 'server' && !settings.adapter;
|
||||
return (
|
||||
(settings.config.output === 'server' || isHybridOutput(settings.config)) && !settings.adapter
|
||||
);
|
||||
}
|
||||
|
||||
export function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.j
|
|||
import { mergeConfig } from '../core/config/config.js';
|
||||
import { info, type LogOptions } from '../core/logger/core.js';
|
||||
import { mdxContentEntryType } from '../vite-plugin-markdown/content-entry-type.js';
|
||||
import { isHybridOutput } from '../prerender/utils.js';
|
||||
|
||||
async function withTakingALongTimeMsg<T>({
|
||||
name,
|
||||
|
@ -329,7 +330,8 @@ export async function runHookBuildGenerated({
|
|||
buildConfig: BuildConfig;
|
||||
logging: LogOptions;
|
||||
}) {
|
||||
const dir = config.output === 'server' ? buildConfig.client : config.outDir;
|
||||
const dir =
|
||||
config.output === 'server' || isHybridOutput(config) ? buildConfig.client : config.outDir;
|
||||
|
||||
for (const integration of config.integrations) {
|
||||
if (integration?.hooks?.['astro:build:generated']) {
|
||||
|
@ -355,7 +357,8 @@ export async function runHookBuildDone({
|
|||
routes: RouteData[];
|
||||
logging: LogOptions;
|
||||
}) {
|
||||
const dir = config.output === 'server' ? buildConfig.client : config.outDir;
|
||||
const dir =
|
||||
config.output === 'server' || isHybridOutput(config) ? buildConfig.client : config.outDir;
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
|
||||
for (const integration of config.integrations) {
|
||||
|
|
11
packages/astro/src/prerender/utils.ts
Normal file
11
packages/astro/src/prerender/utils.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
// TODO: remove after the experimetal phase when
|
||||
|
||||
import type { AstroConfig } from '../@types/astro';
|
||||
|
||||
export function isHybridMalconfigured(config: AstroConfig) {
|
||||
return config.experimental.hybridOutput ? config.output !== 'hybrid' : config.output === 'hybrid';
|
||||
}
|
||||
|
||||
export function isHybridOutput(config: AstroConfig) {
|
||||
return config.experimental.hybridOutput && config.output === 'hybrid';
|
||||
}
|
|
@ -25,7 +25,7 @@ export async function renderEndpoint(mod: EndpointHandler, context: APIContext,
|
|||
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`
|
||||
${chosenMethod} requests are not available when building a static site. Update your config to output: 'server' to handle ${chosenMethod} requests.`);
|
||||
${chosenMethod} requests are not available when building a static site. Update your config to \`output: 'server'\` or \`output: 'hybrid'\` with an \`export const prerender = false\` to handle ${chosenMethod} requests.`);
|
||||
}
|
||||
if (!handler || typeof handler !== 'function') {
|
||||
// No handler found, so this should be a 404. Using a custom header
|
||||
|
|
|
@ -12,6 +12,7 @@ import { eventError, telemetry } from '../events/index.js';
|
|||
import { runWithErrorHandling } from './controller.js';
|
||||
import { handle500Response } from './response.js';
|
||||
import { handleRoute, matchRoute } from './route.js';
|
||||
import { isHybridOutput } from '../prerender/utils.js';
|
||||
|
||||
/** The main logic to route dev server requests to pages in Astro. */
|
||||
export async function handleRequest(
|
||||
|
@ -24,7 +25,7 @@ export async function handleRequest(
|
|||
const { settings, loader: moduleLoader } = env;
|
||||
const { config } = settings;
|
||||
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${req.headers.host}`;
|
||||
const buildingToSSR = config.output === 'server';
|
||||
const buildingToSSR = config.output === 'server' || isHybridOutput(config);
|
||||
|
||||
const url = new URL(origin + req.url);
|
||||
let pathname: string;
|
||||
|
|
|
@ -18,6 +18,7 @@ import { createRequest } from '../core/request.js';
|
|||
import { matchAllRoutes } from '../core/routing/index.js';
|
||||
import { log404 } from './common.js';
|
||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||
import { isHybridOutput } from '../prerender/utils.js';
|
||||
|
||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||
...args: any
|
||||
|
@ -58,7 +59,7 @@ export async function matchRoute(
|
|||
routeCache,
|
||||
pathname: pathname,
|
||||
logging,
|
||||
ssr: settings.config.output === 'server',
|
||||
ssr: settings.config.output === 'server' || isHybridOutput(settings.config),
|
||||
});
|
||||
|
||||
if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
|
@ -131,7 +132,7 @@ export async function handleRoute(
|
|||
const { config } = settings;
|
||||
const filePath: URL | undefined = matchedRoute.filePath;
|
||||
const { route, preloadedComponent, mod } = matchedRoute;
|
||||
const buildingToSSR = config.output === 'server';
|
||||
const buildingToSSR = config.output === 'server' || isHybridOutput(config);
|
||||
|
||||
// Headers are only available when using SSR.
|
||||
const request = createRequest({
|
||||
|
@ -157,7 +158,7 @@ export async function handleRoute(
|
|||
routeCache: env.routeCache,
|
||||
pathname: pathname,
|
||||
logging,
|
||||
ssr: config.output === 'server',
|
||||
ssr: config.output === 'server' || isHybridOutput(config),
|
||||
});
|
||||
|
||||
const options: SSROptions = {
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { AstroSettings } from '../@types/astro.js';
|
|||
import { isEndpoint, isPage } from '../core/util.js';
|
||||
|
||||
import { scan } from './scan.js';
|
||||
import { isHybridOutput } from '../prerender/utils.js';
|
||||
|
||||
export default function astroScannerPlugin({ settings }: { settings: AstroSettings }): VitePlugin {
|
||||
return {
|
||||
|
@ -24,7 +25,12 @@ export default function astroScannerPlugin({ settings }: { settings: AstroSettin
|
|||
const fileIsPage = isPage(fileURL, settings);
|
||||
const fileIsEndpoint = isEndpoint(fileURL, settings);
|
||||
if (!(fileIsPage || fileIsEndpoint)) return;
|
||||
const pageOptions = await scan(code, id);
|
||||
const hybridOutput = isHybridOutput(settings.config);
|
||||
const pageOptions = await scan(code, id, hybridOutput);
|
||||
|
||||
if (typeof pageOptions.prerender === 'undefined') {
|
||||
pageOptions.prerender = hybridOutput ? true : false;
|
||||
}
|
||||
|
||||
const { meta = {} } = this.getModuleInfo(id) ?? {};
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as eslexer from 'es-module-lexer';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import type { PageOptions } from '../vite-plugin-astro/types.js';
|
||||
import type { AstroSettings } from '../@types/astro.js';
|
||||
|
||||
const BOOLEAN_EXPORTS = new Set(['prerender']);
|
||||
|
||||
|
@ -34,7 +35,7 @@ function isFalsy(value: string) {
|
|||
|
||||
let didInit = false;
|
||||
|
||||
export async function scan(code: string, id: string): Promise<PageOptions> {
|
||||
export async function scan(code: string, id: string, isHybridOutput = false): Promise<PageOptions> {
|
||||
if (!includesExport(code)) return {};
|
||||
if (!didInit) {
|
||||
await eslexer.init;
|
||||
|
@ -45,6 +46,7 @@ export async function scan(code: string, id: string): Promise<PageOptions> {
|
|||
let pageOptions: PageOptions = {};
|
||||
for (const _export of exports) {
|
||||
const { n: name, le: endOfLocalName } = _export;
|
||||
// mark that a `prerender` export was found
|
||||
if (BOOLEAN_EXPORTS.has(name)) {
|
||||
// For a given export, check the value of the local declaration
|
||||
// Basically extract the `const` from the statement `export const prerender = true`
|
||||
|
@ -61,7 +63,7 @@ export async function scan(code: string, id: string): Promise<PageOptions> {
|
|||
if (prefix !== 'const' || !(isTruthy(suffix) || isFalsy(suffix))) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.InvalidPrerenderExport,
|
||||
message: AstroErrorData.InvalidPrerenderExport.message(prefix, suffix),
|
||||
message: AstroErrorData.InvalidPrerenderExport.message(prefix, suffix, isHybridOutput),
|
||||
location: { file: id },
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { expect } from 'chai';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
describe('Integrations can hook into the prerendering decision', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
const testIntegration = {
|
||||
name: 'test prerendering integration',
|
||||
hooks: {
|
||||
['astro:build:setup']({ pages, target }) {
|
||||
if (target !== 'client') return;
|
||||
// this page has `export const prerender = true`
|
||||
pages.get('src/pages/static.astro').route.prerender = false;
|
||||
|
||||
// this page does not
|
||||
pages.get('src/pages/not-prerendered.astro').route.prerender = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/ssr-prerender/',
|
||||
output: 'server',
|
||||
integrations: [testIntegration],
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('An integration can override the prerender flag', async () => {
|
||||
// test adapter only hosts dynamic routes
|
||||
// /static is expected to become dynamic
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/static');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('An integration can turn a normal page to a prerendered one', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/not-prerendered');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
|
@ -63,3 +63,48 @@ describe('SSR: prerender', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integrations can hook into the prerendering decision', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
const testIntegration = {
|
||||
name: 'test prerendering integration',
|
||||
hooks: {
|
||||
['astro:build:setup']({ pages, target }) {
|
||||
if (target !== 'client') return;
|
||||
// this page has `export const prerender = true`
|
||||
pages.get('src/pages/static.astro').route.prerender = false;
|
||||
|
||||
// this page does not
|
||||
pages.get('src/pages/not-prerendered.astro').route.prerender = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/ssr-prerender/',
|
||||
output: 'server',
|
||||
integrations: [testIntegration],
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('An integration can override the prerender flag', async () => {
|
||||
// test adapter only hosts dynamic routes
|
||||
// /static is expected to become dynamic
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/static');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('An integration can turn a normal page to a prerendered one', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/not-prerendered');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"chai": "^4.3.6",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"mocha": "^9.2.2",
|
||||
"slash": "^4.0.0",
|
||||
"wrangler": "^2.0.23"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
|
||||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
[@astrojs/cloudflare] \`output: "server"\` is required to use this adapter. Otherwise, this adapter is not necessary to deploy a static site to Cloudflare.
|
||||
[@astrojs/cloudflare] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter. Otherwise, this adapter is not necessary to deploy a static site to Cloudflare.
|
||||
|
||||
`);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
export const prerender = import.meta.env.PRERENDER;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
@ -1,19 +1,60 @@
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import slash from 'slash';
|
||||
|
||||
describe('Prerendering', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
process.env.PRERENDER = true;
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/prerender/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.PRERENDER;
|
||||
fixture.clean();
|
||||
});
|
||||
|
||||
it('includes prerendered routes in the routes.json config', async () => {
|
||||
const routes = JSON.parse(await fixture.readFile('/_routes.json'));
|
||||
expect(routes.exclude).to.include('/one/');
|
||||
const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json')).exclude.map((r) =>
|
||||
slash(r)
|
||||
);
|
||||
const expectedExcludedRoutes = ['/_worker.js', '/one/index.html', '/one/'];
|
||||
|
||||
expect(foundRoutes.every((element) => expectedExcludedRoutes.includes(element))).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hybrid rendering', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
process.env.PRERENDER = false;
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/prerender/',
|
||||
output: 'hybrid',
|
||||
experimental: {
|
||||
hybridOutput: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('includes prerendered routes in the routes.json config', async () => {
|
||||
const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json')).exclude.map((r) =>
|
||||
slash(r)
|
||||
);
|
||||
const expectedExcludedRoutes = ['/_worker.js', '/index.html', '/'];
|
||||
|
||||
expect(foundRoutes.every((element) => expectedExcludedRoutes.includes(element))).to.be.true;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,10 @@ import { fileURLToPath } from 'url';
|
|||
|
||||
export { fixLineEndings } from '../../../astro/test/test-utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
||||
*/
|
||||
|
||||
export function loadFixture(config) {
|
||||
if (config?.root) {
|
||||
config.root = new URL(config.root, import.meta.url);
|
||||
|
|
|
@ -62,7 +62,9 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
_buildConfig = config.build;
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/deno] \`output: "server"\` is required to use this adapter.`);
|
||||
console.warn(
|
||||
`[@astrojs/deno] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
|
||||
);
|
||||
console.warn(
|
||||
`[@astrojs/deno] Otherwise, this adapter is not required to deploy a static site to Deno.`
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { ImageService, SSRImageService, TransformOptions } from './loaders/
|
|||
import type { LoggerLevel } from './utils/logger.js';
|
||||
import { joinPaths, prependForwardSlash, propsToFilename } from './utils/paths.js';
|
||||
import { createPlugin } from './vite-plugin-astro-image.js';
|
||||
import { isHybridOutput } from './utils/prerender.js';
|
||||
|
||||
export { getImage } from './lib/get-image.js';
|
||||
export { getPicture } from './lib/get-picture.js';
|
||||
|
@ -84,7 +85,7 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
vite: getViteConfiguration(command === 'dev'),
|
||||
});
|
||||
|
||||
if (command === 'dev' || config.output === 'server') {
|
||||
if (command === 'dev' || config.output === 'server' || isHybridOutput(config)) {
|
||||
injectRoute({
|
||||
pattern: ROUTE_PATTERN,
|
||||
entryPoint: '@astrojs/image/endpoint',
|
||||
|
|
5
packages/integrations/image/src/utils/prerender.ts
Normal file
5
packages/integrations/image/src/utils/prerender.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { AstroConfig } from 'astro';
|
||||
|
||||
export function isHybridOutput(config: AstroConfig) {
|
||||
return config.experimental.hybridOutput && config.output === 'hybrid';
|
||||
}
|
|
@ -134,7 +134,9 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/netlify] \`output: "server"\` is required to use this adapter.`);
|
||||
console.warn(
|
||||
`[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
|
||||
);
|
||||
console.warn(
|
||||
`[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`
|
||||
);
|
||||
|
|
|
@ -43,7 +43,9 @@ function netlifyFunctions({
|
|||
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/netlify] \`output: "server"\` is required to use this adapter.`);
|
||||
console.warn(
|
||||
`[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
|
||||
);
|
||||
console.warn(
|
||||
`[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`
|
||||
);
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
// @ts-nocheck
|
||||
export { fromFileUrl } from 'https://deno.land/std@0.110.0/path/mod.ts';
|
||||
export { assertEquals, assert } from 'https://deno.land/std@0.132.0/testing/asserts.ts';
|
||||
export {
|
||||
assertEquals,
|
||||
assert,
|
||||
assertExists,
|
||||
} from 'https://deno.land/std@0.132.0/testing/asserts.ts';
|
||||
export * from 'https://deno.land/x/deno_dom/deno-dom-wasm.ts';
|
||||
export * from 'https://deno.land/std@0.142.0/streams/conversion.ts';
|
||||
export * as cheerio from 'https://cdn.skypack.dev/cheerio?dts';
|
||||
export * as fs from 'https://deno.land/std/fs/mod.ts';
|
||||
|
|
|
@ -4,8 +4,8 @@ import { assertEquals, assert, DOMParser } from './deps.ts';
|
|||
Deno.test({
|
||||
name: 'Dynamic imports',
|
||||
async fn() {
|
||||
let close = await runBuild('./fixtures/dynimport/');
|
||||
let stop = await runApp('./fixtures/dynimport/prod.js');
|
||||
await runBuild('./fixtures/dynimport/');
|
||||
const stop = await runApp('./fixtures/dynimport/prod.js');
|
||||
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:8085/');
|
||||
|
@ -20,7 +20,6 @@ Deno.test({
|
|||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
} finally {
|
||||
await close();
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { runBuild } from './test-utils.ts';
|
||||
import { loadFixture } from './test-utils.ts';
|
||||
import { assertEquals, assert, DOMParser } from './deps.ts';
|
||||
|
||||
Deno.env.set('SECRET_STUFF', 'secret');
|
||||
|
@ -10,7 +10,8 @@ Deno.test({
|
|||
name: 'Edge Basics',
|
||||
skip: true,
|
||||
async fn() {
|
||||
let close = await runBuild('./fixtures/edge-basic/');
|
||||
const fixture = loadFixture('./fixtures/edge-basic/');
|
||||
await fixture.runBuild();
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/edge-basic/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
|
@ -26,6 +27,6 @@ Deno.test({
|
|||
const envDiv = doc.querySelector('#env');
|
||||
assertEquals(envDiv?.innerText, 'secret');
|
||||
|
||||
await close();
|
||||
await fixture.cleanup();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import { netlifyEdgeFunctions } from '@astrojs/netlify';
|
||||
import { defineConfig } from "astro/config";
|
||||
import { netlifyEdgeFunctions } from "@astrojs/netlify";
|
||||
|
||||
const isHybridMode = process.env.PRERENDER === "false";
|
||||
|
||||
/** @type {import('astro').AstroConfig} */
|
||||
const partialConfig = {
|
||||
output: isHybridMode ? "hybrid" : "server",
|
||||
...(isHybridMode
|
||||
? ({
|
||||
experimental: {
|
||||
hybridOutput: true,
|
||||
},
|
||||
})
|
||||
: ({})),
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
adapter: netlifyEdgeFunctions({
|
||||
dist: new URL('./dist/', import.meta.url),
|
||||
}),
|
||||
output: 'server',
|
||||
})
|
||||
adapter: netlifyEdgeFunctions({
|
||||
dist: new URL("./dist/", import.meta.url),
|
||||
}),
|
||||
...partialConfig,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
export const prerender = true
|
||||
export const prerender = import.meta.env.PRERENDER;
|
||||
---
|
||||
|
||||
<html>
|
||||
|
|
|
@ -1,15 +1,76 @@
|
|||
import { runBuild } from './test-utils.ts';
|
||||
import { assertEquals } from './deps.ts';
|
||||
import { loadFixture } from './test-utils.ts';
|
||||
import { assertEquals, assertExists, cheerio, fs } from './deps.ts';
|
||||
|
||||
Deno.test({
|
||||
name: 'Prerender',
|
||||
async fn() {
|
||||
let close = await runBuild('./fixtures/prerender/');
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/prerender/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/index.html'));
|
||||
assertEquals(response, undefined, 'No response because this is an asset');
|
||||
await close();
|
||||
async fn(t) {
|
||||
const environmentVariables = {
|
||||
PRERENDER: 'true',
|
||||
};
|
||||
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
|
||||
await fixture.runBuild();
|
||||
|
||||
await t.step('Handler can process requests to non-existing routes', async () => {
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/prerender/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
assertExists(handler);
|
||||
const response = await handler(new Request('http://example.com/index.html'));
|
||||
assertEquals(response, undefined, "No response because this route doesn't exist");
|
||||
});
|
||||
|
||||
await t.step('Prerendered route exists', async () => {
|
||||
let content: string | null = null;
|
||||
try {
|
||||
const path = new URL('./fixtures/prerender/dist/index.html', import.meta.url);
|
||||
content = Deno.readTextFileSync(path);
|
||||
} catch (e) {}
|
||||
assertExists(content);
|
||||
const $ = cheerio.load(content);
|
||||
assertEquals($('h1').text(), 'testing');
|
||||
});
|
||||
|
||||
Deno.env.delete('PRERENDER');
|
||||
await fixture.cleanup();
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: 'Hybrid rendering',
|
||||
async fn(t) {
|
||||
const environmentVariables = {
|
||||
PRERENDER: 'false',
|
||||
};
|
||||
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
|
||||
await fixture.runBuild();
|
||||
|
||||
const stop = await fixture.runApp('./fixtures/prerender/prod.js');
|
||||
await t.step('Can fetch server route', async () => {
|
||||
const response = await fetch('http://127.0.0.1:8085/');
|
||||
assertEquals(response.status, 200);
|
||||
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
assertEquals($('h1').text(), 'testing');
|
||||
});
|
||||
stop();
|
||||
|
||||
await t.step('Handler can process requests to non-existing routes', async () => {
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/prerender/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/index.html'));
|
||||
assertEquals(response, undefined, "No response because this route doesn't exist");
|
||||
});
|
||||
|
||||
await t.step('Has no prerendered route', async () => {
|
||||
let prerenderedRouteExists = false;
|
||||
try {
|
||||
const path = new URL('./fixtures/prerender/dist/index.html', import.meta.url);
|
||||
prerenderedRouteExists = fs.existsSync(path);
|
||||
} catch (e) {}
|
||||
assertEquals(prerenderedRouteExists, false);
|
||||
});
|
||||
await fixture.cleanup();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { runBuild } from './test-utils.ts';
|
||||
import { loadFixture } from './test-utils.ts';
|
||||
import { assertEquals, assert, DOMParser } from './deps.ts';
|
||||
|
||||
Deno.test({
|
||||
|
@ -6,12 +6,14 @@ Deno.test({
|
|||
ignore: true,
|
||||
name: 'Assets are preferred over HTML routes',
|
||||
async fn() {
|
||||
let close = await runBuild('./fixtures/root-dynamic/');
|
||||
const fixture = loadFixture('./fixtures/root-dynamic/');
|
||||
await fixture.runBuild();
|
||||
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/root-dynamic/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/styles.css'));
|
||||
assertEquals(response, undefined, 'No response because this is an asset');
|
||||
await close();
|
||||
await fixture.cleanup();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,29 +1,50 @@
|
|||
import { fromFileUrl, readableStreamFromReader } from './deps.ts';
|
||||
const dir = new URL('./', import.meta.url);
|
||||
|
||||
export async function runBuild(fixturePath: string) {
|
||||
let proc = Deno.run({
|
||||
cmd: ['node', '../../../../../../astro/astro.js', 'build', '--silent'],
|
||||
cwd: fromFileUrl(new URL(fixturePath, dir)),
|
||||
});
|
||||
await proc.status();
|
||||
return async () => await proc.close();
|
||||
}
|
||||
|
||||
export async function runApp(entryPath: string) {
|
||||
const entryUrl = new URL(entryPath, dir);
|
||||
let proc = Deno.run({
|
||||
cmd: ['deno', 'run', '--allow-env', '--allow-net', fromFileUrl(entryUrl)],
|
||||
//cwd: fromFileUrl(entryUrl),
|
||||
stderr: 'piped',
|
||||
});
|
||||
const stderr = readableStreamFromReader(proc.stderr);
|
||||
const dec = new TextDecoder();
|
||||
for await (let bytes of stderr) {
|
||||
let msg = dec.decode(bytes);
|
||||
if (msg.includes(`Server running`)) {
|
||||
break;
|
||||
}
|
||||
export function loadFixture(fixturePath: string, envionmentVariables?: Record<string, string>) {
|
||||
async function runBuild() {
|
||||
const proc = Deno.run({
|
||||
cmd: ['node', '../../../../../../astro/astro.js', 'build'],
|
||||
env: envionmentVariables,
|
||||
cwd: fromFileUrl(new URL(fixturePath, dir)),
|
||||
});
|
||||
await proc.status();
|
||||
proc.close();
|
||||
}
|
||||
return () => proc.close();
|
||||
|
||||
async function runApp(entryPath: string) {
|
||||
const entryUrl = new URL(entryPath, dir);
|
||||
let proc = Deno.run({
|
||||
cmd: ['deno', 'run', '--allow-env', '--allow-net', fromFileUrl(entryUrl)],
|
||||
env: envionmentVariables,
|
||||
//cwd: fromFileUrl(entryUrl),
|
||||
stderr: 'piped',
|
||||
});
|
||||
const stderr = readableStreamFromReader(proc.stderr);
|
||||
const dec = new TextDecoder();
|
||||
for await (let bytes of stderr) {
|
||||
let msg = dec.decode(bytes);
|
||||
if (msg.includes(`Server running`)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return () => proc.close();
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
const netlifyPath = new URL('.netlify', new URL(fixturePath, dir));
|
||||
const distPath = new URL('dist', new URL(fixturePath, dir));
|
||||
|
||||
// remove the netlify folder
|
||||
await Deno.remove(netlifyPath, { recursive: true });
|
||||
|
||||
// remove the dist folder
|
||||
await Deno.remove(distPath, { recursive: true });
|
||||
}
|
||||
|
||||
return {
|
||||
runApp,
|
||||
runBuild,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
export const prerender = import.meta.env.PRERENDER;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { expect } from 'chai';
|
||||
import netlifyAdapter from '../../dist/index.js';
|
||||
import { loadFixture, testIntegration } from './test-utils.js';
|
||||
import { after } from 'node:test';
|
||||
|
||||
describe('Mixed Prerendering with SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
process.env.PRERENDER = true;
|
||||
fixture = await loadFixture({
|
||||
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
|
||||
output: 'server',
|
||||
|
@ -18,13 +20,56 @@ describe('Mixed Prerendering with SSR', () => {
|
|||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('Wildcard 404 is sorted last', async () => {
|
||||
const redir = await fixture.readFile('/_redirects');
|
||||
const baseRouteIndex = redir.indexOf('/ /.netlify/functions/entry 200');
|
||||
const oneRouteIndex = redir.indexOf('/one /one/index.html 200');
|
||||
const fourOhFourWildCardIndex = redir.indexOf('/* /.netlify/functions/entry 404');
|
||||
|
||||
expect(oneRouteIndex).to.not.be.equal(-1);
|
||||
expect(fourOhFourWildCardIndex).to.be.greaterThan(baseRouteIndex);
|
||||
expect(fourOhFourWildCardIndex).to.be.greaterThan(oneRouteIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed Hybrid rendering with SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
process.env.PRERENDER = false;
|
||||
fixture = await loadFixture({
|
||||
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
|
||||
output: 'hybrid',
|
||||
experimental: {
|
||||
hybridOutput: true,
|
||||
},
|
||||
adapter: netlifyAdapter({
|
||||
dist: new URL('./fixtures/prerender/dist/', import.meta.url),
|
||||
}),
|
||||
site: `http://example.com`,
|
||||
integrations: [testIntegration()],
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('outputs a correct redirect file', async () => {
|
||||
const redir = await fixture.readFile('/_redirects');
|
||||
const baseRouteIndex = redir.indexOf('/one /.netlify/functions/entry 200');
|
||||
const rootRouteIndex = redir.indexOf('/ /index.html 200');
|
||||
const fourOhFourIndex = redir.indexOf('/404 /404.html 200');
|
||||
|
||||
expect(rootRouteIndex).to.not.be.equal(-1);
|
||||
expect(baseRouteIndex).to.not.be.equal(-1);
|
||||
expect(fourOhFourIndex).to.not.be.equal(-1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,7 +40,9 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr
|
|||
setAdapter(getAdapter(_options));
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/node] \`output: "server"\` is required to use this adapter.`);
|
||||
console.warn(
|
||||
`[@astrojs/node] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
export const prerender = import.meta.env.PRERENDER;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
import nodejs from '../dist/index.js';
|
||||
import { loadFixture, createRequestAndResponse } from './test-utils.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { fetch } from 'undici';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
||||
*/
|
||||
|
||||
async function load() {
|
||||
const mod = await import(`./fixtures/prerender/dist/server/entry.mjs?dropcache=${Date.now()}`);
|
||||
return mod;
|
||||
}
|
||||
describe('Prerendering', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
let server;
|
||||
|
||||
async function load() {
|
||||
const mod = await import('./fixtures/prerender/dist/server/entry.mjs');
|
||||
return mod;
|
||||
}
|
||||
|
||||
describe('With base', () => {
|
||||
describe('With base', async () => {
|
||||
before(async () => {
|
||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||
process.env.PRERENDER = true;
|
||||
|
||||
fixture = await loadFixture({
|
||||
base: '/some-base',
|
||||
root: './fixtures/prerender/',
|
||||
|
@ -31,6 +36,8 @@ describe('Prerendering', () => {
|
|||
|
||||
after(async () => {
|
||||
await server.stop();
|
||||
await fixture.clean();
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('Can render SSR route', async () => {
|
||||
|
@ -68,9 +75,12 @@ describe('Prerendering', () => {
|
|||
expect(res.headers.get('location')).to.equal('/some-base/two/');
|
||||
});
|
||||
});
|
||||
describe('Without base', () => {
|
||||
|
||||
describe('Without base', async () => {
|
||||
before(async () => {
|
||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||
process.env.PRERENDER = true;
|
||||
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/prerender/',
|
||||
output: 'server',
|
||||
|
@ -84,6 +94,8 @@ describe('Prerendering', () => {
|
|||
|
||||
after(async () => {
|
||||
await server.stop();
|
||||
await fixture.clean();
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('Can render SSR route', async () => {
|
||||
|
@ -114,3 +126,121 @@ describe('Prerendering', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hybrid rendering', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
let server;
|
||||
|
||||
describe('With base', async () => {
|
||||
before(async () => {
|
||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||
process.env.PRERENDER = false;
|
||||
fixture = await loadFixture({
|
||||
base: '/some-base',
|
||||
root: './fixtures/prerender/',
|
||||
output: 'hybrid',
|
||||
experimental: {
|
||||
hybridOutput: true,
|
||||
},
|
||||
adapter: nodejs({ mode: 'standalone' }),
|
||||
});
|
||||
await fixture.build();
|
||||
const { startServer } = await await load();
|
||||
let res = startServer();
|
||||
server = res.server;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.stop();
|
||||
await fixture.clean();
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('Can render SSR route', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/some-base/two`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
expect(res.status).to.equal(200);
|
||||
expect($('h1').text()).to.equal('Two');
|
||||
});
|
||||
|
||||
it('Can render prerendered route', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/some-base/one`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect($('h1').text()).to.equal('One');
|
||||
});
|
||||
|
||||
it('Can render prerendered route with query params', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect($('h1').text()).to.equal('One');
|
||||
});
|
||||
|
||||
it('Omitting the trailing slash results in a redirect that includes the base', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, {
|
||||
redirect: 'manual',
|
||||
});
|
||||
expect(res.status).to.equal(301);
|
||||
expect(res.headers.get('location')).to.equal('/some-base/one/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without base', async () => {
|
||||
before(async () => {
|
||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||
process.env.PRERENDER = false;
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/prerender/',
|
||||
output: 'hybrid',
|
||||
experimental: {
|
||||
hybridOutput: true,
|
||||
},
|
||||
adapter: nodejs({ mode: 'standalone' }),
|
||||
});
|
||||
await fixture.build();
|
||||
const { startServer } = await await load();
|
||||
let res = startServer();
|
||||
server = res.server;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.stop();
|
||||
await fixture.clean();
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('Can render SSR route', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/two`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect($('h1').text()).to.equal('Two');
|
||||
});
|
||||
|
||||
it('Can render prerendered route', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/one`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect($('h1').text()).to.equal('One');
|
||||
});
|
||||
|
||||
it('Can render prerendered route with query params', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect($('h1').text()).to.equal('One');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -80,7 +80,7 @@ export default function vercelEdge({
|
|||
|
||||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
[@astrojs/vercel] \`output: "server"\` is required to use the edge adapter.
|
||||
[@astrojs/vercel] \`output: "server"\` or \`output: "hybrid"\` is required to use the edge adapter.
|
||||
|
||||
`);
|
||||
}
|
||||
|
|
5
packages/integrations/vercel/src/lib/prerender.ts
Normal file
5
packages/integrations/vercel/src/lib/prerender.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { AstroConfig } from 'astro';
|
||||
|
||||
export function isHybridOutput(config: AstroConfig) {
|
||||
return config.experimental.hybridOutput && config.output === 'hybrid';
|
||||
}
|
|
@ -75,7 +75,7 @@ export default function vercelServerless({
|
|||
|
||||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
[@astrojs/vercel] \`output: "server"\` is required to use the serverless adapter.
|
||||
[@astrojs/vercel] \`output: "server"\` or \`output: "hybrid"\` is required to use the serverless adapter.
|
||||
|
||||
`);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { exposeEnv } from '../lib/env.js';
|
||||
import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js';
|
||||
import { getRedirects } from '../lib/redirects.js';
|
||||
import { isHybridOutput } from '../lib/prerender.js';
|
||||
|
||||
const PACKAGE_NAME = '@astrojs/vercel/static';
|
||||
|
||||
|
@ -54,7 +55,7 @@ export default function vercelStatic({
|
|||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
|
||||
if (config.output === 'server') {
|
||||
if (config.output === 'server' || isHybridOutput(config)) {
|
||||
throw new Error(`${PACKAGE_NAME} should be used with output: 'static'`);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
export const prerender = true
|
||||
export const prerender = import.meta.env.PRERENDER;
|
||||
---
|
||||
|
||||
<html>
|
||||
|
|
|
@ -6,6 +6,7 @@ describe('Serverless prerender', () => {
|
|||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
process.env.PRERENDER = true;
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/serverless-prerender/',
|
||||
});
|
||||
|
@ -16,3 +17,24 @@ describe('Serverless prerender', () => {
|
|||
expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Serverless hybrid rendering', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
process.env.PRERENDER = true;
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/serverless-prerender/',
|
||||
output:'hybrid',
|
||||
experimental:{
|
||||
hybridOutput: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('build successful', async () => {
|
||||
await fixture.build();
|
||||
expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3586,6 +3586,9 @@ importers:
|
|||
mocha:
|
||||
specifier: ^9.2.2
|
||||
version: 9.2.2
|
||||
slash:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
wrangler:
|
||||
specifier: ^2.0.23
|
||||
version: 2.0.23
|
||||
|
|
Loading…
Reference in a new issue