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
|
* @docs
|
||||||
* @name output
|
* @name output
|
||||||
* @type {('static' | 'server')}
|
* @type {('static' | 'server' | 'hybrid')}
|
||||||
* @default `'static'`
|
* @default `'static'`
|
||||||
* @see adapter
|
* @see adapter
|
||||||
* @description
|
* @description
|
||||||
|
@ -566,6 +566,7 @@ export interface AstroUserConfig {
|
||||||
*
|
*
|
||||||
* - 'static' - Building a static site to be deploy to any static host.
|
* - '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).
|
* - '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
|
* ```js
|
||||||
* import { defineConfig } from 'astro/config';
|
* import { defineConfig } from 'astro/config';
|
||||||
|
@ -575,7 +576,7 @@ export interface AstroUserConfig {
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
output?: 'static' | 'server';
|
output?: 'static' | 'server' | 'hybrid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
|
@ -616,14 +617,14 @@ export interface AstroUserConfig {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @default `'./dist/client'`
|
* @default `'./dist/client'`
|
||||||
* @description
|
* @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.
|
* `outDir` controls where the code is built to.
|
||||||
*
|
*
|
||||||
* This value is relative to the `outDir`.
|
* This value is relative to the `outDir`.
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* {
|
* {
|
||||||
* output: 'server',
|
* output: 'server', // or 'hybrid'
|
||||||
* build: {
|
* build: {
|
||||||
* client: './client'
|
* client: './client'
|
||||||
* }
|
* }
|
||||||
|
@ -1121,6 +1122,44 @@ export interface AstroUserConfig {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
middleware?: boolean;
|
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
|
// Legacy options to be removed
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { prependForwardSlash } from '../core/path.js';
|
||||||
import { getConfiguredImageService, isESMImportedImage } from './internal.js';
|
import { getConfiguredImageService, isESMImportedImage } from './internal.js';
|
||||||
import type { LocalImageService } from './services/service.js';
|
import type { LocalImageService } from './services/service.js';
|
||||||
import type { ImageTransform } from './types.js';
|
import type { ImageTransform } from './types.js';
|
||||||
|
import { isHybridOutput } from '../prerender/utils.js';
|
||||||
|
|
||||||
interface GenerationDataUncached {
|
interface GenerationDataUncached {
|
||||||
cached: false;
|
cached: false;
|
||||||
|
@ -46,7 +47,7 @@ export async function generateImage(
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverRoot: URL, clientRoot: URL;
|
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;
|
serverRoot = buildOpts.settings.config.build.server;
|
||||||
clientRoot = buildOpts.settings.config.build.client;
|
clientRoot = buildOpts.settings.config.build.client;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import { isLocalService, type ImageService } from './services/service.js';
|
import { isLocalService, type ImageService } from './services/service.js';
|
||||||
import type { GetImageResult, ImageMetadata, ImageTransform } from './types.js';
|
import type { GetImageResult, ImageMetadata, ImageTransform } from './types.js';
|
||||||
|
import { isHybridOutput } from '../prerender/utils.js';
|
||||||
|
|
||||||
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
|
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
|
||||||
return typeof src === 'object';
|
return typeof src === 'object';
|
||||||
|
|
|
@ -51,6 +51,7 @@ import type {
|
||||||
StylesheetAsset,
|
StylesheetAsset,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
import { isHybridOutput } from '../../prerender/utils.js';
|
||||||
|
|
||||||
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
|
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
|
||||||
return (
|
return (
|
||||||
|
@ -89,7 +90,7 @@ export function chunkIsPage(
|
||||||
|
|
||||||
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
|
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||||
const timer = performance.now();
|
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 serverEntry = opts.buildConfig.serverEntry;
|
||||||
const outFolder = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
const outFolder = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
||||||
|
|
||||||
|
@ -227,7 +228,7 @@ async function getPathsForRoute(
|
||||||
route: pageData.route,
|
route: pageData.route,
|
||||||
isValidate: false,
|
isValidate: false,
|
||||||
logging: opts.logging,
|
logging: opts.logging,
|
||||||
ssr: opts.settings.config.output === 'server',
|
ssr: opts.settings.config.output === 'server' || isHybridOutput(opts.settings.config),
|
||||||
})
|
})
|
||||||
.then((_result) => {
|
.then((_result) => {
|
||||||
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
|
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(
|
const url = getUrlForPath(
|
||||||
pathname,
|
pathname,
|
||||||
opts.settings.config.base,
|
opts.settings.config.base,
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import type { AstroTelemetry } from '@astrojs/telemetry';
|
import type { AstroTelemetry } from '@astrojs/telemetry';
|
||||||
import type { AstroSettings, BuildConfig, ManifestData, RuntimeMode } from '../../@types/astro';
|
import type {
|
||||||
import type { LogOptions } from '../logger/core';
|
AstroConfig,
|
||||||
|
AstroSettings,
|
||||||
|
BuildConfig,
|
||||||
|
ManifestData,
|
||||||
|
RuntimeMode,
|
||||||
|
} from '../../@types/astro';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as colors from 'kleur/colors';
|
import * as colors from 'kleur/colors';
|
||||||
|
@ -14,7 +19,7 @@ import {
|
||||||
runHookConfigSetup,
|
runHookConfigSetup,
|
||||||
} from '../../integrations/index.js';
|
} from '../../integrations/index.js';
|
||||||
import { createVite } from '../create-vite.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 { printHelp } from '../messages.js';
|
||||||
import { apply as applyPolyfill } from '../polyfill.js';
|
import { apply as applyPolyfill } from '../polyfill.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
|
@ -233,7 +238,7 @@ class AstroBuilder {
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
buildMode: 'static' | 'server';
|
buildMode: AstroConfig['output'];
|
||||||
}) {
|
}) {
|
||||||
const total = getTimeStat(timeStart, performance.now());
|
const total = getTimeStat(timeStart, performance.now());
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../inter
|
||||||
import type { AstroBuildPlugin } from '../plugin';
|
import type { AstroBuildPlugin } from '../plugin';
|
||||||
import type { StaticBuildOptions } from '../types';
|
import type { StaticBuildOptions } from '../types';
|
||||||
|
|
||||||
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||||
return {
|
return {
|
||||||
name: '@astro/plugin-build-pages',
|
name: '@astro/plugin-build-pages',
|
||||||
|
|
||||||
options(options) {
|
options(options) {
|
||||||
if (opts.settings.config.output === 'static' || hasPrerenderedPages(internals)) {
|
if (opts.settings.config.output === 'static') {
|
||||||
return addRollupInput(options, [pagesVirtualModuleId]);
|
return addRollupInput(options, [pagesVirtualModuleId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,10 +4,7 @@ import type { AstroBuildPlugin } from '../plugin.js';
|
||||||
import type { StaticBuildOptions } from '../types';
|
import type { StaticBuildOptions } from '../types';
|
||||||
import { extendManualChunks } from './util.js';
|
import { extendManualChunks } from './util.js';
|
||||||
|
|
||||||
export function vitePluginPrerender(
|
function vitePluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||||
opts: StaticBuildOptions,
|
|
||||||
internals: BuildInternals
|
|
||||||
): VitePlugin {
|
|
||||||
return {
|
return {
|
||||||
name: 'astro:rollup-plugin-prerender',
|
name: 'astro:rollup-plugin-prerender',
|
||||||
|
|
||||||
|
@ -26,6 +23,7 @@ export function vitePluginPrerender(
|
||||||
pageInfo.route.prerender = true;
|
pageInfo.route.prerender = true;
|
||||||
return 'prerender';
|
return 'prerender';
|
||||||
}
|
}
|
||||||
|
pageInfo.route.prerender = false;
|
||||||
// dynamic pages should all go in their own chunk in the pages/* directory
|
// dynamic pages should all go in their own chunk in the pages/* directory
|
||||||
return `pages/all`;
|
return `pages/all`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
|
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
|
||||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
|
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
|
||||||
import type { BuildInternals } from '../internal.js';
|
|
||||||
import type { StaticBuildOptions } from '../types';
|
import type { StaticBuildOptions } from '../types';
|
||||||
|
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
|
@ -13,15 +12,16 @@ import { joinPaths, prependForwardSlash } from '../../path.js';
|
||||||
import { serializeRouteData } from '../../routing/index.js';
|
import { serializeRouteData } from '../../routing/index.js';
|
||||||
import { addRollupInput } from '../add-rollup-input.js';
|
import { addRollupInput } from '../add-rollup-input.js';
|
||||||
import { getOutFile, getOutFolder } from '../common.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 type { AstroBuildPlugin } from '../plugin';
|
||||||
|
import { isHybridOutput } from '../../../prerender/utils.js';
|
||||||
|
|
||||||
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||||
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
||||||
|
|
||||||
export function vitePluginSSR(
|
function vitePluginSSR(
|
||||||
internals: BuildInternals,
|
internals: BuildInternals,
|
||||||
adapter: AstroAdapter,
|
adapter: AstroAdapter,
|
||||||
config: AstroConfig
|
config: AstroConfig
|
||||||
|
@ -249,7 +249,8 @@ export function pluginSSR(
|
||||||
options: StaticBuildOptions,
|
options: StaticBuildOptions,
|
||||||
internals: BuildInternals
|
internals: BuildInternals
|
||||||
): AstroBuildPlugin {
|
): AstroBuildPlugin {
|
||||||
const ssr = options.settings.config.output === 'server';
|
const ssr =
|
||||||
|
options.settings.config.output === 'server' || isHybridOutput(options.settings.config);
|
||||||
return {
|
return {
|
||||||
build: 'ssr',
|
build: 'ssr',
|
||||||
hooks: {
|
hooks: {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.
|
||||||
import { registerAllPlugins } from './plugins/index.js';
|
import { registerAllPlugins } from './plugins/index.js';
|
||||||
import type { PageBuildData, StaticBuildOptions } from './types';
|
import type { PageBuildData, StaticBuildOptions } from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
import { isHybridOutput } from '../../prerender/utils.js';
|
||||||
|
|
||||||
export async function viteBuild(opts: StaticBuildOptions) {
|
export async function viteBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, settings } = opts;
|
const { allPages, settings } = opts;
|
||||||
|
@ -111,15 +112,16 @@ export async function viteBuild(opts: StaticBuildOptions) {
|
||||||
|
|
||||||
export async function staticBuild(opts: StaticBuildOptions, internals: BuildInternals) {
|
export async function staticBuild(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||||
const { settings } = opts;
|
const { settings } = opts;
|
||||||
switch (settings.config.output) {
|
const hybridOutput = isHybridOutput(settings.config);
|
||||||
case 'static': {
|
switch (true) {
|
||||||
|
case settings.config.output === 'static': {
|
||||||
settings.timer.start('Static generate');
|
settings.timer.start('Static generate');
|
||||||
await generatePages(opts, internals);
|
await generatePages(opts, internals);
|
||||||
await cleanServerOutput(opts);
|
await cleanServerOutput(opts);
|
||||||
settings.timer.end('Static generate');
|
settings.timer.end('Static generate');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'server': {
|
case settings.config.output === 'server' || hybridOutput: {
|
||||||
settings.timer.start('Server generate');
|
settings.timer.start('Server generate');
|
||||||
await generatePages(opts, internals);
|
await generatePages(opts, internals);
|
||||||
await cleanStaticOutput(opts, internals);
|
await cleanStaticOutput(opts, internals);
|
||||||
|
@ -138,7 +140,7 @@ async function ssrBuild(
|
||||||
container: AstroBuildPluginContainer
|
container: AstroBuildPluginContainer
|
||||||
) {
|
) {
|
||||||
const { settings, viteConfig } = opts;
|
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 out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(settings.config.outDir);
|
||||||
|
|
||||||
const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
|
const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
|
||||||
|
@ -207,7 +209,7 @@ async function clientBuild(
|
||||||
) {
|
) {
|
||||||
const { settings, viteConfig } = opts;
|
const { settings, viteConfig } = opts;
|
||||||
const timer = performance.now();
|
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);
|
const out = ssr ? opts.buildConfig.client : getOutDirWithinCwd(settings.config.outDir);
|
||||||
|
|
||||||
// Nothing to do if there is no client-side JS.
|
// Nothing to do if there is no client-side JS.
|
||||||
|
@ -273,7 +275,7 @@ async function runPostBuildHooks(
|
||||||
const buildConfig = container.options.settings.config.build;
|
const buildConfig = container.options.settings.config.build;
|
||||||
for (const [fileName, mutation] of mutations) {
|
for (const [fileName, mutation] of mutations) {
|
||||||
const root =
|
const root =
|
||||||
config.output === 'server'
|
config.output === 'server' || isHybridOutput(config)
|
||||||
? mutation.build === 'server'
|
? mutation.build === 'server'
|
||||||
? buildConfig.server
|
? buildConfig.server
|
||||||
: buildConfig.client
|
: buildConfig.client
|
||||||
|
@ -294,7 +296,7 @@ async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInter
|
||||||
if (pageData.route.prerender)
|
if (pageData.route.prerender)
|
||||||
allStaticFiles.add(internals.pageToBundleMap.get(pageData.moduleSpecifier));
|
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);
|
const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
||||||
// The SSR output is all .mjs files, the client output is not.
|
// The SSR output is all .mjs files, the client output is not.
|
||||||
const files = await glob('**/*.mjs', {
|
const files = await glob('**/*.mjs', {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type { LogOptions } from '../logger/core.js';
|
||||||
import { arraify, isObject, isURL } from '../util.js';
|
import { arraify, isObject, isURL } from '../util.js';
|
||||||
import { createRelativeSchema } from './schema.js';
|
import { createRelativeSchema } from './schema.js';
|
||||||
import { loadConfigWithVite } from './vite-load.js';
|
import { loadConfigWithVite } from './vite-load.js';
|
||||||
|
import { isHybridMalconfigured } from '../../prerender/utils.js';
|
||||||
|
|
||||||
export const LEGACY_ASTRO_CONFIG_KEYS = new Set([
|
export const LEGACY_ASTRO_CONFIG_KEYS = new Set([
|
||||||
'projectRoot',
|
'projectRoot',
|
||||||
|
@ -223,6 +224,12 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
|
||||||
}
|
}
|
||||||
const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd);
|
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 {
|
return {
|
||||||
astroConfig,
|
astroConfig,
|
||||||
userConfig,
|
userConfig,
|
||||||
|
|
|
@ -38,6 +38,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
||||||
legacy: {},
|
legacy: {},
|
||||||
experimental: {
|
experimental: {
|
||||||
assets: false,
|
assets: false,
|
||||||
|
hybridOutput: false,
|
||||||
customClientDirecives: false,
|
customClientDirecives: false,
|
||||||
inlineStylesheets: 'never',
|
inlineStylesheets: 'never',
|
||||||
middleware: false,
|
middleware: false,
|
||||||
|
@ -77,7 +78,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.trailingSlash),
|
.default(ASTRO_CONFIG_DEFAULTS.trailingSlash),
|
||||||
output: z
|
output: z
|
||||||
.union([z.literal('static'), z.literal('server')])
|
.union([z.literal('static'), z.literal('server'), z.literal('hybrid')])
|
||||||
.optional()
|
.optional()
|
||||||
.default('static'),
|
.default('static'),
|
||||||
scopedStyleStrategy: z
|
scopedStyleStrategy: z
|
||||||
|
@ -205,6 +206,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.inlineStylesheets),
|
.default(ASTRO_CONFIG_DEFAULTS.experimental.inlineStylesheets),
|
||||||
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
|
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
|
||||||
|
hybridOutput: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.hybridOutput),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.default({}),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { getDefaultClientDirectives } from '../client-directive/index.js';
|
||||||
import { createDefaultDevConfig } from './config.js';
|
import { createDefaultDevConfig } from './config.js';
|
||||||
import { AstroTimer } from './timer.js';
|
import { AstroTimer } from './timer.js';
|
||||||
import { loadTSConfig } from './tsconfig.js';
|
import { loadTSConfig } from './tsconfig.js';
|
||||||
|
import { isHybridOutput } from '../../prerender/utils.js';
|
||||||
|
|
||||||
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||||
return {
|
return {
|
||||||
|
@ -17,7 +18,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||||
|
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
injectedRoutes:
|
injectedRoutes:
|
||||||
config.experimental.assets && config.output === 'server'
|
config.experimental.assets && (config.output === 'server' || isHybridOutput(config))
|
||||||
? [{ pattern: '/_image', entryPoint: 'astro/assets/image-endpoint' }]
|
? [{ pattern: '/_image', entryPoint: 'astro/assets/image-endpoint' }]
|
||||||
: [],
|
: [],
|
||||||
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
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 { warn, type LogOptions } from '../logger/core.js';
|
||||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { isValueSerializable } from '../render/core.js';
|
import { isValueSerializable } from '../render/core.js';
|
||||||
|
import { isHybridOutput } from '../../prerender/utils.js';
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
@ -168,7 +169,7 @@ function isRedirect(statusCode: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throwIfRedirectNotAllowed(response: Response, config: AstroConfig) {
|
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);
|
throw new AstroError(AstroErrorData.StaticRedirectNotAvailable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const AstroErrorData = {
|
||||||
title: '`Astro.redirect` is not available in static mode.',
|
title: '`Astro.redirect` is not available in static mode.',
|
||||||
code: 3001,
|
code: 3001,
|
||||||
message:
|
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.',
|
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.',
|
title: '`Astro.clientAddress` is not available in static mode.',
|
||||||
code: 3003,
|
code: 3003,
|
||||||
message:
|
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.',
|
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: {
|
NoAdapterInstalled: {
|
||||||
title: 'Cannot use Server-side Rendering without an adapter.',
|
title: 'Cannot use Server-side Rendering without an adapter.',
|
||||||
code: 3017,
|
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.',
|
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: {
|
InvalidPrerenderExport: {
|
||||||
title: 'Invalid prerender export.',
|
title: 'Invalid prerender export.',
|
||||||
code: 3019,
|
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.`;
|
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 (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;
|
return msg;
|
||||||
},
|
},
|
||||||
hint: 'Mutable values declared at runtime are not supported. Please make sure to use exactly `export const prerender = true`.',
|
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 type { AstroSettings, RuntimeMode } from '../../../@types/astro';
|
||||||
|
import { isHybridOutput } from '../../../prerender/utils.js';
|
||||||
import type { LogOptions } from '../../logger/core.js';
|
import type { LogOptions } from '../../logger/core.js';
|
||||||
import type { ModuleLoader } from '../../module-loader/index';
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
import type { Environment } from '../index';
|
import type { Environment } from '../index';
|
||||||
|
@ -29,7 +30,7 @@ export function createDevelopmentEnvironment(
|
||||||
resolve: createResolve(loader, settings.config.root),
|
resolve: createResolve(loader, settings.config.root),
|
||||||
routeCache: new RouteCache(logging, mode),
|
routeCache: new RouteCache(logging, mode),
|
||||||
site: settings.config.site,
|
site: settings.config.site,
|
||||||
ssr: settings.config.output === 'server',
|
ssr: settings.config.output === 'server' || isHybridOutput(settings.config),
|
||||||
streaming: true,
|
streaming: true,
|
||||||
telemetry: Boolean(settings.forceDisableTelemetry),
|
telemetry: Boolean(settings.forceDisableTelemetry),
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { warn } from '../../logger/core.js';
|
||||||
import { removeLeadingForwardSlash } from '../../path.js';
|
import { removeLeadingForwardSlash } from '../../path.js';
|
||||||
import { resolvePages } from '../../util.js';
|
import { resolvePages } from '../../util.js';
|
||||||
import { getRouteGenerator } from './generator.js';
|
import { getRouteGenerator } from './generator.js';
|
||||||
|
import { isHybridOutput } from '../../../prerender/utils.js';
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
|
@ -226,6 +227,7 @@ export function createRouteManifest(
|
||||||
]);
|
]);
|
||||||
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
||||||
const localFs = fsMod ?? nodeFs;
|
const localFs = fsMod ?? nodeFs;
|
||||||
|
const isPrenderDefault = isHybridOutput(settings.config);
|
||||||
|
|
||||||
function walk(
|
function walk(
|
||||||
fs: typeof nodeFs,
|
fs: typeof nodeFs,
|
||||||
|
@ -322,7 +324,6 @@ export function createRouteManifest(
|
||||||
const route = `/${segments
|
const route = `/${segments
|
||||||
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
|
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
|
||||||
.join('/')}`.toLowerCase();
|
.join('/')}`.toLowerCase();
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
route,
|
route,
|
||||||
type: item.isPage ? 'page' : 'endpoint',
|
type: item.isPage ? 'page' : 'endpoint',
|
||||||
|
@ -332,7 +333,7 @@ export function createRouteManifest(
|
||||||
component,
|
component,
|
||||||
generate,
|
generate,
|
||||||
pathname: pathname || undefined,
|
pathname: pathname || undefined,
|
||||||
prerender: false,
|
prerender: isPrenderDefault,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -408,7 +409,7 @@ export function createRouteManifest(
|
||||||
component,
|
component,
|
||||||
generate,
|
generate,
|
||||||
pathname: pathname || void 0,
|
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 { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
|
||||||
import type { ModuleLoader } from './module-loader';
|
import type { ModuleLoader } from './module-loader';
|
||||||
import { prependForwardSlash, removeTrailingForwardSlash } from './path.js';
|
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). */
|
/** Returns true if argument is an object of any prototype/class (but not null). */
|
||||||
export function isObject(value: unknown): value is Record<string, any> {
|
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 {
|
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) {
|
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 { mergeConfig } from '../core/config/config.js';
|
||||||
import { info, type LogOptions } from '../core/logger/core.js';
|
import { info, type LogOptions } from '../core/logger/core.js';
|
||||||
import { mdxContentEntryType } from '../vite-plugin-markdown/content-entry-type.js';
|
import { mdxContentEntryType } from '../vite-plugin-markdown/content-entry-type.js';
|
||||||
|
import { isHybridOutput } from '../prerender/utils.js';
|
||||||
|
|
||||||
async function withTakingALongTimeMsg<T>({
|
async function withTakingALongTimeMsg<T>({
|
||||||
name,
|
name,
|
||||||
|
@ -329,7 +330,8 @@ export async function runHookBuildGenerated({
|
||||||
buildConfig: BuildConfig;
|
buildConfig: BuildConfig;
|
||||||
logging: LogOptions;
|
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) {
|
for (const integration of config.integrations) {
|
||||||
if (integration?.hooks?.['astro:build:generated']) {
|
if (integration?.hooks?.['astro:build:generated']) {
|
||||||
|
@ -355,7 +357,8 @@ export async function runHookBuildDone({
|
||||||
routes: RouteData[];
|
routes: RouteData[];
|
||||||
logging: LogOptions;
|
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 });
|
await fs.promises.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
for (const integration of config.integrations) {
|
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') {
|
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`
|
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') {
|
if (!handler || typeof handler !== 'function') {
|
||||||
// No handler found, so this should be a 404. Using a custom header
|
// 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 { runWithErrorHandling } from './controller.js';
|
||||||
import { handle500Response } from './response.js';
|
import { handle500Response } from './response.js';
|
||||||
import { handleRoute, matchRoute } from './route.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. */
|
/** The main logic to route dev server requests to pages in Astro. */
|
||||||
export async function handleRequest(
|
export async function handleRequest(
|
||||||
|
@ -24,7 +25,7 @@ export async function handleRequest(
|
||||||
const { settings, loader: moduleLoader } = env;
|
const { settings, loader: moduleLoader } = env;
|
||||||
const { config } = settings;
|
const { config } = settings;
|
||||||
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${req.headers.host}`;
|
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);
|
const url = new URL(origin + req.url);
|
||||||
let pathname: string;
|
let pathname: string;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { createRequest } from '../core/request.js';
|
||||||
import { matchAllRoutes } from '../core/routing/index.js';
|
import { matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { log404 } from './common.js';
|
import { log404 } from './common.js';
|
||||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||||
|
import { isHybridOutput } from '../prerender/utils.js';
|
||||||
|
|
||||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||||
...args: any
|
...args: any
|
||||||
|
@ -58,7 +59,7 @@ export async function matchRoute(
|
||||||
routeCache,
|
routeCache,
|
||||||
pathname: pathname,
|
pathname: pathname,
|
||||||
logging,
|
logging,
|
||||||
ssr: settings.config.output === 'server',
|
ssr: settings.config.output === 'server' || isHybridOutput(settings.config),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) {
|
if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
|
@ -131,7 +132,7 @@ export async function handleRoute(
|
||||||
const { config } = settings;
|
const { config } = settings;
|
||||||
const filePath: URL | undefined = matchedRoute.filePath;
|
const filePath: URL | undefined = matchedRoute.filePath;
|
||||||
const { route, preloadedComponent, mod } = matchedRoute;
|
const { route, preloadedComponent, mod } = matchedRoute;
|
||||||
const buildingToSSR = config.output === 'server';
|
const buildingToSSR = config.output === 'server' || isHybridOutput(config);
|
||||||
|
|
||||||
// Headers are only available when using SSR.
|
// Headers are only available when using SSR.
|
||||||
const request = createRequest({
|
const request = createRequest({
|
||||||
|
@ -157,7 +158,7 @@ export async function handleRoute(
|
||||||
routeCache: env.routeCache,
|
routeCache: env.routeCache,
|
||||||
pathname: pathname,
|
pathname: pathname,
|
||||||
logging,
|
logging,
|
||||||
ssr: config.output === 'server',
|
ssr: config.output === 'server' || isHybridOutput(config),
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: SSROptions = {
|
const options: SSROptions = {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { AstroSettings } from '../@types/astro.js';
|
||||||
import { isEndpoint, isPage } from '../core/util.js';
|
import { isEndpoint, isPage } from '../core/util.js';
|
||||||
|
|
||||||
import { scan } from './scan.js';
|
import { scan } from './scan.js';
|
||||||
|
import { isHybridOutput } from '../prerender/utils.js';
|
||||||
|
|
||||||
export default function astroScannerPlugin({ settings }: { settings: AstroSettings }): VitePlugin {
|
export default function astroScannerPlugin({ settings }: { settings: AstroSettings }): VitePlugin {
|
||||||
return {
|
return {
|
||||||
|
@ -24,7 +25,12 @@ export default function astroScannerPlugin({ settings }: { settings: AstroSettin
|
||||||
const fileIsPage = isPage(fileURL, settings);
|
const fileIsPage = isPage(fileURL, settings);
|
||||||
const fileIsEndpoint = isEndpoint(fileURL, settings);
|
const fileIsEndpoint = isEndpoint(fileURL, settings);
|
||||||
if (!(fileIsPage || fileIsEndpoint)) return;
|
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) ?? {};
|
const { meta = {} } = this.getModuleInfo(id) ?? {};
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as eslexer from 'es-module-lexer';
|
import * as eslexer from 'es-module-lexer';
|
||||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import type { PageOptions } from '../vite-plugin-astro/types.js';
|
import type { PageOptions } from '../vite-plugin-astro/types.js';
|
||||||
|
import type { AstroSettings } from '../@types/astro.js';
|
||||||
|
|
||||||
const BOOLEAN_EXPORTS = new Set(['prerender']);
|
const BOOLEAN_EXPORTS = new Set(['prerender']);
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ function isFalsy(value: string) {
|
||||||
|
|
||||||
let didInit = false;
|
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 (!includesExport(code)) return {};
|
||||||
if (!didInit) {
|
if (!didInit) {
|
||||||
await eslexer.init;
|
await eslexer.init;
|
||||||
|
@ -45,6 +46,7 @@ export async function scan(code: string, id: string): Promise<PageOptions> {
|
||||||
let pageOptions: PageOptions = {};
|
let pageOptions: PageOptions = {};
|
||||||
for (const _export of exports) {
|
for (const _export of exports) {
|
||||||
const { n: name, le: endOfLocalName } = _export;
|
const { n: name, le: endOfLocalName } = _export;
|
||||||
|
// mark that a `prerender` export was found
|
||||||
if (BOOLEAN_EXPORTS.has(name)) {
|
if (BOOLEAN_EXPORTS.has(name)) {
|
||||||
// For a given export, check the value of the local declaration
|
// For a given export, check the value of the local declaration
|
||||||
// Basically extract the `const` from the statement `export const prerender = true`
|
// 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))) {
|
if (prefix !== 'const' || !(isTruthy(suffix) || isFalsy(suffix))) {
|
||||||
throw new AstroError({
|
throw new AstroError({
|
||||||
...AstroErrorData.InvalidPrerenderExport,
|
...AstroErrorData.InvalidPrerenderExport,
|
||||||
message: AstroErrorData.InvalidPrerenderExport.message(prefix, suffix),
|
message: AstroErrorData.InvalidPrerenderExport.message(prefix, suffix, isHybridOutput),
|
||||||
location: { file: id },
|
location: { file: id },
|
||||||
});
|
});
|
||||||
} else {
|
} 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",
|
"chai": "^4.3.6",
|
||||||
"cheerio": "^1.0.0-rc.11",
|
"cheerio": "^1.0.0-rc.11",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
|
"slash": "^4.0.0",
|
||||||
"wrangler": "^2.0.23"
|
"wrangler": "^2.0.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
|
|
||||||
if (config.output === 'static') {
|
if (config.output === 'static') {
|
||||||
throw new Error(`
|
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>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
|
@ -1,19 +1,60 @@
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import slash from 'slash';
|
||||||
|
|
||||||
describe('Prerendering', () => {
|
describe('Prerendering', () => {
|
||||||
/** @type {import('./test-utils').Fixture} */
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
process.env.PRERENDER = true;
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/prerender/',
|
root: './fixtures/prerender/',
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
delete process.env.PRERENDER;
|
||||||
|
fixture.clean();
|
||||||
|
});
|
||||||
|
|
||||||
it('includes prerendered routes in the routes.json config', async () => {
|
it('includes prerendered routes in the routes.json config', async () => {
|
||||||
const routes = JSON.parse(await fixture.readFile('/_routes.json'));
|
const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json')).exclude.map((r) =>
|
||||||
expect(routes.exclude).to.include('/one/');
|
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';
|
export { fixLineEndings } from '../../../astro/test/test-utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
||||||
|
*/
|
||||||
|
|
||||||
export function loadFixture(config) {
|
export function loadFixture(config) {
|
||||||
if (config?.root) {
|
if (config?.root) {
|
||||||
config.root = new URL(config.root, import.meta.url);
|
config.root = new URL(config.root, import.meta.url);
|
||||||
|
|
|
@ -62,7 +62,9 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
_buildConfig = config.build;
|
_buildConfig = config.build;
|
||||||
|
|
||||||
if (config.output === 'static') {
|
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(
|
console.warn(
|
||||||
`[@astrojs/deno] Otherwise, this adapter is not required to deploy a static site to Deno.`
|
`[@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 type { LoggerLevel } from './utils/logger.js';
|
||||||
import { joinPaths, prependForwardSlash, propsToFilename } from './utils/paths.js';
|
import { joinPaths, prependForwardSlash, propsToFilename } from './utils/paths.js';
|
||||||
import { createPlugin } from './vite-plugin-astro-image.js';
|
import { createPlugin } from './vite-plugin-astro-image.js';
|
||||||
|
import { isHybridOutput } from './utils/prerender.js';
|
||||||
|
|
||||||
export { getImage } from './lib/get-image.js';
|
export { getImage } from './lib/get-image.js';
|
||||||
export { getPicture } from './lib/get-picture.js';
|
export { getPicture } from './lib/get-picture.js';
|
||||||
|
@ -84,7 +85,7 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
||||||
vite: getViteConfiguration(command === 'dev'),
|
vite: getViteConfiguration(command === 'dev'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (command === 'dev' || config.output === 'server') {
|
if (command === 'dev' || config.output === 'server' || isHybridOutput(config)) {
|
||||||
injectRoute({
|
injectRoute({
|
||||||
pattern: ROUTE_PATTERN,
|
pattern: ROUTE_PATTERN,
|
||||||
entryPoint: '@astrojs/image/endpoint',
|
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/, '');
|
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
|
||||||
|
|
||||||
if (config.output === 'static') {
|
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(
|
console.warn(
|
||||||
`[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`
|
`[@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/, '');
|
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
|
||||||
|
|
||||||
if (config.output === 'static') {
|
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(
|
console.warn(
|
||||||
`[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`
|
`[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
export { fromFileUrl } from 'https://deno.land/std@0.110.0/path/mod.ts';
|
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/x/deno_dom/deno-dom-wasm.ts';
|
||||||
export * from 'https://deno.land/std@0.142.0/streams/conversion.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({
|
Deno.test({
|
||||||
name: 'Dynamic imports',
|
name: 'Dynamic imports',
|
||||||
async fn() {
|
async fn() {
|
||||||
let close = await runBuild('./fixtures/dynimport/');
|
await runBuild('./fixtures/dynimport/');
|
||||||
let stop = await runApp('./fixtures/dynimport/prod.js');
|
const stop = await runApp('./fixtures/dynimport/prod.js');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:8085/');
|
const response = await fetch('http://127.0.0.1:8085/');
|
||||||
|
@ -20,7 +20,6 @@ Deno.test({
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
|
||||||
await stop();
|
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';
|
import { assertEquals, assert, DOMParser } from './deps.ts';
|
||||||
|
|
||||||
Deno.env.set('SECRET_STUFF', 'secret');
|
Deno.env.set('SECRET_STUFF', 'secret');
|
||||||
|
@ -10,7 +10,8 @@ Deno.test({
|
||||||
name: 'Edge Basics',
|
name: 'Edge Basics',
|
||||||
skip: true,
|
skip: true,
|
||||||
async fn() {
|
async fn() {
|
||||||
let close = await runBuild('./fixtures/edge-basic/');
|
const fixture = loadFixture('./fixtures/edge-basic/');
|
||||||
|
await fixture.runBuild();
|
||||||
const { default: handler } = await import(
|
const { default: handler } = await import(
|
||||||
'./fixtures/edge-basic/.netlify/edge-functions/entry.js'
|
'./fixtures/edge-basic/.netlify/edge-functions/entry.js'
|
||||||
);
|
);
|
||||||
|
@ -26,6 +27,6 @@ Deno.test({
|
||||||
const envDiv = doc.querySelector('#env');
|
const envDiv = doc.querySelector('#env');
|
||||||
assertEquals(envDiv?.innerText, 'secret');
|
assertEquals(envDiv?.innerText, 'secret');
|
||||||
|
|
||||||
await close();
|
await fixture.cleanup();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from "astro/config";
|
||||||
import { netlifyEdgeFunctions } from '@astrojs/netlify';
|
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({
|
export default defineConfig({
|
||||||
adapter: netlifyEdgeFunctions({
|
adapter: netlifyEdgeFunctions({
|
||||||
dist: new URL('./dist/', import.meta.url),
|
dist: new URL("./dist/", import.meta.url),
|
||||||
}),
|
}),
|
||||||
output: 'server',
|
...partialConfig,
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
export const prerender = true
|
export const prerender = import.meta.env.PRERENDER;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
|
|
@ -1,15 +1,76 @@
|
||||||
import { runBuild } from './test-utils.ts';
|
import { loadFixture } from './test-utils.ts';
|
||||||
import { assertEquals } from './deps.ts';
|
import { assertEquals, assertExists, cheerio, fs } from './deps.ts';
|
||||||
|
|
||||||
Deno.test({
|
Deno.test({
|
||||||
name: 'Prerender',
|
name: 'Prerender',
|
||||||
async fn() {
|
async fn(t) {
|
||||||
let close = await runBuild('./fixtures/prerender/');
|
const environmentVariables = {
|
||||||
const { default: handler } = await import(
|
PRERENDER: 'true',
|
||||||
'./fixtures/prerender/.netlify/edge-functions/entry.js'
|
};
|
||||||
);
|
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
|
||||||
const response = await handler(new Request('http://example.com/index.html'));
|
await fixture.runBuild();
|
||||||
assertEquals(response, undefined, 'No response because this is an asset');
|
|
||||||
await close();
|
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';
|
import { assertEquals, assert, DOMParser } from './deps.ts';
|
||||||
|
|
||||||
Deno.test({
|
Deno.test({
|
||||||
|
@ -6,12 +6,14 @@ Deno.test({
|
||||||
ignore: true,
|
ignore: true,
|
||||||
name: 'Assets are preferred over HTML routes',
|
name: 'Assets are preferred over HTML routes',
|
||||||
async fn() {
|
async fn() {
|
||||||
let close = await runBuild('./fixtures/root-dynamic/');
|
const fixture = loadFixture('./fixtures/root-dynamic/');
|
||||||
|
await fixture.runBuild();
|
||||||
|
|
||||||
const { default: handler } = await import(
|
const { default: handler } = await import(
|
||||||
'./fixtures/root-dynamic/.netlify/edge-functions/entry.js'
|
'./fixtures/root-dynamic/.netlify/edge-functions/entry.js'
|
||||||
);
|
);
|
||||||
const response = await handler(new Request('http://example.com/styles.css'));
|
const response = await handler(new Request('http://example.com/styles.css'));
|
||||||
assertEquals(response, undefined, 'No response because this is an asset');
|
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';
|
import { fromFileUrl, readableStreamFromReader } from './deps.ts';
|
||||||
const dir = new URL('./', import.meta.url);
|
const dir = new URL('./', import.meta.url);
|
||||||
|
|
||||||
export async function runBuild(fixturePath: string) {
|
export function loadFixture(fixturePath: string, envionmentVariables?: Record<string, string>) {
|
||||||
let proc = Deno.run({
|
async function runBuild() {
|
||||||
cmd: ['node', '../../../../../../astro/astro.js', 'build', '--silent'],
|
const proc = Deno.run({
|
||||||
cwd: fromFileUrl(new URL(fixturePath, dir)),
|
cmd: ['node', '../../../../../../astro/astro.js', 'build'],
|
||||||
});
|
env: envionmentVariables,
|
||||||
await proc.status();
|
cwd: fromFileUrl(new URL(fixturePath, dir)),
|
||||||
return async () => await proc.close();
|
});
|
||||||
}
|
await proc.status();
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import netlifyAdapter from '../../dist/index.js';
|
import netlifyAdapter from '../../dist/index.js';
|
||||||
import { loadFixture, testIntegration } from './test-utils.js';
|
import { loadFixture, testIntegration } from './test-utils.js';
|
||||||
|
import { after } from 'node:test';
|
||||||
|
|
||||||
describe('Mixed Prerendering with SSR', () => {
|
describe('Mixed Prerendering with SSR', () => {
|
||||||
/** @type {import('./test-utils').Fixture} */
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
process.env.PRERENDER = true;
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
|
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
|
||||||
output: 'server',
|
output: 'server',
|
||||||
|
@ -18,13 +20,56 @@ describe('Mixed Prerendering with SSR', () => {
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
delete process.env.PRERENDER;
|
||||||
|
});
|
||||||
|
|
||||||
it('Wildcard 404 is sorted last', async () => {
|
it('Wildcard 404 is sorted last', async () => {
|
||||||
const redir = await fixture.readFile('/_redirects');
|
const redir = await fixture.readFile('/_redirects');
|
||||||
const baseRouteIndex = redir.indexOf('/ /.netlify/functions/entry 200');
|
const baseRouteIndex = redir.indexOf('/ /.netlify/functions/entry 200');
|
||||||
const oneRouteIndex = redir.indexOf('/one /one/index.html 200');
|
const oneRouteIndex = redir.indexOf('/one /one/index.html 200');
|
||||||
const fourOhFourWildCardIndex = redir.indexOf('/* /.netlify/functions/entry 404');
|
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(baseRouteIndex);
|
||||||
expect(fourOhFourWildCardIndex).to.be.greaterThan(oneRouteIndex);
|
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));
|
setAdapter(getAdapter(_options));
|
||||||
|
|
||||||
if (config.output === 'static') {
|
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>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
|
@ -1,22 +1,27 @@
|
||||||
import nodejs from '../dist/index.js';
|
import nodejs from '../dist/index.js';
|
||||||
import { loadFixture, createRequestAndResponse } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { fetch } from 'undici';
|
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', () => {
|
describe('Prerendering', () => {
|
||||||
/** @type {import('./test-utils').Fixture} */
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
async function load() {
|
describe('With base', async () => {
|
||||||
const mod = await import('./fixtures/prerender/dist/server/entry.mjs');
|
|
||||||
return mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('With base', () => {
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||||
|
process.env.PRERENDER = true;
|
||||||
|
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
base: '/some-base',
|
base: '/some-base',
|
||||||
root: './fixtures/prerender/',
|
root: './fixtures/prerender/',
|
||||||
|
@ -31,6 +36,8 @@ describe('Prerendering', () => {
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await server.stop();
|
await server.stop();
|
||||||
|
await fixture.clean();
|
||||||
|
delete process.env.PRERENDER;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can render SSR route', async () => {
|
it('Can render SSR route', async () => {
|
||||||
|
@ -68,9 +75,12 @@ describe('Prerendering', () => {
|
||||||
expect(res.headers.get('location')).to.equal('/some-base/two/');
|
expect(res.headers.get('location')).to.equal('/some-base/two/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Without base', () => {
|
|
||||||
|
describe('Without base', async () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||||
|
process.env.PRERENDER = true;
|
||||||
|
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/prerender/',
|
root: './fixtures/prerender/',
|
||||||
output: 'server',
|
output: 'server',
|
||||||
|
@ -84,6 +94,8 @@ describe('Prerendering', () => {
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await server.stop();
|
await server.stop();
|
||||||
|
await fixture.clean();
|
||||||
|
delete process.env.PRERENDER;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can render SSR route', async () => {
|
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') {
|
if (config.output === 'static') {
|
||||||
throw new Error(`
|
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') {
|
if (config.output === 'static') {
|
||||||
throw new Error(`
|
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 { exposeEnv } from '../lib/env.js';
|
||||||
import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js';
|
import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js';
|
||||||
import { getRedirects } from '../lib/redirects.js';
|
import { getRedirects } from '../lib/redirects.js';
|
||||||
|
import { isHybridOutput } from '../lib/prerender.js';
|
||||||
|
|
||||||
const PACKAGE_NAME = '@astrojs/vercel/static';
|
const PACKAGE_NAME = '@astrojs/vercel/static';
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ export default function vercelStatic({
|
||||||
setAdapter(getAdapter());
|
setAdapter(getAdapter());
|
||||||
_config = config;
|
_config = config;
|
||||||
|
|
||||||
if (config.output === 'server') {
|
if (config.output === 'server' || isHybridOutput(config)) {
|
||||||
throw new Error(`${PACKAGE_NAME} should be used with output: 'static'`);
|
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>
|
<html>
|
||||||
|
|
|
@ -6,6 +6,7 @@ describe('Serverless prerender', () => {
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
process.env.PRERENDER = true;
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/serverless-prerender/',
|
root: './fixtures/serverless-prerender/',
|
||||||
});
|
});
|
||||||
|
@ -16,3 +17,24 @@ describe('Serverless prerender', () => {
|
||||||
expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok;
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
@ -3586,6 +3586,9 @@ importers:
|
||||||
mocha:
|
mocha:
|
||||||
specifier: ^9.2.2
|
specifier: ^9.2.2
|
||||||
version: 9.2.2
|
version: 9.2.2
|
||||||
|
slash:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
wrangler:
|
wrangler:
|
||||||
specifier: ^2.0.23
|
specifier: ^2.0.23
|
||||||
version: 2.0.23
|
version: 2.0.23
|
||||||
|
|
Loading…
Add table
Reference in a new issue