Skip to content

Commit cbfb0f9

Browse files
committed
feat(cli): add "rocket preview" command to verify a production build
1 parent 5869214 commit cbfb0f9

13 files changed

Lines changed: 212 additions & 33 deletions

File tree

.changeset/nasty-suns-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket/cli': patch
3+
---
4+
5+
Add `rocket preview` command to enable fast checking of the production build

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"setup:ts-configs": "node scripts/generate-ts-configs.mjs",
3636
"start:experimental": "NODE_DEBUG=engine:rendering node --no-warnings --experimental-loader ./packages/engine/src/litCssLoader.js packages/cli/src/cli.js start --open",
3737
"start": "NODE_DEBUG=engine:rendering node --trace-warnings packages/cli/src/cli.js start --open",
38+
"preview": "node packages/cli/src/cli.js preview --open",
3839
"test": "yarn test:node && yarn test:web",
3940
"test:integration": "playwright test packages/*/test-node/*.spec.js --retries=3",
4041
"test:node": "yarn test:unit && yarn test:integration",

packages/cli/src/RocketBuild.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class RocketBuild {
8888

8989
async build() {
9090
await this.cli.events.dispatchEventDone('build-start');
91+
await this.cli.clearOutputDirs();
9192

9293
this.engine = new Engine();
9394
this.engine.setOptions({

packages/cli/src/RocketCli.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RocketStart } from './RocketStart.js';
44
import { RocketBuild } from './RocketBuild.js';
55
import { RocketInit } from './RocketInit.js';
66
import { RocketUpgrade } from './RocketUpgrade.js';
7+
import { RocketPreview } from './RocketPreview.js';
78
// import { ignore } from './images/ignore.js';
89

910
import path from 'path';
@@ -34,7 +35,7 @@ export class RocketCli {
3435
open: false,
3536
cwd: process.cwd(),
3637
inputDir: 'FALLBACK',
37-
outputDir: '_site',
38+
outputDir: 'FALLBACK',
3839
outputDevDir: '_site-dev',
3940

4041
serviceWorkerSourcePath: '',
@@ -93,6 +94,9 @@ export class RocketCli {
9394
if (this.options.inputDir === 'FALLBACK') {
9495
this.options.inputDir = path.join(this.options.cwd, 'site', 'pages');
9596
}
97+
if (this.options.outputDir === 'FALLBACK') {
98+
this.options.outputDir = path.join(this.options.cwd, '_site');
99+
}
96100
if (this.options.inputDir instanceof URL) {
97101
this.options.inputDir = this.options.inputDir.pathname;
98102
}
@@ -122,7 +126,6 @@ export class RocketCli {
122126
}
123127

124128
async prepare() {
125-
await this.clearOutputDirs();
126129
if (!this.options.presets) {
127130
return;
128131
}
@@ -180,6 +183,7 @@ export class RocketCli {
180183
{ plugin: RocketInit, options: {} },
181184
// { plugin: RocketLint },
182185
{ plugin: RocketUpgrade, options: {} },
186+
{ plugin: RocketPreview, options: {} },
183187
];
184188

185189
if (Array.isArray(this.options.setupCliPlugins)) {

packages/cli/src/RocketPreview.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { logPreviewMessage } from './preview/logPreviewMessage.js';
2+
import { startDevServer } from '@web/dev-server';
3+
import path from 'path';
4+
import { existsSync } from 'fs';
5+
import { gray, bold } from 'colorette';
6+
7+
export class RocketPreview {
8+
/**
9+
* @param {import('commander').Command} program
10+
* @param {import('./RocketCli.js').RocketCli} cli
11+
*/
12+
async setupCommand(program, cli) {
13+
this.cli = cli;
14+
this.active = true;
15+
16+
program
17+
.command('preview')
18+
.option('-i, --input-dir <path>', 'path to the folder with the build html files')
19+
.option('-o, --open', 'automatically open the browser')
20+
.action(async cliOptions => {
21+
cli.setOptions(cliOptions);
22+
cli.activePlugin = this;
23+
24+
await this.preview();
25+
});
26+
}
27+
28+
async preview() {
29+
if (!this.cli) {
30+
return;
31+
}
32+
33+
// for typescript as `this.cli.options.outputDir` supports other inputs as well
34+
// but the cli will normalize it to a string before calling plugins
35+
if (
36+
typeof this.cli.options.inputDir !== 'string' ||
37+
typeof this.cli.options.outputDir !== 'string'
38+
) {
39+
return;
40+
}
41+
42+
const rootIndexHtml = path.join(this.cli.options.outputDir, 'index.html');
43+
if (!existsSync(rootIndexHtml)) {
44+
console.log(`${bold(`👀 Previewing Production Build`)}`);
45+
console.log('');
46+
console.log(` 🛑 No index.html found in the build directory ${gray(`${rootIndexHtml}`)}`);
47+
console.log(' 🤔 Did you forget to run `rocket build` before?');
48+
console.log('');
49+
return;
50+
}
51+
52+
/** @type {import('@web/dev-server').DevServerConfig} */
53+
const config = {
54+
open: this.cli.options.open,
55+
rootDir: this.cli.options.outputDir,
56+
clearTerminalOnReload: false,
57+
};
58+
59+
try {
60+
this.devServer = await startDevServer({
61+
config,
62+
logStartMessage: false,
63+
readCliArgs: false,
64+
readFileConfig: false,
65+
// argv: this.__argv,
66+
});
67+
logPreviewMessage({ devServerOptions: this.devServer.config }, console);
68+
} catch (e) {
69+
console.log('🛑 Starting preview server failed');
70+
console.error(e);
71+
}
72+
}
73+
74+
async stop() {
75+
if (this.devServer) {
76+
await this.devServer.stop();
77+
}
78+
}
79+
}

packages/cli/src/RocketStart.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class RocketStart {
3131
if (!this.cli) {
3232
return;
3333
}
34+
await this.cli.clearOutputDirs();
3435

3536
// TODO: enable URL support in the Engine and remove this "workaround"
3637
if (
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import ip from 'ip';
2+
import { white, cyan } from 'colorette';
3+
4+
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
5+
6+
/**
7+
*
8+
* @param {DevServerConfig} devServerOptions
9+
* @param {string} host
10+
* @param {string} path
11+
* @returns {string}
12+
*/
13+
export function createAddress(devServerOptions, host, path) {
14+
return `http${devServerOptions.http2 ? 's' : ''}://${host}:${devServerOptions.port}${path}`;
15+
}
16+
17+
/**
18+
*
19+
* @param {DevServerConfig} devServerOptions
20+
* @param {console} logger
21+
* @param {string} openPath
22+
*/
23+
export function logNetworkAddress(devServerOptions, logger, openPath) {
24+
try {
25+
const address = ip.address();
26+
if (typeof address === 'string') {
27+
logger.log(
28+
`${white(' 🌐 Network:')} ${cyan(createAddress(devServerOptions, address, openPath))}`,
29+
);
30+
}
31+
} catch (_a) {
32+
//
33+
}
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { white, bold, cyan, gray } from 'colorette';
2+
import { createAddress, logNetworkAddress } from '../helpers/infoMessages.js';
3+
4+
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
5+
6+
/**
7+
* @param {{ devServerOptions: DevServerConfig}} options
8+
* @param {console} logger
9+
*/
10+
export function logPreviewMessage({ devServerOptions }, logger) {
11+
const prettyHost = devServerOptions.hostname ?? 'localhost';
12+
let openPath = typeof devServerOptions.open === 'string' ? devServerOptions.open : '/';
13+
if (!openPath.startsWith('/')) {
14+
openPath = `/${openPath}`;
15+
}
16+
17+
logger.log(`${bold(`👀 Previewing Production Build`)}`);
18+
logger.log('');
19+
logger.log(
20+
`${white(' 🚧 Local:')} ${cyan(createAddress(devServerOptions, prettyHost, openPath))}`,
21+
);
22+
logNetworkAddress(devServerOptions, logger, openPath);
23+
const sourceDir = devServerOptions.rootDir;
24+
if (sourceDir) {
25+
logger.log(`${white(' 📝 Source:')} ${cyan(sourceDir)}`);
26+
}
27+
logger.log('');
28+
logger.log(
29+
gray(
30+
'If what you see works as expected then you can upload "source" to your production web server.',
31+
),
32+
);
33+
}

packages/cli/src/start/logStartMessage.js

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,8 @@
1-
import ip from 'ip';
21
import { white, bold, cyan, gray } from 'colorette';
2+
import { createAddress, logNetworkAddress } from '../helpers/infoMessages.js';
33

44
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
55

6-
/**
7-
*
8-
* @param {DevServerConfig} devServerOptions
9-
* @param {string} host
10-
* @param {string} path
11-
* @returns {string}
12-
*/
13-
function createAddress(devServerOptions, host, path) {
14-
return `http${devServerOptions.http2 ? 's' : ''}://${host}:${devServerOptions.port}${path}`;
15-
}
16-
17-
/**
18-
*
19-
* @param {DevServerConfig} devServerOptions
20-
* @param {console} logger
21-
* @param {string} openPath
22-
*/
23-
function logNetworkAddress(devServerOptions, logger, openPath) {
24-
try {
25-
const address = ip.address();
26-
if (typeof address === 'string') {
27-
logger.log(
28-
`${white(' 🌐 Network:')} ${cyan(createAddress(devServerOptions, address, openPath))}`,
29-
);
30-
}
31-
} catch (_a) {
32-
//
33-
}
34-
}
35-
366
/**
377
* @param {{ devServerOptions: DevServerConfig, engine: import('@rocket/engine/server').Engine}} options
388
* @param {console} logger
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import chai from 'chai';
2+
import { white, bold, gray } from 'colorette';
3+
4+
import { setupTestCli } from './test-helpers.js';
5+
6+
const { expect } = chai;
7+
8+
describe('Preview', () => {
9+
it('01: Preview Message', async () => {
10+
const { cli, capturedLogs, cleanup } = await setupTestCli({
11+
cwd: 'fixtures/06-preview/01-preview-message',
12+
cliOptions: ['preview'],
13+
testOptions: { captureLogs: true },
14+
});
15+
16+
await cli.start();
17+
await cleanup();
18+
19+
expect(capturedLogs[0]).to.equal(`${bold(`👀 Previewing Production Build`)}`);
20+
expect(capturedLogs[1]).to.equal('');
21+
expect(capturedLogs[2].startsWith(`${white(' 🚧 Local:')}`)).to.be.true;
22+
expect(capturedLogs[3].startsWith(`${white(' 🌐 Network:')}`)).to.be.true;
23+
expect(capturedLogs[4].startsWith(`${white(' 📝 Source:')}`)).to.be.true;
24+
expect(capturedLogs[5]).to.equal('');
25+
expect(capturedLogs[6]).to.equal(
26+
`${gray(
27+
'If what you see works as expected then you can upload "source" to your production web server.',
28+
)}`,
29+
);
30+
});
31+
32+
it('02: Error Message if there is no build output', async () => {
33+
const { cli, capturedLogs, cleanup } = await setupTestCli({
34+
cwd: 'fixtures/06-preview/02-error-no-build',
35+
cliOptions: ['preview'],
36+
testOptions: { captureLogs: true },
37+
});
38+
39+
await cli.start();
40+
await cleanup();
41+
42+
expect(capturedLogs[0]).to.equal(`${bold(`👀 Previewing Production Build`)}`);
43+
expect(capturedLogs[1]).to.equal('');
44+
expect(capturedLogs[2].startsWith(` 🛑 No index.html found in the build directory`)).to.be
45+
.true;
46+
expect(capturedLogs[3]).to.equal(' 🤔 Did you forget to run `rocket build` before?');
47+
expect(capturedLogs[4]).to.equal('');
48+
});
49+
});

0 commit comments

Comments
 (0)