Improve CLI & CLI Testing (#2245)

* Fix astro --version, astro --help

* Improve CLI testing

* nit: fix for windows

* nit: try different async cli tests

* fix: core dev

* nit: cleanup core

* nit: change port for config test

* nit: write config differently than project-root

* nit: cleanup AstroDevServer properties
This commit is contained in:
Jonathan Neal 2021-12-22 14:23:15 -05:00 committed by GitHub
parent 4208640587
commit b214b095f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 117 additions and 63 deletions

View file

@ -64,7 +64,7 @@ function resolveArgs(flags: Arguments): CLIState {
/** Display --help flag */ /** Display --help flag */
function printHelp() { function printHelp() {
console.error(` ${colors.bold('astro')} - Futuristic web development tool. console.log(` ${colors.bold('astro')} - Futuristic web development tool.
${colors.bold('Commands:')} ${colors.bold('Commands:')}
astro dev Run Astro in development mode. astro dev Run Astro in development mode.
astro build Build a pre-compiled production version of your site. astro build Build a pre-compiled production version of your site.
@ -85,8 +85,11 @@ function printHelp() {
/** Display --version flag */ /** Display --version flag */
async function printVersion() { async function printVersion() {
const pkg = JSON.parse(await fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf8')); const pkgURL = new URL('../../package.json', import.meta.url);
console.error(pkg.version); const pkg = JSON.parse(await fs.promises.readFile(pkgURL, 'utf8'));
const pkgVersion = pkg.version;
console.log(pkgVersion);
} }
/** Merge CLI flags & config options (CLI flags take priority) */ /** Merge CLI flags & config options (CLI flags take priority) */
@ -105,11 +108,21 @@ export async function cli(args: string[]) {
const options = { ...state.options }; const options = { ...state.options };
const projectRoot = options.projectRoot || flags._[3]; const projectRoot = options.projectRoot || flags._[3];
switch (state.cmd) {
case 'help':
printHelp();
return process.exit(0);
case 'version':
await printVersion();
return process.exit(0);
}
// logLevel // logLevel
let logging: LogOptions = { let logging: LogOptions = {
dest: defaultLogDestination, dest: defaultLogDestination,
level: 'info', level: 'info',
}; };
if (flags.verbose) logging.level = 'debug'; if (flags.verbose) logging.level = 'debug';
if (flags.silent) logging.level = 'silent'; if (flags.silent) logging.level = 'silent';
let config: AstroConfig; let config: AstroConfig;
@ -118,7 +131,7 @@ export async function cli(args: string[]) {
mergeCLIFlags(config, options); mergeCLIFlags(config, options);
} catch (err) { } catch (err) {
if (err instanceof z.ZodError) { if (err instanceof z.ZodError) {
console.log(formatConfigError(err)); console.error(formatConfigError(err));
} else { } else {
console.error(colors.red((err as any).toString() || err)); console.error(colors.red((err as any).toString() || err));
} }
@ -126,23 +139,15 @@ export async function cli(args: string[]) {
} }
switch (state.cmd) { switch (state.cmd) {
case 'help': {
printHelp();
process.exit(1);
return;
}
case 'version': {
await printVersion();
process.exit(0);
return;
}
case 'dev': { case 'dev': {
try { try {
const server = await devServer(config, { logging }); await devServer(config, { logging });
await new Promise(() => {}); // dont close dev server await new Promise(() => {}); // dont close dev server
} catch (err) { } catch (err) {
throwAndExit(err); throwAndExit(err);
} }
return; return;
} }
case 'build': { case 'build': {
@ -156,8 +161,7 @@ export async function cli(args: string[]) {
} }
case 'check': { case 'check': {
const ret = await check(config); const ret = await check(config);
process.exit(ret); return process.exit(ret);
return;
} }
case 'preview': { case 'preview': {
try { try {

View file

@ -57,27 +57,27 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
/** Dev server */ /** Dev server */
export class AstroDevServer { export class AstroDevServer {
app = connect(); app: connect.Server = connect();
httpServer: http.Server | undefined; config: AstroConfig;
devRoot: string;
hostname: string; hostname: string;
httpServer: http.Server | undefined;
logging: LogOptions;
manifest: ManifestData;
mostRecentRoute?: RouteData;
origin: string;
port: number; port: number;
private config: AstroConfig; routeCache: RouteCache = {};
private logging: LogOptions; site: URL | undefined;
private manifest: ManifestData; url: URL;
private mostRecentRoute?: RouteData; viteServer: vite.ViteDevServer | undefined;
private site: URL | undefined;
private devRoot: string;
private url: URL;
private origin: string;
private routeCache: RouteCache = {};
private viteServer: vite.ViteDevServer | undefined;
constructor(config: AstroConfig, options: DevOptions) { constructor(config: AstroConfig, options: DevOptions) {
this.config = config; this.config = config;
this.hostname = config.devOptions.hostname || 'localhost'; this.hostname = config.devOptions.hostname || 'localhost';
this.logging = options.logging; this.logging = options.logging;
this.port = config.devOptions.port; this.port = config.devOptions.port;
this.origin = `http://localhost:${this.port}`; this.origin = `http://${this.hostname}:${this.port}`;
this.site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined; this.site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
this.devRoot = this.site ? this.site.pathname : '/'; this.devRoot = this.site ? this.site.pathname : '/';
this.url = new URL(this.devRoot, this.origin); this.url = new URL(this.devRoot, this.origin);
@ -204,7 +204,7 @@ export class AstroDevServer {
public listen(devStart: number): Promise<void> { public listen(devStart: number): Promise<void> {
let showedPortTakenMsg = false; let showedPortTakenMsg = false;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const listen = () => { const appListen = () => {
this.httpServer = this.app.listen(this.port, this.hostname, () => { this.httpServer = this.app.listen(this.port, this.hostname, () => {
info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart })); info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}${this.devRoot}` })); info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}${this.devRoot}` }));
@ -220,7 +220,7 @@ export class AstroDevServer {
showedPortTakenMsg = true; // only print this once showedPortTakenMsg = true; // only print this once
} }
this.port++; this.port++;
return listen(); // retry return appListen(); // retry
} else { } else {
error(this.logging, 'astro', err.stack); error(this.logging, 'astro', err.stack);
this.httpServer?.removeListener('error', onError); this.httpServer?.removeListener('error', onError);
@ -228,7 +228,7 @@ export class AstroDevServer {
} }
}; };
listen(); appListen();
}); });
} }

View file

@ -38,8 +38,8 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
}).pipe(res); }).pipe(res);
}); });
let port = config.devOptions.port; let { hostname, port } = config.devOptions;
const { hostname } = config.devOptions;
let httpServer: http.Server; let httpServer: http.Server;
/** Expose dev server to `port` */ /** Expose dev server to `port` */

View file

@ -0,0 +1,47 @@
import { expect } from 'chai';
import { cli } from './test-utils.js';
import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
describe('astro cli', () => {
it('astro', async () => {
const proc = await cli();
expect(proc.stdout).to.include('astro - Futuristic web development tool');
});
it('astro --version', async () => {
const pkgURL = new URL('../package.json', import.meta.url);
const pkgVersion = await fs.readFile(pkgURL, 'utf8').then((data) => JSON.parse(data).version);
const proc = await cli('--version');
expect(proc.stdout).to.equal(pkgVersion);
});
it('astro dev', async () => {
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
const proc = cli('dev', '--project-root', fileURLToPath(projectRootURL));
let stdout = '';
for await (const chunk of proc.stdout) {
stdout += chunk;
if (chunk.includes('Local:')) break;
}
proc.kill();
expect(stdout).to.include('Server started');
});
it('astro build', async () => {
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
const proc = await cli('build', '--project-root', fileURLToPath(projectRootURL));
expect(proc.stdout).to.include('Done');
});
});

View file

@ -1,5 +1,6 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { devCLI, loadFixture } from './test-utils.js'; import { cli, loadFixture } from './test-utils.js';
import { fileURLToPath } from 'url';
describe('config', () => { describe('config', () => {
let hostnameFixture; let hostnameFixture;
@ -15,41 +16,40 @@ describe('config', () => {
}); });
it('can be specified via --hostname flag', async () => { it('can be specified via --hostname flag', async () => {
const cwd = './fixtures/config-hostname/'; const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
const cwdURL = new URL(cwd, import.meta.url); const proc = cli('dev', '--project-root', fileURLToPath(projectRootURL), '--hostname', '127.0.0.1');
const args = ['--hostname', '127.0.0.1'];
const proc = devCLI(cwdURL, args);
proc.stdout.setEncoding('utf8'); let stdout = '';
for await (const chunk of proc.stdout) { for await (const chunk of proc.stdout) {
if (/Local:/.test(chunk)) { stdout += chunk;
expect(chunk).to.include('127.0.0.1');
break; if (chunk.includes('Local:')) break;
}
} }
proc.kill(); proc.kill();
expect(stdout).to.include('127.0.0.1');
}); });
}); });
describe('path', () => { describe('path', () => {
it('can be passed via --config', async () => { it('can be passed via --config', async () => {
const cwd = './fixtures/config-path/'; const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
const cwdURL = new URL(cwd, import.meta.url); const configFileURL = new URL('./fixtures/config-path/config/my-config.mjs', import.meta.url);
const configPath = new URL('./config/my-config.mjs', cwdURL).pathname; const proc = cli('dev', '--project-root', fileURLToPath(projectRootURL), '--config', configFileURL.pathname);
const args = ['--config', configPath];
const proc = devCLI(cwdURL, args);
proc.stdout.setEncoding('utf8'); let stdout = '';
for await (const chunk of proc.stdout) { for await (const chunk of proc.stdout) {
if (/Server started/.test(chunk)) { stdout += chunk;
break;
} if (chunk.includes('Local:')) break;
} }
proc.kill(); proc.kill();
expect(stdout).to.include('127.0.0.1');
}); });
}); });

View file

@ -1,5 +1,6 @@
export default { export default {
renderers: [ devOptions: {
'@astrojs/renderer-preact' hostname: '127.0.0.1',
] port: 8080,
},
} }

View file

@ -99,11 +99,13 @@ function merge(a, b) {
return c; return c;
} }
const cliURL = new URL('../astro.js', import.meta.url); const cliPath = fileURLToPath(new URL('../astro.js', import.meta.url));
/** Start Dev server via CLI */ /** Returns a process running the Astro CLI. */
export function devCLI(root, additionalArgs = []) { export function cli(/** @type {string[]} */ ...args) {
const args = [cliURL.pathname, 'dev', '--project-root', root.pathname].concat(additionalArgs); const spawned = execa('node', [cliPath, ...args]);
const proc = execa('node', args);
return proc; spawned.stdout.setEncoding('utf8');
return spawned;
} }