mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-10 08:51:24 +00:00
feat(cli): add "rocket preview" command to verify a production build
This commit is contained in:
5
.changeset/nasty-suns-taste.md
Normal file
5
.changeset/nasty-suns-taste.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@rocket/cli': patch
|
||||
---
|
||||
|
||||
Add `rocket preview` command to enable fast checking of the production build
|
||||
@@ -35,6 +35,7 @@
|
||||
"setup:ts-configs": "node scripts/generate-ts-configs.mjs",
|
||||
"start:experimental": "NODE_DEBUG=engine:rendering node --no-warnings --experimental-loader ./packages/engine/src/litCssLoader.js packages/cli/src/cli.js start --open",
|
||||
"start": "NODE_DEBUG=engine:rendering node --trace-warnings packages/cli/src/cli.js start --open",
|
||||
"preview": "node packages/cli/src/cli.js preview --open",
|
||||
"test": "yarn test:node && yarn test:web",
|
||||
"test:integration": "playwright test packages/*/test-node/*.spec.js --retries=3",
|
||||
"test:node": "yarn test:unit && yarn test:integration",
|
||||
|
||||
@@ -88,6 +88,7 @@ export class RocketBuild {
|
||||
|
||||
async build() {
|
||||
await this.cli.events.dispatchEventDone('build-start');
|
||||
await this.cli.clearOutputDirs();
|
||||
|
||||
this.engine = new Engine();
|
||||
this.engine.setOptions({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RocketStart } from './RocketStart.js';
|
||||
import { RocketBuild } from './RocketBuild.js';
|
||||
import { RocketInit } from './RocketInit.js';
|
||||
import { RocketUpgrade } from './RocketUpgrade.js';
|
||||
import { RocketPreview } from './RocketPreview.js';
|
||||
// import { ignore } from './images/ignore.js';
|
||||
|
||||
import path from 'path';
|
||||
@@ -34,7 +35,7 @@ export class RocketCli {
|
||||
open: false,
|
||||
cwd: process.cwd(),
|
||||
inputDir: 'FALLBACK',
|
||||
outputDir: '_site',
|
||||
outputDir: 'FALLBACK',
|
||||
outputDevDir: '_site-dev',
|
||||
|
||||
serviceWorkerSourcePath: '',
|
||||
@@ -93,6 +94,9 @@ export class RocketCli {
|
||||
if (this.options.inputDir === 'FALLBACK') {
|
||||
this.options.inputDir = path.join(this.options.cwd, 'site', 'pages');
|
||||
}
|
||||
if (this.options.outputDir === 'FALLBACK') {
|
||||
this.options.outputDir = path.join(this.options.cwd, '_site');
|
||||
}
|
||||
if (this.options.inputDir instanceof URL) {
|
||||
this.options.inputDir = this.options.inputDir.pathname;
|
||||
}
|
||||
@@ -122,7 +126,6 @@ export class RocketCli {
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
await this.clearOutputDirs();
|
||||
if (!this.options.presets) {
|
||||
return;
|
||||
}
|
||||
@@ -180,6 +183,7 @@ export class RocketCli {
|
||||
{ plugin: RocketInit, options: {} },
|
||||
// { plugin: RocketLint },
|
||||
{ plugin: RocketUpgrade, options: {} },
|
||||
{ plugin: RocketPreview, options: {} },
|
||||
];
|
||||
|
||||
if (Array.isArray(this.options.setupCliPlugins)) {
|
||||
|
||||
79
packages/cli/src/RocketPreview.js
Executable file
79
packages/cli/src/RocketPreview.js
Executable file
@@ -0,0 +1,79 @@
|
||||
import { logPreviewMessage } from './preview/logPreviewMessage.js';
|
||||
import { startDevServer } from '@web/dev-server';
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { gray, bold } from 'colorette';
|
||||
|
||||
export class RocketPreview {
|
||||
/**
|
||||
* @param {import('commander').Command} program
|
||||
* @param {import('./RocketCli.js').RocketCli} cli
|
||||
*/
|
||||
async setupCommand(program, cli) {
|
||||
this.cli = cli;
|
||||
this.active = true;
|
||||
|
||||
program
|
||||
.command('preview')
|
||||
.option('-i, --input-dir <path>', 'path to the folder with the build html files')
|
||||
.option('-o, --open', 'automatically open the browser')
|
||||
.action(async cliOptions => {
|
||||
cli.setOptions(cliOptions);
|
||||
cli.activePlugin = this;
|
||||
|
||||
await this.preview();
|
||||
});
|
||||
}
|
||||
|
||||
async preview() {
|
||||
if (!this.cli) {
|
||||
return;
|
||||
}
|
||||
|
||||
// for typescript as `this.cli.options.outputDir` supports other inputs as well
|
||||
// but the cli will normalize it to a string before calling plugins
|
||||
if (
|
||||
typeof this.cli.options.inputDir !== 'string' ||
|
||||
typeof this.cli.options.outputDir !== 'string'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootIndexHtml = path.join(this.cli.options.outputDir, 'index.html');
|
||||
if (!existsSync(rootIndexHtml)) {
|
||||
console.log(`${bold(`👀 Previewing Production Build`)}`);
|
||||
console.log('');
|
||||
console.log(` 🛑 No index.html found in the build directory ${gray(`${rootIndexHtml}`)}`);
|
||||
console.log(' 🤔 Did you forget to run `rocket build` before?');
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {import('@web/dev-server').DevServerConfig} */
|
||||
const config = {
|
||||
open: this.cli.options.open,
|
||||
rootDir: this.cli.options.outputDir,
|
||||
clearTerminalOnReload: false,
|
||||
};
|
||||
|
||||
try {
|
||||
this.devServer = await startDevServer({
|
||||
config,
|
||||
logStartMessage: false,
|
||||
readCliArgs: false,
|
||||
readFileConfig: false,
|
||||
// argv: this.__argv,
|
||||
});
|
||||
logPreviewMessage({ devServerOptions: this.devServer.config }, console);
|
||||
} catch (e) {
|
||||
console.log('🛑 Starting preview server failed');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.devServer) {
|
||||
await this.devServer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export class RocketStart {
|
||||
if (!this.cli) {
|
||||
return;
|
||||
}
|
||||
await this.cli.clearOutputDirs();
|
||||
|
||||
// TODO: enable URL support in the Engine and remove this "workaround"
|
||||
if (
|
||||
|
||||
34
packages/cli/src/helpers/infoMessages.js
Normal file
34
packages/cli/src/helpers/infoMessages.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import ip from 'ip';
|
||||
import { white, cyan } from 'colorette';
|
||||
|
||||
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DevServerConfig} devServerOptions
|
||||
* @param {string} host
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
export function createAddress(devServerOptions, host, path) {
|
||||
return `http${devServerOptions.http2 ? 's' : ''}://${host}:${devServerOptions.port}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DevServerConfig} devServerOptions
|
||||
* @param {console} logger
|
||||
* @param {string} openPath
|
||||
*/
|
||||
export function logNetworkAddress(devServerOptions, logger, openPath) {
|
||||
try {
|
||||
const address = ip.address();
|
||||
if (typeof address === 'string') {
|
||||
logger.log(
|
||||
`${white(' 🌐 Network:')} ${cyan(createAddress(devServerOptions, address, openPath))}`,
|
||||
);
|
||||
}
|
||||
} catch (_a) {
|
||||
//
|
||||
}
|
||||
}
|
||||
33
packages/cli/src/preview/logPreviewMessage.js
Normal file
33
packages/cli/src/preview/logPreviewMessage.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { white, bold, cyan, gray } from 'colorette';
|
||||
import { createAddress, logNetworkAddress } from '../helpers/infoMessages.js';
|
||||
|
||||
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
|
||||
|
||||
/**
|
||||
* @param {{ devServerOptions: DevServerConfig}} options
|
||||
* @param {console} logger
|
||||
*/
|
||||
export function logPreviewMessage({ devServerOptions }, logger) {
|
||||
const prettyHost = devServerOptions.hostname ?? 'localhost';
|
||||
let openPath = typeof devServerOptions.open === 'string' ? devServerOptions.open : '/';
|
||||
if (!openPath.startsWith('/')) {
|
||||
openPath = `/${openPath}`;
|
||||
}
|
||||
|
||||
logger.log(`${bold(`👀 Previewing Production Build`)}`);
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${white(' 🚧 Local:')} ${cyan(createAddress(devServerOptions, prettyHost, openPath))}`,
|
||||
);
|
||||
logNetworkAddress(devServerOptions, logger, openPath);
|
||||
const sourceDir = devServerOptions.rootDir;
|
||||
if (sourceDir) {
|
||||
logger.log(`${white(' 📝 Source:')} ${cyan(sourceDir)}`);
|
||||
}
|
||||
logger.log('');
|
||||
logger.log(
|
||||
gray(
|
||||
'If what you see works as expected then you can upload "source" to your production web server.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,8 @@
|
||||
import ip from 'ip';
|
||||
import { white, bold, cyan, gray } from 'colorette';
|
||||
import { createAddress, logNetworkAddress } from '../helpers/infoMessages.js';
|
||||
|
||||
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DevServerConfig} devServerOptions
|
||||
* @param {string} host
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function createAddress(devServerOptions, host, path) {
|
||||
return `http${devServerOptions.http2 ? 's' : ''}://${host}:${devServerOptions.port}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DevServerConfig} devServerOptions
|
||||
* @param {console} logger
|
||||
* @param {string} openPath
|
||||
*/
|
||||
function logNetworkAddress(devServerOptions, logger, openPath) {
|
||||
try {
|
||||
const address = ip.address();
|
||||
if (typeof address === 'string') {
|
||||
logger.log(
|
||||
`${white(' 🌐 Network:')} ${cyan(createAddress(devServerOptions, address, openPath))}`,
|
||||
);
|
||||
}
|
||||
} catch (_a) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ devServerOptions: DevServerConfig, engine: import('@rocket/engine/server').Engine}} options
|
||||
* @param {console} logger
|
||||
|
||||
49
packages/cli/test-node/06-preview.test.js
Normal file
49
packages/cli/test-node/06-preview.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import chai from 'chai';
|
||||
import { white, bold, gray } from 'colorette';
|
||||
|
||||
import { setupTestCli } from './test-helpers.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('Preview', () => {
|
||||
it('01: Preview Message', async () => {
|
||||
const { cli, capturedLogs, cleanup } = await setupTestCli({
|
||||
cwd: 'fixtures/06-preview/01-preview-message',
|
||||
cliOptions: ['preview'],
|
||||
testOptions: { captureLogs: true },
|
||||
});
|
||||
|
||||
await cli.start();
|
||||
await cleanup();
|
||||
|
||||
expect(capturedLogs[0]).to.equal(`${bold(`👀 Previewing Production Build`)}`);
|
||||
expect(capturedLogs[1]).to.equal('');
|
||||
expect(capturedLogs[2].startsWith(`${white(' 🚧 Local:')}`)).to.be.true;
|
||||
expect(capturedLogs[3].startsWith(`${white(' 🌐 Network:')}`)).to.be.true;
|
||||
expect(capturedLogs[4].startsWith(`${white(' 📝 Source:')}`)).to.be.true;
|
||||
expect(capturedLogs[5]).to.equal('');
|
||||
expect(capturedLogs[6]).to.equal(
|
||||
`${gray(
|
||||
'If what you see works as expected then you can upload "source" to your production web server.',
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('02: Error Message if there is no build output', async () => {
|
||||
const { cli, capturedLogs, cleanup } = await setupTestCli({
|
||||
cwd: 'fixtures/06-preview/02-error-no-build',
|
||||
cliOptions: ['preview'],
|
||||
testOptions: { captureLogs: true },
|
||||
});
|
||||
|
||||
await cli.start();
|
||||
await cleanup();
|
||||
|
||||
expect(capturedLogs[0]).to.equal(`${bold(`👀 Previewing Production Build`)}`);
|
||||
expect(capturedLogs[1]).to.equal('');
|
||||
expect(capturedLogs[2].startsWith(` 🛑 No index.html found in the build directory`)).to.be
|
||||
.true;
|
||||
expect(capturedLogs[3]).to.equal(' 🤔 Did you forget to run `rocket build` before?');
|
||||
expect(capturedLogs[4]).to.equal('');
|
||||
});
|
||||
});
|
||||
1
packages/cli/test-node/fixtures/06-preview/01-preview-message/.gitignore
vendored
Normal file
1
packages/cli/test-node/fixtures/06-preview/01-preview-message/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!__output
|
||||
@@ -0,0 +1 @@
|
||||
so preview does not stop
|
||||
Reference in New Issue
Block a user