Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default preview host to localhost #5753

Merged
merged 25 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-bobcats-kick.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 1 addition & 4 deletions packages/astro/src/core/dev/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,14 @@ export default async function dev(
// Start listening to the port
const devServerAddressInfo = await startContainer(restart.container);

const site = settings.config.site
? new URL(settings.config.base, settings.config.site)
: undefined;
info(
options.logging,
null,
msg.serverStart({
startupTime: performance.now() - devStart,
resolvedUrls: restart.container.viteServer.resolvedUrls || { local: [], network: [] },
host: settings.config.server.host,
site,
base: settings.config.base,
isRestart: options.isRestart,
})
);
Expand Down
67 changes: 5 additions & 62 deletions packages/astro/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@ import {
underline,
yellow,
} from 'kleur/colors';
import type { AddressInfo } from 'net';
import os from 'os';
import { ResolvedServerUrls } from 'vite';
import { ZodError } from 'zod';
import { renderErrorMarkdown } from './errors/dev/utils.js';
import { AstroError, CompilerError, ErrorWithMetadata } from './errors/index.js';
import { removeTrailingForwardSlash } from './path.js';
import { emoji, getLocalAddress, padMultilineString } from './util.js';
import { emoji, padMultilineString } from './util.js';

const PREFIX_PADDING = 6;

Expand Down Expand Up @@ -58,31 +55,26 @@ export function serverStart({
startupTime,
resolvedUrls,
host,
site,
base,
isRestart = false,
}: {
startupTime: number;
resolvedUrls: ResolvedServerUrls;
host: string | boolean;
site: URL | undefined;
base: string;
isRestart?: boolean;
}): string {
// PACKAGE_VERSION is injected at build-time
const version = process.env.PACKAGE_VERSION ?? '0.0.0';
const rootPath = site ? site.pathname : '/';
const localPrefix = `${dim('┃')} Local `;
const networkPrefix = `${dim('┃')} Network `;
const emptyPrefix = ' '.repeat(11);

const localUrlMessages = resolvedUrls.local.map((url, i) => {
return `${i === 0 ? localPrefix : emptyPrefix}${bold(
cyan(removeTrailingForwardSlash(url) + rootPath)
)}`;
return `${i === 0 ? localPrefix : emptyPrefix}${bold(cyan(new URL(url).origin + base))}`;
});
const networkUrlMessages = resolvedUrls.network.map((url, i) => {
return `${i === 0 ? networkPrefix : emptyPrefix}${bold(
cyan(removeTrailingForwardSlash(url) + rootPath)
)}`;
return `${i === 0 ? networkPrefix : emptyPrefix}${bold(cyan(new URL(url).origin + base))}`;
});

if (networkUrlMessages.length === 0) {
Expand All @@ -109,50 +101,6 @@ export function serverStart({
.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() {
const headline = yellow(`Astro now collects ${bold('anonymous')} usage data.`);
const why = `This ${bold('optional program')} will help shape our roadmap.`;
Expand Down Expand Up @@ -228,11 +176,6 @@ export function cancelled(message: string, tip?: string) {
.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']);

export function getNetworkLogging(host: string | boolean): 'none' | 'host-to-expose' | 'visible' {
Expand Down
8 changes: 3 additions & 5 deletions packages/astro/src/core/preview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ export default async function preview(
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') {
const server = await createStaticPreviewServer(settings, { logging, host, port, headers });
const server = await createStaticPreviewServer(settings, logging);
return server;
}
if (!settings.adapter) {
Expand Down Expand Up @@ -55,8 +53,8 @@ export default async function preview(
outDir: settings.config.outDir,
client: settings.config.build.client,
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
host,
port,
host: getResolvedHostForHttpServer(settings.config.server.host),
port: settings.config.server.port,
base: settings.config.base,
});

Expand Down
187 changes: 44 additions & 143 deletions packages/astro/src/core/preview/static-preview-server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { AddressInfo } from 'net';
import type { AstroSettings } from '../../@types/astro';
import type { LogOptions } from '../logger/core';

import fs from 'fs';
import http, { OutgoingHttpHeaders } from 'http';
import http from 'http';
import { performance } from 'perf_hooks';
import sirv from 'sirv';
import { fileURLToPath } from 'url';
import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js';
import { preview, type PreviewServer as VitePreviewServer } from 'vite';
import type { AstroSettings } from '../../@types/astro';
import type { LogOptions } from '../logger/core';
import { error, info } from '../logger/core.js';
import * as msg from '../messages.js';
import { getResolvedHostForHttpServer } from './util.js';
import { vitePluginAstroPreview } from './vite-plugin-astro-preview.js';

export interface PreviewServer {
host?: string;
Expand All @@ -19,160 +17,63 @@ export interface PreviewServer {
stop(): Promise<void>;
}

const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;

/** The primary dev action */
export default async function createStaticPreviewServer(
settings: AstroSettings,
{
logging,
host,
port,
headers,
}: {
logging: LogOptions;
host: string | undefined;
port: number;
headers: OutgoingHttpHeaders | undefined;
}
logging: LogOptions
): Promise<PreviewServer> {
const startServerTime = performance.now();
const defaultOrigin = 'http://localhost';
const trailingSlash = settings.config.trailingSlash;
/** Base request URL. */
let baseURL = new URL(settings.config.base, new URL(settings.config.site || '/', defaultOrigin));
const staticFileServer = sirv(fileURLToPath(settings.config.outDir), {
dev: true,
etag: true,
maxAge: 0,
setHeaders: (res, pathname, stats) => {
for (const [name, value] of Object.entries(headers ?? {})) {
if (value) res.setHeader(name, value);
}
},
});
// 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,
https: false,
});
info(
logging,
null,
msg.serverStart({
startupTime: performance.now() - timerStart,
resolvedUrls,
host: settings.config.server.host,
site: baseURL,
})
);
}
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 }));
Copy link
Member Author

Choose a reason for hiding this comment

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

This PR doesn't log port in use anymore as that's deferred to the Vite preview server, which also has it's own logging.

Copy link
Member

Choose a reason for hiding this comment

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

can you share a screenshot or video of what this looks like?

Copy link
Member Author

Choose a reason for hiding this comment

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

It should look something like this!

> @example/basics@0.0.1 preview /Users/bjorn/Work/oss/astro/examples/basics
> astro preview

Port 3000 is in use, trying another one...
Port 3001 is in use, trying another one...
  🚀  astro  v2.0.0-beta.0 started in 137ms
  
  ┃ Local    http://127.0.0.1:3002/
  ┃ Network  use --host to expose

I've made 3000 and 3001 occupied, so it'll log two lines that it's trying another one. Previously here's how it looks:

> @example/basics@0.0.1 preview /Users/bjorn/Work/oss/astro/examples/basics
> astro preview

10:40:28 [astro] Port 3000 in use. Trying a new one…
  🚀  astro  v2.0.0-beta.0 started in 2ms
  
  ┃ Local    http://localhost:3002/
  ┃ Network  use --host to expose

(Note: the 127.0.0.1 and localhost change is part of the main change of this PR to align with dev)

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();
let previewServer: VitePreviewServer;
try {
previewServer = await preview({
configFile: false,
base: settings.config.base,
appType: 'mpa',
build: {
outDir: fileURLToPath(settings.config.outDir),
},
preview: {
host: settings.config.server.host,
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;
}

// Start listening on `hostname:port`.
await startServer(startServerTime);
// Log server start URLs
info(
logging,
null,
msg.serverStart({
startupTime: performance.now() - startServerTime,
resolvedUrls: previewServer.resolvedUrls,
host: settings.config.server.host,
base: settings.config.base,
})
);

// Resolves once the server is closed
function closed() {
return new Promise<void>((resolve, reject) => {
httpServer!.addListener('close', resolve);
httpServer!.addListener('error', reject);
previewServer.httpServer.addListener('close', resolve);
previewServer.httpServer.addListener('error', reject);
});
}

return {
host,
port,
host: getResolvedHostForHttpServer(settings.config.server.host),
port: settings.config.server.port,
closed,
server: httpServer!,
server: previewServer.httpServer,
stop: async () => {
await new Promise((resolve, reject) => {
httpServer.close((err) => (err ? reject(err) : resolve(undefined)));
previewServer.httpServer.close((err) => (err ? reject(err) : resolve(undefined)));
});
},
};
Expand Down
Loading