Skip to content

Commit a48dcd8

Browse files
committed
feat(cli): introduce "rocket lint"
1 parent 0ed3d6d commit a48dcd8

File tree

12 files changed

+380
-248
lines changed

12 files changed

+380
-248
lines changed

.changeset/bright-emus-design.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@rocket/cli': patch
3+
---
4+
5+
Introducing `rocket lint` to verify if all your links are correct.
6+
7+
There are two modes:
8+
9+
```bash
10+
# check existing production build in _site (need to execute "rocket build" before)
11+
rocket lint
12+
13+
# run a fast html only build and then check it
14+
rocket lint --build-html
15+
```

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"dependencies": {
5555
"@rocket/building-rollup": "^0.4.0",
5656
"@rocket/engine": "^0.2.6",
57-
"@web/rollup-plugin-copy": "^0.3.0",
57+
"check-html-links": "^0.2.3",
5858
"colorette": "^2.0.16",
5959
"commander": "^9.0.0",
6060
"fs-extra": "^9.0.1",

packages/cli/src/RocketBuild.js

Lines changed: 24 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,18 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
2-
// @ts-nocheck
3-
4-
import { Engine } from '@rocket/engine/server';
5-
import { gatherFiles } from '@rocket/engine';
6-
7-
import { fromRollup } from '@web/dev-server-rollup';
8-
9-
import { rollup } from 'rollup';
102
import path from 'path';
11-
import { rollupPluginHTML } from '@web/rollup-plugin-html';
123

13-
import { createMpaConfig, createServiceWorkerConfig } from '@rocket/building-rollup';
14-
import { adjustPluginOptions } from 'plugins-manager';
154
import { existsSync } from 'fs';
16-
import { readFile, unlink, writeFile } from 'fs/promises';
17-
18-
import puppeteer from 'puppeteer';
19-
20-
/**
21-
* @param {object} config
22-
*/
23-
async function buildAndWrite(config) {
24-
const bundle = await rollup(config);
25-
26-
if (Array.isArray(config.output)) {
27-
await bundle.write(config.output[0]);
28-
await bundle.write(config.output[1]);
29-
} else {
30-
await bundle.write(config.output);
31-
}
32-
}
33-
34-
async function productionBuild(config) {
35-
const defaultSetupPlugins = [];
36-
if (config.pathPrefix) {
37-
defaultSetupPlugins.push(
38-
adjustPluginOptions(rollupPluginHTML, { absolutePathPrefix: config.pathPrefix }),
39-
);
40-
}
41-
42-
const mpaConfig = createMpaConfig({
43-
input: '**/*.html',
44-
output: {
45-
dir: config.outputDir,
46-
},
47-
// custom
48-
rootDir: path.resolve(config.outputDevDir),
49-
absoluteBaseUrl: config.absoluteBaseUrl,
50-
setupPlugins: [
51-
...defaultSetupPlugins,
52-
...config.setupDevServerAndBuildPlugins,
53-
...config.setupBuildPlugins,
54-
],
55-
});
56-
const finalConfig =
57-
typeof config.adjustBuildOptions === 'function'
58-
? config.adjustBuildOptions(mpaConfig)
59-
: mpaConfig;
60-
await buildAndWrite(finalConfig);
61-
62-
const { serviceWorkerSourcePath } = config;
63-
if (existsSync(serviceWorkerSourcePath)) {
64-
const serviceWorkerConfig = createServiceWorkerConfig({
65-
input: serviceWorkerSourcePath,
66-
output: {
67-
file: path.join(path.resolve(config.outputDir), config.serviceWorkerName),
68-
},
69-
});
5+
import { readFile, writeFile } from 'fs/promises';
706

71-
await buildAndWrite(serviceWorkerConfig);
72-
}
73-
}
7+
import { buildHtml } from './build/buildHtml.js';
8+
import { buildOpenGraphImages } from './build/buildOpenGraphImages.js';
9+
import { buildJavaScriptOptimizedOutput } from './build/buildJavaScriptOptimizedOutput.js';
7410

7511
export class RocketBuild {
12+
/**
13+
* @param {import('commander').Command} program
14+
* @param {import('./RocketCli.js').RocketCli} cli
15+
*/
7616
async setupCommand(program, cli) {
7717
this.cli = cli;
7818

@@ -87,32 +27,30 @@ export class RocketBuild {
8727
}
8828

8929
async build() {
30+
if (!this.cli) {
31+
return;
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 (typeof this.cli.options.outputDir !== 'string') {
36+
return;
37+
}
38+
9039
await this.cli.events.dispatchEventDone('build-start');
91-
await this.cli.clearOutputDir();
92-
await this.cli.clearOutputDevDir();
9340

94-
this.engine = new Engine();
95-
this.engine.setOptions({
96-
docsDir: this.cli.options.inputDir,
97-
outputDir: this.cli.options.outputDevDir,
98-
setupPlugins: this.cli.options.setupEnginePlugins,
99-
longFileHeaderWidth: this.cli.options.longFileHeaderWidth,
100-
longFileHeaderComment: this.cli.options.longFileHeaderComment,
101-
renderMode: 'production',
102-
clearOutputDir: this.cli.options.clearOutputDir,
103-
});
104-
console.log('Engine building...');
105-
await this.engine.build({ autoStop: this.cli.options.buildAutoStop });
41+
// 1. build html
42+
this.engine = await buildHtml(this.cli);
10643

44+
// 2. build open graph images
10745
if (this.cli.options.buildOpenGraphImages) {
10846
console.log('Generating Open Graph Images...');
109-
await this.buildOpenGraphImages();
47+
await buildOpenGraphImages(this.cli);
11048
}
11149

112-
if (this.cli.options.buildOptimize) {
50+
// 3. build optimized output
51+
if (this.cli.options.buildOptimize && this.engine) {
11352
console.log('Optimize Production Build...');
114-
await productionBuild(this.cli.options);
115-
await this.engine.copyPublicFilesTo(this.cli.options.outputDir);
53+
await buildJavaScriptOptimizedOutput(this.cli, this.engine);
11654
}
11755

11856
// hackfix 404.html by making all asset urls absolute (rollup always makes them relative) which will break if netlify serves the content form a different url
@@ -130,87 +68,4 @@ export class RocketBuild {
13068

13169
await this.cli.events.dispatchEventDone('build-end');
13270
}
133-
134-
async buildOpenGraphImages() {
135-
const openGraphFiles = await gatherFiles(this.cli.options.outputDevDir, {
136-
fileEndings: ['.opengraph.html'],
137-
});
138-
if (openGraphFiles.length === 0) {
139-
return;
140-
}
141-
142-
// TODO: enable URL support in the Engine and remove this "workaround"
143-
if (
144-
typeof this.cli.options.inputDir !== 'string' ||
145-
typeof this.cli.options.outputDevDir !== 'string'
146-
) {
147-
return;
148-
}
149-
150-
const withWrap = this.cli.options.setupDevServerAndBuildPlugins
151-
? this.cli.options.setupDevServerAndBuildPlugins.map(modFunction => {
152-
modFunction.wrapPlugin = fromRollup;
153-
return modFunction;
154-
})
155-
: [];
156-
157-
this.engine = new Engine();
158-
this.engine.setOptions({
159-
docsDir: this.cli.options.inputDir,
160-
outputDir: this.cli.options.outputDevDir,
161-
setupPlugins: this.cli.options.setupEnginePlugins,
162-
open: false,
163-
clearOutputDir: false,
164-
adjustDevServerOptions: this.cli.options.adjustDevServerOptions,
165-
setupDevServerMiddleware: this.cli.options.setupDevServerMiddleware,
166-
setupDevServerPlugins: [...this.cli.options.setupDevServerPlugins, ...withWrap],
167-
});
168-
try {
169-
await this.engine.start();
170-
171-
const browser = await puppeteer.launch();
172-
const page = await browser.newPage();
173-
174-
// In 2022 Twitter & Facebook recommend a size of 1200x628 - we capture with 2 dpr for retina displays
175-
await page.setViewport({
176-
width: 1200,
177-
height: 628,
178-
deviceScaleFactor: 2,
179-
});
180-
181-
for (const openGraphFile of openGraphFiles) {
182-
const relUrl = path.relative(this.cli.options.outputDevDir, openGraphFile);
183-
const imagePath = openGraphFile.replace('.opengraph.html', '.opengraph.png');
184-
const htmlPath = openGraphFile.replace('.opengraph.html', '.html');
185-
const relImageUrl = path.basename(imagePath);
186-
187-
let htmlString = await readFile(htmlPath, 'utf8');
188-
if (!htmlString.includes('<meta property="og:image"')) {
189-
if (htmlString.includes('</head>')) {
190-
htmlString = htmlString.replace(
191-
'</head>',
192-
[
193-
' <meta property="og:image:width" content="2400">',
194-
' <meta property="og:image:height" content="1256">',
195-
` <meta property="og:image" content="./${relImageUrl}">`,
196-
' </head>',
197-
].join('\n'),
198-
);
199-
}
200-
}
201-
const url = `http://localhost:${this.engine.devServer.config.port}/${relUrl}`;
202-
await page.goto(url, { waitUntil: 'networkidle0' });
203-
await page.screenshot({ path: imagePath });
204-
205-
await unlink(openGraphFile);
206-
await writeFile(htmlPath, htmlString);
207-
}
208-
await browser.close();
209-
210-
await this.engine.stop();
211-
} catch (e) {
212-
console.log('Could not start dev server to generate open graph images');
213-
console.error(e);
214-
}
215-
}
21671
}

packages/cli/src/RocketCli.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Command } from 'commander';
33
import { RocketStart } from './RocketStart.js';
44
import { RocketBuild } from './RocketBuild.js';
5+
import { RocketLint } from './RocketLint.js';
56
import { RocketUpgrade } from './RocketUpgrade.js';
67
import { RocketPreview } from './RocketPreview.js';
78
// import { ignore } from './images/ignore.js';
@@ -53,6 +54,10 @@ export class RocketCli {
5354
absoluteBaseUrl: '',
5455
clearOutputDir: true,
5556

57+
lint: {
58+
buildHtml: false,
59+
},
60+
5661
// /** @type {{[key: string]: ImagePreset}} */
5762
// imagePresets: {
5863
// responsive: {
@@ -179,7 +184,7 @@ export class RocketCli {
179184
let pluginsMeta = [
180185
{ plugin: RocketStart, options: {} },
181186
{ plugin: RocketBuild, options: {} },
182-
// { plugin: RocketLint },
187+
{ plugin: RocketLint, options: {} },
183188
{ plugin: RocketUpgrade, options: {} },
184189
{ plugin: RocketPreview, options: {} },
185190
];

0 commit comments

Comments
 (0)