Feat: expose server on local network with new --host flag (#2760)
* feat: update config to support bool --hostname * fix: show localhost for --hostname=true * feat: address logging feature parity w/ Vite * chore: update type docs * refactor: extract local, network prefs to variable * feat: add --host to --help output * feat: deprecate --hostname, add --host * feat: add --host tests * feat: update preview to support new flags * fix: show --host in dev server log * feat: update config tests for --host flag * chore: test lint * chore: update lock with new fixture * chore: add changeset * refactor: add more details to JSdocs * fix: update path tests * feat: only expose when --host is not local * fix: make flag --help less verbose * fix: address @types comments * fix: lint * chore: remove unused import * fix: use host flag for config test * fix: ensure local logs come before network * refactor: switch up that network logging one last time! * feat: update unit tests * chore: remove debugging block * fix: only parse network logs if network is present
This commit is contained in:
parent
2bb2c2f7d1
commit
77b9c95352
16 changed files with 239 additions and 86 deletions
5
.changeset/breezy-walls-guess.md
Normal file
5
.changeset/breezy-walls-guess.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Introduce a new --host flag + host devOption to expose your server on a network IP
|
|
@ -24,6 +24,7 @@ export interface CLIFlags {
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
site?: string;
|
site?: string;
|
||||||
sitemap?: boolean;
|
sitemap?: boolean;
|
||||||
|
host?: string | boolean;
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
config?: string;
|
config?: string;
|
||||||
|
@ -314,12 +315,29 @@ export interface AstroUserConfig {
|
||||||
* @name Dev Options
|
* @name Dev Options
|
||||||
*/
|
*/
|
||||||
devOptions?: {
|
devOptions?: {
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name devOptions.host
|
||||||
|
* @type {string | boolean}
|
||||||
|
* @default `false`
|
||||||
|
* @version 0.24.0
|
||||||
|
* @description
|
||||||
|
* Set which network IP addresses the dev server should listen on (i.e. non-localhost IPs).
|
||||||
|
* - `false` - do not expose on a network IP address
|
||||||
|
* - `true` - listen on all addresses, including LAN and public addresses
|
||||||
|
* - `[custom-address]` - expose on a network IP address at `[custom-address]`
|
||||||
|
*/
|
||||||
|
host?: string | boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
* @name devOptions.hostname
|
* @name devOptions.hostname
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @default `'localhost'`
|
* @default `'localhost'`
|
||||||
|
* @deprecated Use `host` instead
|
||||||
* @description
|
* @description
|
||||||
|
* > **This option is deprecated.** Consider using `host` instead.
|
||||||
|
*
|
||||||
* Set which IP addresses the dev server should listen on. Set this to 0.0.0.0 to listen on all addresses, including LAN and public addresses.
|
* Set which IP addresses the dev server should listen on. Set this to 0.0.0.0 to listen on all addresses, including LAN and public addresses.
|
||||||
*/
|
*/
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
|
|
|
@ -38,6 +38,7 @@ function printHelp() {
|
||||||
title('Flags');
|
title('Flags');
|
||||||
table(
|
table(
|
||||||
[
|
[
|
||||||
|
['--host [optional IP]', 'Expose server on network'],
|
||||||
['--config <path>', 'Specify the path to the Astro config file.'],
|
['--config <path>', 'Specify the path to the Astro config file.'],
|
||||||
['--project-root <path>', 'Specify the path to the project root folder.'],
|
['--project-root <path>', 'Specify the path to the project root folder.'],
|
||||||
['--no-sitemap', 'Disable sitemap generation (build only).'],
|
['--no-sitemap', 'Disable sitemap generation (build only).'],
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.default({}),
|
.default({}),
|
||||||
devOptions: z
|
devOptions: z
|
||||||
.object({
|
.object({
|
||||||
|
host: z.union([z.string(), z.boolean()]).optional().default(false),
|
||||||
hostname: z.string().optional().default('localhost'),
|
hostname: z.string().optional().default('localhost'),
|
||||||
port: z.number().optional().default(3000),
|
port: z.number().optional().default(3000),
|
||||||
trailingSlash: z
|
trailingSlash: z
|
||||||
|
@ -125,6 +126,7 @@ function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
||||||
port: typeof flags.port === 'number' ? flags.port : undefined,
|
port: typeof flags.port === 'number' ? flags.port : undefined,
|
||||||
config: typeof flags.config === 'string' ? flags.config : undefined,
|
config: typeof flags.config === 'string' ? flags.config : undefined,
|
||||||
hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined,
|
hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined,
|
||||||
|
host: typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
|
||||||
legacyBuild: typeof flags.legacyBuild === 'boolean' ? flags.legacyBuild : false,
|
legacyBuild: typeof flags.legacyBuild === 'boolean' ? flags.legacyBuild : false,
|
||||||
experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : false,
|
experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : false,
|
||||||
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : false,
|
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : false,
|
||||||
|
@ -138,6 +140,7 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
|
||||||
if (typeof flags.sitemap === 'boolean') astroConfig.buildOptions.sitemap = flags.sitemap;
|
if (typeof flags.sitemap === 'boolean') astroConfig.buildOptions.sitemap = flags.sitemap;
|
||||||
if (typeof flags.site === 'string') astroConfig.buildOptions.site = flags.site;
|
if (typeof flags.site === 'string') astroConfig.buildOptions.site = flags.site;
|
||||||
if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port;
|
if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port;
|
||||||
|
if (typeof flags.host === 'string' || typeof flags.host === 'boolean') astroConfig.devOptions.host = flags.host;
|
||||||
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
|
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
|
||||||
if (typeof flags.legacyBuild === 'boolean') astroConfig.buildOptions.legacyBuild = flags.legacyBuild;
|
if (typeof flags.legacyBuild === 'boolean') astroConfig.buildOptions.legacyBuild = flags.legacyBuild;
|
||||||
if (typeof flags.experimentalSsr === 'boolean') {
|
if (typeof flags.experimentalSsr === 'boolean') {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { createVite } from '../create-vite.js';
|
||||||
import { defaultLogOptions, info, warn, LogOptions } from '../logger.js';
|
import { defaultLogOptions, info, warn, LogOptions } from '../logger.js';
|
||||||
import * as vite from 'vite';
|
import * as vite from 'vite';
|
||||||
import * as msg from '../messages.js';
|
import * as msg from '../messages.js';
|
||||||
import { getLocalAddress } from './util.js';
|
import { getResolvedHostForVite } from './util.js';
|
||||||
|
|
||||||
export interface DevOptions {
|
export interface DevOptions {
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
@ -24,13 +24,13 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
|
||||||
polyfill(globalThis, {
|
polyfill(globalThis, {
|
||||||
exclude: 'window document',
|
exclude: 'window document',
|
||||||
});
|
});
|
||||||
// start the server
|
|
||||||
|
// TODO: remove call once --hostname is baselined
|
||||||
|
const host = getResolvedHostForVite(config);
|
||||||
const viteUserConfig = vite.mergeConfig(
|
const viteUserConfig = vite.mergeConfig(
|
||||||
{
|
{
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
server: {
|
server: { host },
|
||||||
host: config.devOptions.hostname,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
config.vite || {}
|
config.vite || {}
|
||||||
);
|
);
|
||||||
|
@ -38,15 +38,9 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
|
||||||
const viteServer = await vite.createServer(viteConfig);
|
const viteServer = await vite.createServer(viteConfig);
|
||||||
await viteServer.listen(config.devOptions.port);
|
await viteServer.listen(config.devOptions.port);
|
||||||
|
|
||||||
const address = viteServer.httpServer!.address() as AddressInfo;
|
const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo;
|
||||||
const localAddress = getLocalAddress(address.address, config.devOptions.hostname);
|
|
||||||
// Log to console
|
|
||||||
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
|
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
|
||||||
info(
|
info(options.logging, null, msg.devStart({ startupTime: performance.now() - devStart, config, devServerAddressInfo, site, https: !!viteUserConfig.server?.https }));
|
||||||
options.logging,
|
|
||||||
null,
|
|
||||||
msg.devStart({ startupTime: performance.now() - devStart, port: address.port, localAddress, networkAddress: address.address, site, https: !!viteUserConfig.server?.https })
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
|
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
|
||||||
if (currentVersion.includes('-')) {
|
if (currentVersion.includes('-')) {
|
||||||
|
@ -54,7 +48,7 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
address,
|
address: devServerAddressInfo,
|
||||||
stop: () => viteServer.close(),
|
stop: () => viteServer.close(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
|
|
||||||
|
export const localIps = new Set(['localhost', '127.0.0.1']);
|
||||||
|
|
||||||
/** Pad string () */
|
/** Pad string () */
|
||||||
export function pad(input: string, minLength: number, dir?: 'left' | 'right'): string {
|
export function pad(input: string, minLength: number, dir?: 'left' | 'right'): string {
|
||||||
let output = input;
|
let output = input;
|
||||||
|
@ -11,10 +15,36 @@ export function emoji(char: string, fallback: string) {
|
||||||
return process.platform !== 'win32' ? char : fallback;
|
return process.platform !== 'win32' ? char : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocalAddress(serverAddress: string, configHostname: string): string {
|
// TODO: remove once --hostname is baselined
|
||||||
if (configHostname === 'localhost' || serverAddress === '127.0.0.1' || serverAddress === '0.0.0.0') {
|
export function getResolvedHostForVite(config: AstroConfig) {
|
||||||
|
if (config.devOptions.host === false && config.devOptions.hostname !== 'localhost') {
|
||||||
|
return config.devOptions.hostname;
|
||||||
|
} else {
|
||||||
|
return config.devOptions.host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalAddress(serverAddress: string, config: AstroConfig): string {
|
||||||
|
// TODO: remove once --hostname is baselined
|
||||||
|
const host = getResolvedHostForVite(config);
|
||||||
|
if (typeof host === 'boolean' || host === 'localhost') {
|
||||||
return 'localhost';
|
return 'localhost';
|
||||||
} else {
|
} else {
|
||||||
return serverAddress;
|
return serverAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NetworkLogging = 'none' | 'host-to-expose' | 'visible';
|
||||||
|
|
||||||
|
export function getNetworkLogging(config: AstroConfig): NetworkLogging {
|
||||||
|
// TODO: remove once --hostname is baselined
|
||||||
|
const host = getResolvedHostForVite(config);
|
||||||
|
|
||||||
|
if (host === false) {
|
||||||
|
return 'host-to-expose';
|
||||||
|
} else if (typeof host === 'string' && localIps.has(host)) {
|
||||||
|
return 'none';
|
||||||
|
} else {
|
||||||
|
return 'visible';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
* Dev server messages (organized here to prevent clutter)
|
* Dev server messages (organized here to prevent clutter)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AddressInfo } from 'net';
|
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
import { bold, dim, red, green, underline, yellow, bgYellow, cyan, bgGreen, black } from 'kleur/colors';
|
import { bold, dim, red, green, underline, yellow, bgYellow, cyan, bgGreen, black } from 'kleur/colors';
|
||||||
import { pad, emoji } from './dev/util.js';
|
import { pad, emoji, getLocalAddress, getNetworkLogging } from './dev/util.js';
|
||||||
|
import os from 'os';
|
||||||
|
import type { AddressInfo } from 'net';
|
||||||
|
import type { AstroConfig } from '../@types/astro';
|
||||||
|
|
||||||
const PREFIX_PADDING = 6;
|
const PREFIX_PADDING = 6;
|
||||||
|
|
||||||
|
@ -30,31 +32,50 @@ export function hmr({ file }: { file: string }): string {
|
||||||
/** Display dev server host and startup time */
|
/** Display dev server host and startup time */
|
||||||
export function devStart({
|
export function devStart({
|
||||||
startupTime,
|
startupTime,
|
||||||
port,
|
devServerAddressInfo,
|
||||||
localAddress,
|
config,
|
||||||
networkAddress,
|
|
||||||
https,
|
https,
|
||||||
site,
|
site,
|
||||||
}: {
|
}: {
|
||||||
startupTime: number;
|
startupTime: number;
|
||||||
port: number;
|
devServerAddressInfo: AddressInfo;
|
||||||
localAddress: string;
|
config: AstroConfig;
|
||||||
networkAddress: string;
|
|
||||||
https: boolean;
|
https: boolean;
|
||||||
site: URL | undefined;
|
site: URL | undefined;
|
||||||
}): string {
|
}): string {
|
||||||
// PACAKGE_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 rootPath = site ? site.pathname : '/';
|
||||||
const toDisplayUrl = (hostname: string) => `${https ? 'https' : 'http'}://${hostname}:${port}${rootPath}`;
|
const localPrefix = `${dim('┃')} Local `;
|
||||||
|
const networkPrefix = `${dim('┃')} Network `;
|
||||||
|
|
||||||
const messages = [
|
const { address: networkAddress, port } = devServerAddressInfo;
|
||||||
`${emoji('🚀 ', '')}${bgGreen(black(` astro `))} ${green(`v${version}`)} ${dim(`started in ${Math.round(startupTime)}ms`)}`,
|
const localAddress = getLocalAddress(networkAddress, config);
|
||||||
'',
|
const networkLogging = getNetworkLogging(config);
|
||||||
`${dim('┃')} Local ${bold(cyan(toDisplayUrl(localAddress)))}`,
|
const toDisplayUrl = (hostname: string) => `${https ? 'https' : 'http'}://${hostname}:${port}${rootPath}`;
|
||||||
`${dim('┃')} Network ${bold(cyan(toDisplayUrl(networkAddress)))}`,
|
let addresses = [];
|
||||||
'',
|
|
||||||
];
|
if (networkLogging === 'none') {
|
||||||
|
addresses = [`${localPrefix}${bold(cyan(toDisplayUrl(localAddress)))}`];
|
||||||
|
} else if (networkLogging === 'host-to-expose') {
|
||||||
|
addresses = [`${localPrefix}${bold(cyan(toDisplayUrl(localAddress)))}`, `${networkPrefix}${dim('use --host to expose')}`];
|
||||||
|
} else {
|
||||||
|
addresses = Object.values(os.networkInterfaces())
|
||||||
|
.flatMap((networkInterface) => networkInterface ?? [])
|
||||||
|
.filter((networkInterface) => networkInterface?.address && networkInterface?.family === 'IPv4')
|
||||||
|
.map(({ address }) => {
|
||||||
|
if (address.includes('127.0.0.1')) {
|
||||||
|
const displayAddress = address.replace('127.0.0.1', localAddress);
|
||||||
|
return `${localPrefix}${bold(cyan(toDisplayUrl(displayAddress)))}`;
|
||||||
|
} else {
|
||||||
|
return `${networkPrefix}${bold(cyan(toDisplayUrl(address)))}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// ensure Local logs come before Network
|
||||||
|
.sort((msg) => (msg.startsWith(localPrefix) ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = [`${emoji('🚀 ', '')}${bgGreen(black(` astro `))} ${green(`v${version}`)} ${dim(`started in ${Math.round(startupTime)}ms`)}`, '', ...addresses, ''];
|
||||||
return messages.map((msg) => ` ${msg}`).join('\n');
|
return messages.map((msg) => ` ${msg}`).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { AstroConfig } from '../../@types/astro';
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
import type { Stats } from 'fs';
|
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import sirv from 'sirv';
|
import sirv from 'sirv';
|
||||||
|
@ -8,16 +7,15 @@ import { performance } from 'perf_hooks';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import * as msg from '../messages.js';
|
import * as msg from '../messages.js';
|
||||||
import { error, info } from '../logger.js';
|
import { error, info } from '../logger.js';
|
||||||
import { appendForwardSlash, trimSlashes } from '../path.js';
|
|
||||||
import { getLocalAddress } from '../dev/util.js';
|
|
||||||
import { subpathNotUsedTemplate, notFoundTemplate } from '../../template/4xx.js';
|
import { subpathNotUsedTemplate, notFoundTemplate } from '../../template/4xx.js';
|
||||||
|
import { getResolvedHostForHttpServer } from './util.js';
|
||||||
|
|
||||||
interface PreviewOptions {
|
interface PreviewOptions {
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreviewServer {
|
export interface PreviewServer {
|
||||||
hostname: string;
|
host?: string;
|
||||||
port: number;
|
port: number;
|
||||||
server: http.Server;
|
server: http.Server;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
|
@ -75,7 +73,8 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let { hostname, port } = config.devOptions;
|
let { port } = config.devOptions;
|
||||||
|
const host = getResolvedHostForHttpServer(config);
|
||||||
|
|
||||||
let httpServer: http.Server;
|
let httpServer: http.Server;
|
||||||
|
|
||||||
|
@ -85,12 +84,10 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
|
||||||
let showedListenMsg = false;
|
let showedListenMsg = false;
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const listen = () => {
|
const listen = () => {
|
||||||
httpServer = server.listen(port, hostname, async () => {
|
httpServer = server.listen(port, host, async () => {
|
||||||
if (!showedListenMsg) {
|
if (!showedListenMsg) {
|
||||||
const { address: networkAddress } = server.address() as AddressInfo;
|
const devServerAddressInfo = server.address() as AddressInfo;
|
||||||
const localAddress = getLocalAddress(networkAddress, hostname);
|
info(logging, null, msg.devStart({ startupTime: performance.now() - timerStart, config, devServerAddressInfo, https: false, site: baseURL }));
|
||||||
|
|
||||||
info(logging, null, msg.devStart({ startupTime: performance.now() - timerStart, port, localAddress, networkAddress, https: false, site: baseURL }));
|
|
||||||
}
|
}
|
||||||
showedListenMsg = true;
|
showedListenMsg = true;
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -121,7 +118,7 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
|
||||||
await startServer(startServerTime);
|
await startServer(startServerTime);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostname,
|
host,
|
||||||
port,
|
port,
|
||||||
server: httpServer!,
|
server: httpServer!,
|
||||||
stop: async () => {
|
stop: async () => {
|
||||||
|
|
17
packages/astro/src/core/preview/util.ts
Normal file
17
packages/astro/src/core/preview/util.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
|
|
||||||
|
export function getResolvedHostForHttpServer(config: AstroConfig) {
|
||||||
|
const { host, hostname } = config.devOptions;
|
||||||
|
|
||||||
|
if (host === false && hostname === 'localhost') {
|
||||||
|
// Use a secure default
|
||||||
|
return '127.0.0.1';
|
||||||
|
} else if (host === true) {
|
||||||
|
// If passed --host in the CLI without arguments
|
||||||
|
return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs)
|
||||||
|
} else if (typeof host === 'string') {
|
||||||
|
return host;
|
||||||
|
} else {
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,15 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { cli, parseCliDevStart } from './test-utils.js';
|
import { cli, parseCliDevStart, cliServerLogSetup } from './test-utils.js';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { isIPv4 } from 'net';
|
||||||
|
|
||||||
describe('astro cli', () => {
|
describe('astro cli', () => {
|
||||||
|
const cliServerLogSetupWithFixture = (flags, cmd) => {
|
||||||
|
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
||||||
|
return cliServerLogSetup(['--project-root', fileURLToPath(projectRootURL), ...flags], cmd);
|
||||||
|
};
|
||||||
|
|
||||||
it('astro', async () => {
|
it('astro', async () => {
|
||||||
const proc = await cli();
|
const proc = await cli();
|
||||||
|
|
||||||
|
@ -32,28 +38,52 @@ describe('astro cli', () => {
|
||||||
expect(messages[0]).to.contain('started in');
|
expect(messages[0]).to.contain('started in');
|
||||||
});
|
});
|
||||||
|
|
||||||
const hostnames = [undefined, '0.0.0.0', '127.0.0.1'];
|
['dev', 'preview'].forEach((cmd) => {
|
||||||
|
const networkLogFlags = [['--host'], ['--host', '0.0.0.0'], ['--hostname', '0.0.0.0']];
|
||||||
|
networkLogFlags.forEach(([flag, flagValue]) => {
|
||||||
|
it(`astro ${cmd} ${flag} ${flagValue ?? ''} - network log`, async () => {
|
||||||
|
const { local, network } = await cliServerLogSetupWithFixture(flagValue ? [flag, flagValue] : [flag], cmd);
|
||||||
|
|
||||||
hostnames.forEach((hostname) => {
|
expect(local).to.not.be.undefined;
|
||||||
const hostnameArgs = hostname ? ['--hostname', hostname] : [];
|
expect(network).to.not.be.undefined;
|
||||||
it(`astro dev ${hostnameArgs.join(' ') || '(no --hostname)'}`, async () => {
|
|
||||||
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
|
||||||
const proc = cli('dev', '--project-root', fileURLToPath(projectRootURL), ...hostnameArgs);
|
|
||||||
|
|
||||||
const { messages } = await parseCliDevStart(proc);
|
const localURL = new URL(local);
|
||||||
|
const networkURL = new URL(network);
|
||||||
|
|
||||||
const local = messages[1].replace(/Local\s*/g, '');
|
expect(localURL.hostname).to.be.equal(flagValue ?? 'localhost', `Expected local URL to be on localhost`);
|
||||||
const network = messages[2].replace(/Network\s*/g, '');
|
// Note: our tests run in parallel so this could be 3000+!
|
||||||
|
expect(Number.parseInt(localURL.port)).to.be.greaterThanOrEqual(3000, `Expected Port to be >= 3000`);
|
||||||
|
expect(networkURL.port).to.be.equal(localURL.port, `Expected local and network ports to be equal`);
|
||||||
|
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --host flag`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(local).to.not.be.undefined;
|
|
||||||
expect(network).to.not.be.undefined;
|
|
||||||
const localURL = new URL(local);
|
|
||||||
const networkURL = new URL(network);
|
|
||||||
|
|
||||||
expect(localURL.hostname).to.be.equal('localhost', `Expected local URL to be on localhost`);
|
const hostToExposeFlags = [['', ''], ['--hostname', 'localhost']];
|
||||||
// Note: our tests run in parallel so this could be 3000+!
|
hostToExposeFlags.forEach(([flag, flagValue]) => {
|
||||||
expect(Number.parseInt(localURL.port)).to.be.greaterThanOrEqual(3000, `Expected Port to be >= 3000`);
|
it(`astro ${cmd} ${flag} ${flagValue} - host to expose`, async () => {
|
||||||
expect(networkURL.hostname).to.be.equal(hostname ?? '127.0.0.1', `Expected Network URL to use passed hostname`);
|
const { local, network } = await cliServerLogSetupWithFixture([flag, flagValue], cmd);
|
||||||
|
|
||||||
|
expect(local).to.not.be.undefined;
|
||||||
|
expect(network).to.not.be.undefined;
|
||||||
|
const localURL = new URL(local);
|
||||||
|
|
||||||
|
expect(localURL.hostname).to.be.equal('localhost', `Expected local URL to be on localhost`);
|
||||||
|
expect(() => new URL(networkURL)).to.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const noNetworkLogFlags = [['--host', 'localhost'], ['--host', '127.0.0.1'], ['--hostname', '127.0.0.1']];
|
||||||
|
noNetworkLogFlags.forEach(([flag, flagValue]) => {
|
||||||
|
it(`astro ${cmd} ${flag} ${flagValue} - no network log`, async () => {
|
||||||
|
const { local, network } = await cliServerLogSetupWithFixture([flag, flagValue], cmd);
|
||||||
|
|
||||||
|
expect(local).to.not.be.undefined;
|
||||||
|
expect(network).to.be.undefined;
|
||||||
|
|
||||||
|
const localURL = new URL(local);
|
||||||
|
expect(localURL.hostname).to.be.equal(flagValue, `Expected local URL to be on localhost`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { cli, loadFixture } from './test-utils.js';
|
import { cli, loadFixture, cliServerLogSetup } from './test-utils.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { isIPv4 } from 'net';
|
||||||
|
|
||||||
describe('config', () => {
|
describe('config', () => {
|
||||||
let hostnameFixture;
|
let hostnameFixture;
|
||||||
|
let hostFixture;
|
||||||
let portFixture;
|
let portFixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
[hostnameFixture, portFixture] = await Promise.all([loadFixture({ projectRoot: './fixtures/config-hostname/' }), loadFixture({ projectRoot: './fixtures/config-port/' })]);
|
[hostnameFixture, hostFixture, portFixture] = await Promise.all([
|
||||||
|
loadFixture({ projectRoot: './fixtures/config-hostname/' }),
|
||||||
|
loadFixture({ projectRoot: './fixtures/config-host/' }),
|
||||||
|
loadFixture({ projectRoot: './fixtures/config-port/' }),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: remove test once --hostname is baselined
|
||||||
describe('hostname', () => {
|
describe('hostname', () => {
|
||||||
it('can be specified in astro.config.mjs', async () => {
|
it('can be specified in astro.config.mjs', async () => {
|
||||||
expect(hostnameFixture.config.devOptions.hostname).to.equal('0.0.0.0');
|
expect(hostnameFixture.config.devOptions.hostname).to.equal('0.0.0.0');
|
||||||
|
@ -17,19 +24,24 @@ describe('config', () => {
|
||||||
|
|
||||||
it('can be specified via --hostname flag', async () => {
|
it('can be specified via --hostname flag', async () => {
|
||||||
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
||||||
const proc = cli('dev', '--project-root', fileURLToPath(projectRootURL), '--hostname', '127.0.0.1');
|
const { network } = await cliServerLogSetup(['--project-root', fileURLToPath(projectRootURL), '--hostname', '0.0.0.0']);
|
||||||
|
|
||||||
let stdout = '';
|
const networkURL = new URL(network);
|
||||||
|
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --hostname flag`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
for await (const chunk of proc.stdout) {
|
describe('host', () => {
|
||||||
stdout += chunk;
|
it('can be specified in astro.config.mjs', async () => {
|
||||||
|
expect(hostFixture.config.devOptions.host).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
if (chunk.includes('Local')) break;
|
it('can be specified via --host flag', async () => {
|
||||||
}
|
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
||||||
|
const { network } = await cliServerLogSetup(['--project-root', fileURLToPath(projectRootURL), '--host']);
|
||||||
|
|
||||||
proc.kill();
|
const networkURL = new URL(network);
|
||||||
|
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --hostname flag`);
|
||||||
expect(stdout).to.include('127.0.0.1');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,19 +49,10 @@ describe('config', () => {
|
||||||
it('can be passed via --config', async () => {
|
it('can be passed via --config', async () => {
|
||||||
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
|
||||||
const configFileURL = new URL('./fixtures/config-path/config/my-config.mjs', import.meta.url);
|
const configFileURL = new URL('./fixtures/config-path/config/my-config.mjs', import.meta.url);
|
||||||
const proc = cli('dev', '--project-root', fileURLToPath(projectRootURL), '--config', configFileURL.pathname);
|
const { network } = await cliServerLogSetup(['--project-root', fileURLToPath(projectRootURL), '--config', configFileURL.pathname]);
|
||||||
|
|
||||||
let stdout = '';
|
const networkURL = new URL(network);
|
||||||
|
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --hostname flag`);
|
||||||
for await (const chunk of proc.stdout) {
|
|
||||||
stdout += chunk;
|
|
||||||
|
|
||||||
if (chunk.includes('Local')) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
proc.kill();
|
|
||||||
|
|
||||||
expect(stdout).to.include('127.0.0.1');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
6
packages/astro/test/fixtures/config-host/astro.config.mjs
vendored
Normal file
6
packages/astro/test/fixtures/config-host/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
export default {
|
||||||
|
devOptions: {
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
}
|
8
packages/astro/test/fixtures/config-host/package.json
vendored
Normal file
8
packages/astro/test/fixtures/config-host/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@astrojs/test-config-host",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
devOptions: {
|
devOptions: {
|
||||||
hostname: '127.0.0.1',
|
host: true,
|
||||||
port: 8080,
|
port: 8080,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,4 +143,18 @@ export async function parseCliDevStart(proc) {
|
||||||
return { messages };
|
return { messages };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cliServerLogSetup(flags = [], cmd = 'dev') {
|
||||||
|
const proc = cli(cmd, ...flags);
|
||||||
|
|
||||||
|
const { messages } = await parseCliDevStart(proc);
|
||||||
|
|
||||||
|
const localRaw = (messages[1] ?? '').includes('Local') ? messages[1] : undefined;
|
||||||
|
const networkRaw = (messages[2] ?? '').includes('Network') ? messages[2] : undefined;
|
||||||
|
|
||||||
|
const local = localRaw?.replace(/Local\s*/g, '');
|
||||||
|
const network = networkRaw?.replace(/Network\s*/g, '');
|
||||||
|
|
||||||
|
return { local, network };
|
||||||
|
}
|
||||||
|
|
||||||
export const isWindows = os.platform() === 'win32';
|
export const isWindows = os.platform() === 'win32';
|
||||||
|
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
@ -754,6 +754,12 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/config-host:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/config-hostname:
|
packages/astro/test/fixtures/config-hostname:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
|
Loading…
Add table
Reference in a new issue