Default preview host to localhost (#5753)
* Initial refactor * Extract as vite plugin * Cleanup vite plugin * Reduce option passing * Use localhost as preview default host * Simplify base handling * Fix host handling * Add changeset * Remove unused imports * Remove unused sirv dep * Try pin playwright to 1.28.1 * Update playwright * Try this * Speed up CI * Try fix page off * Refactor networkidle * Ensure open connections are destroyed when the preview server is closed * Revert debug code Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
This commit is contained in:
parent
f35411487b
commit
302e0ef8f5
15 changed files with 183 additions and 270 deletions
5
.changeset/lemon-bobcats-kick.md
Normal file
5
.changeset/lemon-bobcats-kick.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Default preview host to `localhost` instead of `127.0.0.1`. This allows the static server and integration preview servers to serve under ipv6.
|
|
@ -155,8 +155,8 @@
|
||||||
"rehype": "^12.0.1",
|
"rehype": "^12.0.1",
|
||||||
"resolve": "^1.22.0",
|
"resolve": "^1.22.0",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
|
"server-destroy": "^1.0.1",
|
||||||
"shiki": "^0.11.1",
|
"shiki": "^0.11.1",
|
||||||
"sirv": "^2.0.2",
|
|
||||||
"slash": "^4.0.0",
|
"slash": "^4.0.0",
|
||||||
"string-width": "^5.1.2",
|
"string-width": "^5.1.2",
|
||||||
"strip-ansi": "^7.0.1",
|
"strip-ansi": "^7.0.1",
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
"zod": "^3.17.3"
|
"zod": "^3.17.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.22.2",
|
"@playwright/test": "^1.29.2",
|
||||||
"@types/babel__generator": "^7.6.4",
|
"@types/babel__generator": "^7.6.4",
|
||||||
"@types/babel__traverse": "^7.17.1",
|
"@types/babel__traverse": "^7.17.1",
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
|
@ -191,6 +191,7 @@
|
||||||
"@types/resolve": "^1.20.2",
|
"@types/resolve": "^1.20.2",
|
||||||
"@types/rimraf": "^3.0.2",
|
"@types/rimraf": "^3.0.2",
|
||||||
"@types/send": "^0.17.1",
|
"@types/send": "^0.17.1",
|
||||||
|
"@types/server-destroy": "^1.0.1",
|
||||||
"@types/unist": "^2.0.6",
|
"@types/unist": "^2.0.6",
|
||||||
"astro-scripts": "workspace:*",
|
"astro-scripts": "workspace:*",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
|
|
|
@ -49,9 +49,6 @@ export default async function dev(
|
||||||
// Start listening to the port
|
// Start listening to the port
|
||||||
const devServerAddressInfo = await startContainer(restart.container);
|
const devServerAddressInfo = await startContainer(restart.container);
|
||||||
|
|
||||||
const site = settings.config.site
|
|
||||||
? new URL(settings.config.base, settings.config.site)
|
|
||||||
: undefined;
|
|
||||||
info(
|
info(
|
||||||
options.logging,
|
options.logging,
|
||||||
null,
|
null,
|
||||||
|
@ -59,7 +56,7 @@ export default async function dev(
|
||||||
startupTime: performance.now() - devStart,
|
startupTime: performance.now() - devStart,
|
||||||
resolvedUrls: restart.container.viteServer.resolvedUrls || { local: [], network: [] },
|
resolvedUrls: restart.container.viteServer.resolvedUrls || { local: [], network: [] },
|
||||||
host: settings.config.server.host,
|
host: settings.config.server.host,
|
||||||
site,
|
base: settings.config.base,
|
||||||
isRestart: options.isRestart,
|
isRestart: options.isRestart,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,14 +14,11 @@ import {
|
||||||
underline,
|
underline,
|
||||||
yellow,
|
yellow,
|
||||||
} from 'kleur/colors';
|
} from 'kleur/colors';
|
||||||
import type { AddressInfo } from 'net';
|
|
||||||
import os from 'os';
|
|
||||||
import { ResolvedServerUrls } from 'vite';
|
import { ResolvedServerUrls } from 'vite';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { renderErrorMarkdown } from './errors/dev/utils.js';
|
import { renderErrorMarkdown } from './errors/dev/utils.js';
|
||||||
import { AstroError, CompilerError, ErrorWithMetadata } from './errors/index.js';
|
import { AstroError, CompilerError, ErrorWithMetadata } from './errors/index.js';
|
||||||
import { removeTrailingForwardSlash } from './path.js';
|
import { emoji, padMultilineString } from './util.js';
|
||||||
import { emoji, getLocalAddress, padMultilineString } from './util.js';
|
|
||||||
|
|
||||||
const PREFIX_PADDING = 6;
|
const PREFIX_PADDING = 6;
|
||||||
|
|
||||||
|
@ -58,31 +55,26 @@ export function serverStart({
|
||||||
startupTime,
|
startupTime,
|
||||||
resolvedUrls,
|
resolvedUrls,
|
||||||
host,
|
host,
|
||||||
site,
|
base,
|
||||||
isRestart = false,
|
isRestart = false,
|
||||||
}: {
|
}: {
|
||||||
startupTime: number;
|
startupTime: number;
|
||||||
resolvedUrls: ResolvedServerUrls;
|
resolvedUrls: ResolvedServerUrls;
|
||||||
host: string | boolean;
|
host: string | boolean;
|
||||||
site: URL | undefined;
|
base: string;
|
||||||
isRestart?: boolean;
|
isRestart?: boolean;
|
||||||
}): string {
|
}): string {
|
||||||
// PACKAGE_VERSION is injected at build-time
|
// PACKAGE_VERSION is injected at build-time
|
||||||
const version = process.env.PACKAGE_VERSION ?? '0.0.0';
|
const version = process.env.PACKAGE_VERSION ?? '0.0.0';
|
||||||
const rootPath = site ? site.pathname : '/';
|
|
||||||
const localPrefix = `${dim('┃')} Local `;
|
const localPrefix = `${dim('┃')} Local `;
|
||||||
const networkPrefix = `${dim('┃')} Network `;
|
const networkPrefix = `${dim('┃')} Network `;
|
||||||
const emptyPrefix = ' '.repeat(11);
|
const emptyPrefix = ' '.repeat(11);
|
||||||
|
|
||||||
const localUrlMessages = resolvedUrls.local.map((url, i) => {
|
const localUrlMessages = resolvedUrls.local.map((url, i) => {
|
||||||
return `${i === 0 ? localPrefix : emptyPrefix}${bold(
|
return `${i === 0 ? localPrefix : emptyPrefix}${bold(cyan(new URL(url).origin + base))}`;
|
||||||
cyan(removeTrailingForwardSlash(url) + rootPath)
|
|
||||||
)}`;
|
|
||||||
});
|
});
|
||||||
const networkUrlMessages = resolvedUrls.network.map((url, i) => {
|
const networkUrlMessages = resolvedUrls.network.map((url, i) => {
|
||||||
return `${i === 0 ? networkPrefix : emptyPrefix}${bold(
|
return `${i === 0 ? networkPrefix : emptyPrefix}${bold(cyan(new URL(url).origin + base))}`;
|
||||||
cyan(removeTrailingForwardSlash(url) + rootPath)
|
|
||||||
)}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (networkUrlMessages.length === 0) {
|
if (networkUrlMessages.length === 0) {
|
||||||
|
@ -109,50 +101,6 @@ export function serverStart({
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveServerUrls({
|
|
||||||
address,
|
|
||||||
host,
|
|
||||||
https,
|
|
||||||
}: {
|
|
||||||
address: AddressInfo;
|
|
||||||
host: string | boolean;
|
|
||||||
https: boolean;
|
|
||||||
}): ResolvedServerUrls {
|
|
||||||
const { address: networkAddress, port } = address;
|
|
||||||
const localAddress = getLocalAddress(networkAddress, host);
|
|
||||||
const networkLogging = getNetworkLogging(host);
|
|
||||||
const toDisplayUrl = (hostname: string) => `${https ? 'https' : 'http'}://${hostname}:${port}`;
|
|
||||||
|
|
||||||
let local = toDisplayUrl(localAddress);
|
|
||||||
let network: string | null = null;
|
|
||||||
|
|
||||||
if (networkLogging === 'visible') {
|
|
||||||
const ipv4Networks = Object.values(os.networkInterfaces())
|
|
||||||
.flatMap((networkInterface) => networkInterface ?? [])
|
|
||||||
.filter(
|
|
||||||
(networkInterface) =>
|
|
||||||
networkInterface?.address &&
|
|
||||||
// Node < v18
|
|
||||||
((typeof networkInterface.family === 'string' && networkInterface.family === 'IPv4') ||
|
|
||||||
// Node >= v18
|
|
||||||
(typeof networkInterface.family === 'number' && (networkInterface as any).family === 4))
|
|
||||||
);
|
|
||||||
for (let { address: ipv4Address } of ipv4Networks) {
|
|
||||||
if (ipv4Address.includes('127.0.0.1')) {
|
|
||||||
const displayAddress = ipv4Address.replace('127.0.0.1', localAddress);
|
|
||||||
local = toDisplayUrl(displayAddress);
|
|
||||||
} else {
|
|
||||||
network = toDisplayUrl(ipv4Address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
local: [local],
|
|
||||||
network: network ? [network] : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function telemetryNotice() {
|
export function telemetryNotice() {
|
||||||
const headline = yellow(`Astro now collects ${bold('anonymous')} usage data.`);
|
const headline = yellow(`Astro now collects ${bold('anonymous')} usage data.`);
|
||||||
const why = `This ${bold('optional program')} will help shape our roadmap.`;
|
const why = `This ${bold('optional program')} will help shape our roadmap.`;
|
||||||
|
@ -228,11 +176,6 @@ export function cancelled(message: string, tip?: string) {
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Display port in use */
|
|
||||||
export function portInUse({ port }: { port: number }): string {
|
|
||||||
return `Port ${port} in use. Trying a new one…`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOCAL_IP_HOSTS = new Set(['localhost', '127.0.0.1']);
|
const LOCAL_IP_HOSTS = new Set(['localhost', '127.0.0.1']);
|
||||||
|
|
||||||
export function getNetworkLogging(host: string | boolean): 'none' | 'host-to-expose' | 'visible' {
|
export function getNetworkLogging(host: string | boolean): 'none' | 'host-to-expose' | 'visible' {
|
||||||
|
|
|
@ -23,11 +23,9 @@ export default async function preview(
|
||||||
logging: logging,
|
logging: logging,
|
||||||
});
|
});
|
||||||
await runHookConfigDone({ settings: settings, logging: logging });
|
await runHookConfigDone({ settings: settings, logging: logging });
|
||||||
const host = getResolvedHostForHttpServer(settings.config.server.host);
|
|
||||||
const { port, headers } = settings.config.server;
|
|
||||||
|
|
||||||
if (settings.config.output === 'static') {
|
if (settings.config.output === 'static') {
|
||||||
const server = await createStaticPreviewServer(settings, { logging, host, port, headers });
|
const server = await createStaticPreviewServer(settings, logging);
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
if (!settings.adapter) {
|
if (!settings.adapter) {
|
||||||
|
@ -55,8 +53,8 @@ export default async function preview(
|
||||||
outDir: settings.config.outDir,
|
outDir: settings.config.outDir,
|
||||||
client: settings.config.build.client,
|
client: settings.config.build.client,
|
||||||
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
|
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
|
||||||
host,
|
host: getResolvedHostForHttpServer(settings.config.server.host),
|
||||||
port,
|
port: settings.config.server.port,
|
||||||
base: settings.config.base,
|
base: settings.config.base,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import type { AddressInfo } from 'net';
|
import http from 'http';
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { preview, type PreviewServer as VitePreviewServer } from 'vite';
|
||||||
import type { AstroSettings } from '../../@types/astro';
|
import type { AstroSettings } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger/core';
|
import type { LogOptions } from '../logger/core';
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import http, { OutgoingHttpHeaders } from 'http';
|
|
||||||
import { performance } from 'perf_hooks';
|
|
||||||
import sirv from 'sirv';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js';
|
|
||||||
import { error, info } from '../logger/core.js';
|
import { error, info } from '../logger/core.js';
|
||||||
import * as msg from '../messages.js';
|
import * as msg from '../messages.js';
|
||||||
|
import { getResolvedHostForHttpServer } from './util.js';
|
||||||
|
import { vitePluginAstroPreview } from './vite-plugin-astro-preview.js';
|
||||||
|
import enableDestroy from 'server-destroy';
|
||||||
|
|
||||||
export interface PreviewServer {
|
export interface PreviewServer {
|
||||||
host?: string;
|
host?: string;
|
||||||
|
@ -19,160 +18,65 @@ export interface PreviewServer {
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
|
|
||||||
|
|
||||||
/** The primary dev action */
|
|
||||||
export default async function createStaticPreviewServer(
|
export default async function createStaticPreviewServer(
|
||||||
settings: AstroSettings,
|
settings: AstroSettings,
|
||||||
{
|
logging: LogOptions
|
||||||
logging,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
headers,
|
|
||||||
}: {
|
|
||||||
logging: LogOptions;
|
|
||||||
host: string | undefined;
|
|
||||||
port: number;
|
|
||||||
headers: OutgoingHttpHeaders | undefined;
|
|
||||||
}
|
|
||||||
): Promise<PreviewServer> {
|
): Promise<PreviewServer> {
|
||||||
const startServerTime = performance.now();
|
const startServerTime = performance.now();
|
||||||
const defaultOrigin = 'http://localhost';
|
|
||||||
const trailingSlash = settings.config.trailingSlash;
|
let previewServer: VitePreviewServer;
|
||||||
/** Base request URL. */
|
try {
|
||||||
let baseURL = new URL(settings.config.base, new URL(settings.config.site || '/', defaultOrigin));
|
previewServer = await preview({
|
||||||
const staticFileServer = sirv(fileURLToPath(settings.config.outDir), {
|
configFile: false,
|
||||||
dev: true,
|
base: settings.config.base,
|
||||||
etag: true,
|
appType: 'mpa',
|
||||||
maxAge: 0,
|
build: {
|
||||||
setHeaders: (res, pathname, stats) => {
|
outDir: fileURLToPath(settings.config.outDir),
|
||||||
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
||||||
if (value) res.setHeader(name, value);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
preview: {
|
||||||
// Create the preview server, send static files out of the `dist/` directory.
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
const requestURL = new URL(req.url as string, defaultOrigin);
|
|
||||||
|
|
||||||
// respond 404 to requests outside the base request directory
|
|
||||||
if (!requestURL.pathname.startsWith(baseURL.pathname)) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end(subpathNotUsedTemplate(baseURL.pathname, requestURL.pathname));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Relative request path. */
|
|
||||||
const pathname = requestURL.pathname.slice(baseURL.pathname.length - 1);
|
|
||||||
|
|
||||||
const isRoot = pathname === '/';
|
|
||||||
const hasTrailingSlash = isRoot || pathname.endsWith('/');
|
|
||||||
|
|
||||||
function sendError(message: string) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end(notFoundTemplate(pathname, message));
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case hasTrailingSlash && trailingSlash == 'never' && !isRoot:
|
|
||||||
sendError('Not Found (trailingSlash is set to "never")');
|
|
||||||
return;
|
|
||||||
case !hasTrailingSlash &&
|
|
||||||
trailingSlash == 'always' &&
|
|
||||||
!isRoot &&
|
|
||||||
!HAS_FILE_EXTENSION_REGEXP.test(pathname):
|
|
||||||
sendError('Not Found (trailingSlash is set to "always")');
|
|
||||||
return;
|
|
||||||
default: {
|
|
||||||
// HACK: rewrite req.url so that sirv finds the file
|
|
||||||
req.url = '/' + req.url?.replace(baseURL.pathname, '');
|
|
||||||
staticFileServer(req, res, () => {
|
|
||||||
const errorPagePath = fileURLToPath(settings.config.outDir + '/404.html');
|
|
||||||
if (fs.existsSync(errorPagePath)) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.setHeader('Content-Type', 'text/html;charset=utf-8');
|
|
||||||
res.end(fs.readFileSync(errorPagePath));
|
|
||||||
} else {
|
|
||||||
staticFileServer(req, res, () => {
|
|
||||||
sendError('Not Found');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let httpServer: http.Server;
|
|
||||||
|
|
||||||
/** Expose dev server to `port` */
|
|
||||||
function startServer(timerStart: number): Promise<void> {
|
|
||||||
let showedPortTakenMsg = false;
|
|
||||||
let showedListenMsg = false;
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const listen = () => {
|
|
||||||
httpServer = server.listen(port, host, async () => {
|
|
||||||
if (!showedListenMsg) {
|
|
||||||
const resolvedUrls = msg.resolveServerUrls({
|
|
||||||
address: server.address() as AddressInfo,
|
|
||||||
host: settings.config.server.host,
|
host: settings.config.server.host,
|
||||||
https: false,
|
port: settings.config.server.port,
|
||||||
|
headers: settings.config.server.headers,
|
||||||
|
},
|
||||||
|
plugins: [vitePluginAstroPreview(settings)],
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
error(logging, 'astro', err.stack || err.message);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
enableDestroy(previewServer.httpServer);
|
||||||
|
|
||||||
|
// Log server start URLs
|
||||||
info(
|
info(
|
||||||
logging,
|
logging,
|
||||||
null,
|
null,
|
||||||
msg.serverStart({
|
msg.serverStart({
|
||||||
startupTime: performance.now() - timerStart,
|
startupTime: performance.now() - startServerTime,
|
||||||
resolvedUrls,
|
resolvedUrls: previewServer.resolvedUrls,
|
||||||
host: settings.config.server.host,
|
host: settings.config.server.host,
|
||||||
site: baseURL,
|
base: settings.config.base,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
showedListenMsg = true;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
httpServer?.on('error', onError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = (err: NodeJS.ErrnoException) => {
|
|
||||||
if (err.code && err.code === 'EADDRINUSE') {
|
|
||||||
if (!showedPortTakenMsg) {
|
|
||||||
info(logging, 'astro', msg.portInUse({ port }));
|
|
||||||
showedPortTakenMsg = true; // only print this once
|
|
||||||
}
|
|
||||||
port++;
|
|
||||||
return listen(); // retry
|
|
||||||
} else {
|
|
||||||
error(logging, 'astro', err.stack || err.message);
|
|
||||||
httpServer?.removeListener('error', onError);
|
|
||||||
reject(err); // reject
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
listen();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start listening on `hostname:port`.
|
|
||||||
await startServer(startServerTime);
|
|
||||||
|
|
||||||
// Resolves once the server is closed
|
// Resolves once the server is closed
|
||||||
function closed() {
|
function closed() {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
httpServer!.addListener('close', resolve);
|
previewServer.httpServer.addListener('close', resolve);
|
||||||
httpServer!.addListener('error', reject);
|
previewServer.httpServer.addListener('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
host,
|
host: getResolvedHostForHttpServer(settings.config.server.host),
|
||||||
port,
|
port: settings.config.server.port,
|
||||||
closed,
|
closed,
|
||||||
server: httpServer!,
|
server: previewServer.httpServer,
|
||||||
stop: async () => {
|
stop: async () => {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
httpServer.close((err) => (err ? reject(err) : resolve(undefined)));
|
previewServer.httpServer.destroy((err) => (err ? reject(err) : resolve(undefined)));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export function getResolvedHostForHttpServer(host: string | boolean) {
|
export function getResolvedHostForHttpServer(host: string | boolean) {
|
||||||
if (host === false) {
|
if (host === false) {
|
||||||
// Use a secure default
|
// Use a secure default
|
||||||
return '127.0.0.1';
|
return 'localhost';
|
||||||
} else if (host === true) {
|
} else if (host === true) {
|
||||||
// If passed --host in the CLI without arguments
|
// If passed --host in the CLI without arguments
|
||||||
return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs)
|
return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs)
|
||||||
|
@ -9,3 +9,11 @@ export function getResolvedHostForHttpServer(host: string | boolean) {
|
||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stripBase(path: string, base: string): string {
|
||||||
|
if (path === base) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
const baseWithSlash = base.endsWith('/') ? base : base + '/';
|
||||||
|
return path.replace(RegExp('^' + baseWithSlash), '/');
|
||||||
|
}
|
||||||
|
|
68
packages/astro/src/core/preview/vite-plugin-astro-preview.ts
Normal file
68
packages/astro/src/core/preview/vite-plugin-astro-preview.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { Plugin } from 'vite';
|
||||||
|
import { AstroSettings } from '../../@types/astro.js';
|
||||||
|
import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js';
|
||||||
|
import { stripBase } from './util.js';
|
||||||
|
|
||||||
|
const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
|
||||||
|
|
||||||
|
export function vitePluginAstroPreview(settings: AstroSettings): Plugin {
|
||||||
|
const { base, outDir, trailingSlash } = settings.config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'astro:preview',
|
||||||
|
apply: 'serve',
|
||||||
|
configurePreviewServer(server) {
|
||||||
|
server.middlewares.use((req, res, next) => {
|
||||||
|
// respond 404 to requests outside the base request directory
|
||||||
|
if (!req.url!.startsWith(base)) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(subpathNotUsedTemplate(base, req.url!));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathname = stripBase(req.url!, base);
|
||||||
|
const isRoot = pathname === '/';
|
||||||
|
|
||||||
|
// Validate trailingSlash
|
||||||
|
if (!isRoot) {
|
||||||
|
const hasTrailingSlash = pathname.endsWith('/');
|
||||||
|
|
||||||
|
if (hasTrailingSlash && trailingSlash == 'never') {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(notFoundTemplate(pathname, 'Not Found (trailingSlash is set to "never")'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasTrailingSlash &&
|
||||||
|
trailingSlash == 'always' &&
|
||||||
|
!HAS_FILE_EXTENSION_REGEXP.test(pathname)
|
||||||
|
) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(notFoundTemplate(pathname, 'Not Found (trailingSlash is set to "always")'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
server.middlewares.use((req, res) => {
|
||||||
|
const errorPagePath = fileURLToPath(outDir + '/404.html');
|
||||||
|
if (fs.existsSync(errorPagePath)) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.setHeader('Content-Type', 'text/html;charset=utf-8');
|
||||||
|
res.end(fs.readFileSync(errorPagePath));
|
||||||
|
} else {
|
||||||
|
const pathname = stripBase(req.url!, base);
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(notFoundTemplate(pathname, 'Not Found'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -164,14 +164,6 @@ export function emoji(char: string, fallback: string) {
|
||||||
return process.platform !== 'win32' ? char : fallback;
|
return process.platform !== 'win32' ? char : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocalAddress(serverAddress: string, host: string | boolean): string {
|
|
||||||
if (typeof host === 'boolean' || host === 'localhost') {
|
|
||||||
return 'localhost';
|
|
||||||
} else {
|
|
||||||
return serverAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulate Vite's resolve and import analysis so we can import the id as an URL
|
* Simulate Vite's resolve and import analysis so we can import the id as an URL
|
||||||
* through a script tag or a dynamic import as-is.
|
* through a script tag or a dynamic import as-is.
|
||||||
|
|
|
@ -77,17 +77,11 @@ describe('astro cli', () => {
|
||||||
const localURL = new URL(local);
|
const localURL = new URL(local);
|
||||||
const networkURL = new URL(network);
|
const networkURL = new URL(network);
|
||||||
|
|
||||||
if (cmd === 'dev') {
|
|
||||||
expect(localURL.hostname).to.be.oneOf(
|
expect(localURL.hostname).to.be.oneOf(
|
||||||
['localhost', '127.0.0.1'],
|
['localhost', '127.0.0.1'],
|
||||||
`Expected local URL to be on localhost`
|
`Expected local URL to be on localhost`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
expect(localURL.hostname).to.be.equal(
|
|
||||||
flagValue ?? 'localhost',
|
|
||||||
`Expected local URL to be on localhost`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Note: our tests run in parallel so this could be 3000+!
|
// Note: our tests run in parallel so this could be 3000+!
|
||||||
expect(Number.parseInt(localURL.port)).to.be.greaterThanOrEqual(
|
expect(Number.parseInt(localURL.port)).to.be.greaterThanOrEqual(
|
||||||
3000,
|
3000,
|
||||||
|
@ -113,17 +107,11 @@ describe('astro cli', () => {
|
||||||
expect(network).to.not.be.undefined;
|
expect(network).to.not.be.undefined;
|
||||||
const localURL = new URL(local);
|
const localURL = new URL(local);
|
||||||
|
|
||||||
if (cmd === 'dev') {
|
|
||||||
expect(localURL.hostname).to.be.oneOf(
|
expect(localURL.hostname).to.be.oneOf(
|
||||||
['localhost', '127.0.0.1'],
|
['localhost', '127.0.0.1'],
|
||||||
`Expected local URL to be on localhost`
|
`Expected local URL to be on localhost`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
expect(localURL.hostname).to.be.equal(
|
|
||||||
'localhost',
|
|
||||||
`Expected local URL to be on localhost`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
expect(() => new URL(networkURL)).to.throw();
|
expect(() => new URL(networkURL)).to.throw();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -140,14 +128,10 @@ describe('astro cli', () => {
|
||||||
expect(network).to.be.undefined;
|
expect(network).to.be.undefined;
|
||||||
|
|
||||||
const localURL = new URL(local);
|
const localURL = new URL(local);
|
||||||
if (cmd === 'dev') {
|
|
||||||
expect(localURL.hostname).to.be.oneOf(
|
expect(localURL.hostname).to.be.oneOf(
|
||||||
['localhost', '127.0.0.1'],
|
['localhost', '127.0.0.1'],
|
||||||
`Expected local URL to be on localhost`
|
`Expected local URL to be on localhost`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
expect(localURL.hostname).to.be.equal(flagValue, `Expected local URL to be on localhost`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,13 +31,13 @@
|
||||||
"test:match": "playwright test -g"
|
"test:match": "playwright test -g"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.26.0",
|
"@playwright/test": "^1.29.2",
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
"astro-scripts": "workspace:*",
|
"astro-scripts": "workspace:*",
|
||||||
"playwright": "^1.22.2"
|
"playwright": "^1.29.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"throttles": "^1.0.1"
|
"throttles": "^1.0.1"
|
||||||
|
|
|
@ -19,7 +19,7 @@ test.describe('Basic prefetch', () => {
|
||||||
test('skips /admin', async ({ page, astro }) => {
|
test('skips /admin', async ({ page, astro }) => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
|
|
||||||
page.on('request', async (request) => requests.push(request.url()));
|
page.on('request', (request) => requests.push(request.url()));
|
||||||
|
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ test.describe('Basic prefetch', () => {
|
||||||
test('skips /admin', async ({ page, astro }) => {
|
test('skips /admin', async ({ page, astro }) => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
|
|
||||||
page.on('request', async (request) => requests.push(request.url()));
|
page.on('request', (request) => requests.push(request.url()));
|
||||||
|
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ test.describe('Custom prefetch selectors', () => {
|
||||||
test('only prefetches /contact', async ({ page, astro }) => {
|
test('only prefetches /contact', async ({ page, astro }) => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
|
|
||||||
page.on('request', async (request) => requests.push(request.url()));
|
page.on('request', (request) => requests.push(request.url()));
|
||||||
|
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ test.describe('Custom prefetch selectors', () => {
|
||||||
test('only prefetches /contact', async ({ page, astro }) => {
|
test('only prefetches /contact', async ({ page, astro }) => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
|
|
||||||
page.on('request', async (request) => requests.push(request.url()));
|
page.on('request', (request) => requests.push(request.url()));
|
||||||
|
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ test.describe('Style prefetch', () => {
|
||||||
test('style fetching', async ({ page, astro }) => {
|
test('style fetching', async ({ page, astro }) => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
|
|
||||||
page.on('request', async (request) => requests.push(request.url()));
|
page.on('request', (request) => requests.push(request.url()));
|
||||||
|
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
|
|
@ -396,7 +396,7 @@ importers:
|
||||||
'@babel/plugin-transform-react-jsx': ^7.17.12
|
'@babel/plugin-transform-react-jsx': ^7.17.12
|
||||||
'@babel/traverse': ^7.18.2
|
'@babel/traverse': ^7.18.2
|
||||||
'@babel/types': ^7.18.4
|
'@babel/types': ^7.18.4
|
||||||
'@playwright/test': ^1.22.2
|
'@playwright/test': ^1.29.2
|
||||||
'@types/babel__core': ^7.1.19
|
'@types/babel__core': ^7.1.19
|
||||||
'@types/babel__generator': ^7.6.4
|
'@types/babel__generator': ^7.6.4
|
||||||
'@types/babel__traverse': ^7.17.1
|
'@types/babel__traverse': ^7.17.1
|
||||||
|
@ -418,6 +418,7 @@ importers:
|
||||||
'@types/resolve': ^1.20.2
|
'@types/resolve': ^1.20.2
|
||||||
'@types/rimraf': ^3.0.2
|
'@types/rimraf': ^3.0.2
|
||||||
'@types/send': ^0.17.1
|
'@types/send': ^0.17.1
|
||||||
|
'@types/server-destroy': ^1.0.1
|
||||||
'@types/unist': ^2.0.6
|
'@types/unist': ^2.0.6
|
||||||
'@types/yargs-parser': ^21.0.0
|
'@types/yargs-parser': ^21.0.0
|
||||||
acorn: ^8.8.1
|
acorn: ^8.8.1
|
||||||
|
@ -466,8 +467,8 @@ importers:
|
||||||
rollup: ^3.9.0
|
rollup: ^3.9.0
|
||||||
sass: ^1.52.2
|
sass: ^1.52.2
|
||||||
semver: ^7.3.7
|
semver: ^7.3.7
|
||||||
|
server-destroy: ^1.0.1
|
||||||
shiki: ^0.11.1
|
shiki: ^0.11.1
|
||||||
sirv: ^2.0.2
|
|
||||||
slash: ^4.0.0
|
slash: ^4.0.0
|
||||||
srcset-parse: ^1.1.0
|
srcset-parse: ^1.1.0
|
||||||
string-width: ^5.1.2
|
string-width: ^5.1.2
|
||||||
|
@ -529,8 +530,8 @@ importers:
|
||||||
rehype: 12.0.1
|
rehype: 12.0.1
|
||||||
resolve: 1.22.1
|
resolve: 1.22.1
|
||||||
semver: 7.3.8
|
semver: 7.3.8
|
||||||
|
server-destroy: 1.0.1
|
||||||
shiki: 0.11.1
|
shiki: 0.11.1
|
||||||
sirv: 2.0.2
|
|
||||||
slash: 4.0.0
|
slash: 4.0.0
|
||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
strip-ansi: 7.0.1
|
strip-ansi: 7.0.1
|
||||||
|
@ -564,6 +565,7 @@ importers:
|
||||||
'@types/resolve': 1.20.2
|
'@types/resolve': 1.20.2
|
||||||
'@types/rimraf': 3.0.2
|
'@types/rimraf': 3.0.2
|
||||||
'@types/send': 0.17.1
|
'@types/send': 0.17.1
|
||||||
|
'@types/server-destroy': 1.0.1
|
||||||
'@types/unist': 2.0.6
|
'@types/unist': 2.0.6
|
||||||
astro-scripts: link:../../scripts
|
astro-scripts: link:../../scripts
|
||||||
chai: 4.3.7
|
chai: 4.3.7
|
||||||
|
@ -3139,13 +3141,13 @@ importers:
|
||||||
|
|
||||||
packages/integrations/prefetch:
|
packages/integrations/prefetch:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@playwright/test': ^1.26.0
|
'@playwright/test': ^1.29.2
|
||||||
'@types/chai': ^4.3.1
|
'@types/chai': ^4.3.1
|
||||||
'@types/chai-as-promised': ^7.1.5
|
'@types/chai-as-promised': ^7.1.5
|
||||||
'@types/mocha': ^9.1.1
|
'@types/mocha': ^9.1.1
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
astro-scripts: workspace:*
|
astro-scripts: workspace:*
|
||||||
playwright: ^1.22.2
|
playwright: ^1.29.2
|
||||||
throttles: ^1.0.1
|
throttles: ^1.0.1
|
||||||
dependencies:
|
dependencies:
|
||||||
throttles: 1.0.1
|
throttles: 1.0.1
|
||||||
|
@ -7055,6 +7057,7 @@ packages:
|
||||||
|
|
||||||
/@types/node/14.18.36:
|
/@types/node/14.18.36:
|
||||||
resolution: {integrity: sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==}
|
resolution: {integrity: sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node/16.18.11:
|
/@types/node/16.18.11:
|
||||||
resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==}
|
resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==}
|
||||||
|
@ -7126,7 +7129,7 @@ packages:
|
||||||
/@types/resolve/1.17.1:
|
/@types/resolve/1.17.1:
|
||||||
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
|
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 14.18.36
|
'@types/node': 18.11.18
|
||||||
|
|
||||||
/@types/resolve/1.20.2:
|
/@types/resolve/1.20.2:
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
@ -7141,7 +7144,7 @@ packages:
|
||||||
/@types/sax/1.2.4:
|
/@types/sax/1.2.4:
|
||||||
resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==}
|
resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 17.0.45
|
'@types/node': 18.11.18
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/scheduler/0.16.2:
|
/@types/scheduler/0.16.2:
|
||||||
|
@ -7162,6 +7165,12 @@ packages:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/server-destroy/1.0.1:
|
||||||
|
resolution: {integrity: sha512-77QGr7waZbE0Y0uF+G+uH3H3SmhyA78Jf2r5r7QSrpg0U3kSXduWpGjzP9PvPLR/KCy+kHjjpnugRHsYTnHopg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.11.18
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/set-cookie-parser/2.4.2:
|
/@types/set-cookie-parser/2.4.2:
|
||||||
resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==}
|
resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -11129,7 +11138,7 @@ packages:
|
||||||
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
|
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 14.18.36
|
'@types/node': 18.11.18
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
@ -13994,6 +14003,10 @@ packages:
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/server-destroy/1.0.1:
|
||||||
|
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/set-blocking/2.0.0:
|
/set-blocking/2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue