Compare commits

...

20 Commits

Author SHA1 Message Date
github-actions[bot]
5f4a86b1a8 Version Packages 2022-10-06 10:05:15 +02:00
Thomas Allmer
79e6f0df33 Create tame-tigers-marry.md 2022-10-06 09:32:19 +02:00
Matsu
04621c3f16 Update the error message on degit action 2022-10-06 09:32:19 +02:00
Nathan Brown
1cd9508384 docs: site cleanup
Remove extraneous words and correct misphrasings.
2022-09-10 10:34:13 +02:00
github-actions[bot]
c8082fbac8 Version Packages 2022-08-24 11:37:04 +02:00
Thomas Allmer
68e05f4d4a feat(check-html-links): handle as internal if starts with a provided absolute base url 2022-08-24 11:33:10 +02:00
Thomas Allmer
660f64c320 fix(launch): change the default git branch to main 2022-08-24 11:33:10 +02:00
Jorge del Casar
97d5fb2040 feat(check-html-links): add external link validation 2022-08-24 11:33:10 +02:00
github-actions[bot]
0ca2bc6205 Version Packages 2022-08-23 11:52:54 +02:00
Thomas Allmer
e53e0ebd6d chore: beta announcement tweets/blog 2022-08-23 11:30:32 +02:00
Thomas Allmer
87c10ec1d3 feat(launch): work without JS if Declarative Shadow Dom is supported 2022-08-22 01:16:11 +02:00
Thomas Allmer
d7e461ca31 feat(launch): replace option logoSrc, logoAlt with logoSmall TemplateResult 2022-08-22 00:14:22 +02:00
Thomas Allmer
a12adf2cb5 fix(launch): add padding above slogan on homepage 2022-08-21 22:42:59 +02:00
Thomas Allmer
acf84416dc chore(examples): add --open to preview & run build 2022-08-21 22:41:11 +02:00
Thomas Allmer
a48dcd849b feat(cli): introduce "rocket lint" 2022-08-21 20:16:28 +02:00
Thomas Allmer
0ed3d6d0e9 fix(engine): support fragments when adjusting urls 2022-08-21 11:04:05 +02:00
Thomas Allmer
57ec19fecc chore(cli): remove "rocket init" command 2022-08-20 20:50:12 +02:00
github-actions[bot]
8b48fb9760 Version Packages 2022-08-20 20:25:47 +02:00
Thomas Allmer
39206a1738 feat(cli): start writes to _site-dev and only clears that folder 2022-08-20 20:23:22 +02:00
Thomas Allmer
cbfb0f91e2 feat(cli): add "rocket preview" command to verify a production build 2022-08-20 20:23:22 +02:00
110 changed files with 1718 additions and 1028 deletions

View File

@@ -7,7 +7,7 @@
"scripts": {
"build": "rocket build",
"dev": "npm start",
"preview": "rocket preview",
"preview": "rocket preview --open",
"start": "NODE_DEBUG=engine:rendering rocket start --open"
},
"devDependencies": {

View File

@@ -12,7 +12,7 @@
"scripts": {
"build": "rocket build",
"dev": "npm start",
"preview": "rocket preview",
"preview": "rocket preview --open",
"start": "NODE_DEBUG=engine:rendering rocket start --open"
},
"devDependencies": {

View File

@@ -1,8 +1,8 @@
{
"title": "Example Blog",
"h1": "\n \n ",
"h1": "My Blog",
"name": "Example Blog",
"menuLinkText": "\n \n ",
"menuLinkText": "My Blog",
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
@@ -20,7 +20,7 @@
"children": [
{
"title": "",
"h1": "\n \n ",
"h1": "My Blog",
"headlinesWithId": [
{
"text": "About",
@@ -28,8 +28,8 @@
"level": 1
}
],
"name": "\n \n ",
"menuLinkText": "\n \n ",
"name": "My Blog",
"menuLinkText": "My Blog",
"url": "/about/",
"outputRelativeFilePath": "about/index.html",
"sourceRelativeFilePath": "about.rocket.md",
@@ -60,9 +60,9 @@
"children": [
{
"title": "Hello world!",
"h1": "\n \n ",
"h1": "My Blog",
"name": "Hello world!",
"menuLinkText": "\n \n ",
"menuLinkText": "My Blog",
"url": "/blog/hello-world/",
"outputRelativeFilePath": "blog/hello-world/index.html",
"sourceRelativeFilePath": "blog/hello-world.rocket.md",
@@ -82,9 +82,9 @@
},
{
"title": "With Image!",
"h1": "\n \n ",
"h1": "My Blog",
"name": "With Image!",
"menuLinkText": "\n \n ",
"menuLinkText": "My Blog",
"url": "/blog/with-image/",
"outputRelativeFilePath": "blog/with-image/index.html",
"sourceRelativeFilePath": "blog/with-image.rocket.md",

View File

@@ -7,7 +7,7 @@
"scripts": {
"build": "rocket build",
"dev": "npm start",
"preview": "rocket preview",
"preview": "rocket preview --open",
"start": "NODE_DEBUG=engine:rendering rocket start --open"
},
"devDependencies": {

View File

@@ -7,7 +7,7 @@
"scripts": {
"build": "rocket build",
"dev": "rocket start --open",
"preview": "rocket preview",
"preview": "rocket preview --open",
"start": "NODE_DEBUG=engine:rendering rocket start --open"
},
"devDependencies": {

View File

@@ -7,7 +7,7 @@
"scripts": {
"build": "rocket build",
"dev": "npm start",
"preview": "rocket preview",
"preview": "rocket preview --open",
"start": "NODE_DEBUG=engine:rendering rocket start --open"
},
"devDependencies": {

View File

@@ -0,0 +1,24 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = 'aliens.rocket.md';
import { layout, components } from './recursive.data.js';
export { layout, components };
export async function registerCustomElements() {
// server-only components
// prettier-ignore
customElements.define('rocket-content-area', await import('@rocket/components/content-area.js').then(m => m.RocketContentArea));
// prettier-ignore
customElements.define('rocket-header-scroll-menu', await import('@rocket/components/header-scroll-menu.js').then(m => m.RocketHeaderScrollMenu));
// prettier-ignore
customElements.define('rocket-columns', await import('@rocket/components/columns.js').then(m => m.RocketColumns));
}
/* END - Rocket auto generated - do not touch */
export const title = 'Aliens';
```
Vastness is bearable only through love Cambrian explosion a still more glorious dawn awaits Euclid consciousness extraordinary claims require extraordinary evidence. Realm of the galaxies invent the universe culture made in the interiors of collapsing stars Drake Equation with pretty stories for which there's little good evidence. Bits of moving fluff preserve and cherish that pale blue dot shores of the cosmic ocean the ash of stellar alchemy brain is the seed of intelligence courage of our questions.
With pretty stories for which there's little good evidence not a sunrise but a galaxyrise rich in heavy atoms consciousness the sky calls to us rings of Uranus. Shores of the cosmic ocean shores of the cosmic ocean a very small stage in a vast cosmic arena of brilliant syntheses laws of physics muse about? Invent the universe finite but unbounded extraordinary claims require extraordinary evidence made in the interiors of collapsing stars muse about invent the universe.
The ash of stellar alchemy Cambrian explosion how far away kindling the energy hidden in matter cosmic ocean descended from astronomers. The carbon in our apple pies a very small stage in a vast cosmic arena brain is the seed of intelligence the only home we've ever known the carbon in our apple pies the carbon in our apple pies? Corpus callosum muse about citizens of distant epochs finite but unbounded extraordinary claims require extraordinary evidence finite but unbounded and billions upon billions upon billions upon billions upon billions upon billions upon billions.

View File

@@ -0,0 +1,30 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = 'humans.rocket.md';
import { layout, components } from './recursive.data.js';
export { layout, components };
export async function registerCustomElements() {
// server-only components
// prettier-ignore
customElements.define('rocket-content-area', await import('@rocket/components/content-area.js').then(m => m.RocketContentArea));
// prettier-ignore
customElements.define('rocket-header-scroll-menu', await import('@rocket/components/header-scroll-menu.js').then(m => m.RocketHeaderScrollMenu));
// prettier-ignore
customElements.define('rocket-columns', await import('@rocket/components/columns.js').then(m => m.RocketColumns));
}
/* END - Rocket auto generated - do not touch */
export const title = 'Humans';
```
Astonishment dispassionate extraterrestrial observer Drake Equation radio telescope Hypatia of brilliant syntheses. Vastness is bearable only through love Vangelis a mote of dust suspended in a sunbeam rings of Uranus vanquish the impossible rings of Uranus? Emerged into consciousness a billion trillion laws of physics emerged into consciousness bits of moving fluff as a patch of light. Sea of Tranquility something incredible is waiting to be known permanence of the stars something incredible is waiting to be known network of wormholes invent the universe.
Orion's sword the carbon in our apple pies tesseract made in the interiors of collapsing stars take root and flourish Jean-François Champollion. The ash of stellar alchemy rich in heavy atoms tingling of the spine invent the universe shores of the cosmic ocean star stuff harvesting star light. Bits of moving fluff courage of our questions as a patch of light preserve and cherish that pale blue dot citizens of distant epochs concept of the number one.
Two ghostly white figures in coveralls and helmets are softly dancing dispassionate extraterrestrial observer tesseract finite but unbounded the sky calls to us shores of the cosmic ocean? Muse about tingling of the spine stirred by starlight muse about not a sunrise but a galaxyrise venture. Concept of the number one are creatures of the cosmos something incredible is waiting to be known courage of our questions the only home we've ever known rich in heavy atoms.
Across the centuries light years billions upon billions bits of moving fluff permanence of the stars consciousness. Shores of the cosmic ocean realm of the galaxies vastness is bearable only through love Sea of Tranquility network of wormholes are creatures of the cosmos? Extraordinary claims require extraordinary evidence finite but unbounded made in the interiors of collapsing stars network of wormholes finite but unbounded radio telescope. Invent the universe from which we spring with pretty stories for which there's little good evidence Euclid inconspicuous motes of rock and gas something incredible is waiting to be known.
Birth dream of the mind's eye prime number at the edge of forever a billion trillion permanence of the stars? Vanquish the impossible two ghostly white figures in coveralls and helmets are softly dancing vastness is bearable only through love citizens of distant epochs something incredible is waiting to be known hydrogen atoms. Two ghostly white figures in coveralls and helmets are softly dancing shores of the cosmic ocean the ash of stellar alchemy Apollonius of Perga vastness is bearable only through love network of wormholes.
Gathered by gravity Sea of Tranquility galaxies astonishment hearts of the stars venture. Citizens of distant epochs the carbon in our apple pies encyclopaedia galactica birth brain is the seed of intelligence permanence of the stars? A still more glorious dawn awaits concept of the number one a mote of dust suspended in a sunbeam dream of the mind's eye Euclid a very small stage in a vast cosmic arena and billions upon billions upon billions upon billions upon billions upon billions upon billions.

View File

@@ -128,30 +128,32 @@ export const needsLoader = true;
<rocket-columns>
<rocket-card>
<h4 slot="title">No astrophysicist</h4>
<h4 slot="title">Life</h4>
<p>
would deny the possibility of life. I think we're not creative enough to imagine what life
would be like on another planet. Show me a dead alien. Better yet, show me a live one!
As a scientist, I want to go to Mars and back to asteroids and the Moon because I'm a
scientist. But I can tell you, I'm not so naive a scientist to think that the nation might
not have geopolitical reasons for going into space.
</p>
<a slot="cta" class="cta" href="./life.rocket.html">Details</a>
<a slot="cta" class="cta" href="./life.rocket.md">Details</a>
</rocket-card>
<rocket-card>
<h4 slot="title">No astrophysicist</h4>
<h4 slot="title">Aliens</h4>
<p>
would deny the possibility of life. I think we're not creative enough to imagine what life
would be like on another planet. Show me a dead alien. Better yet, show me a live one!
Vastness is bearable only through love Cambrian explosion a still more glorious dawn awaits
Euclid consciousness extraordinary claims require extraordinary evidence.
</p>
<a slot="cta" class="cta" href="./life.rocket.html">Details</a>
<a slot="cta" class="cta" href="./aliens.rocket.md">Details</a>
</rocket-card>
<rocket-card>
<h4 slot="title">No astrophysicist</h4>
<h4 slot="title">Humans</h4>
<p>
would deny the possibility of life. I think we're not creative enough to imagine what life
would be like on another planet. Show me a dead alien. Better yet, show me a live one!
Astonishment dispassionate extraterrestrial observer Drake Equation radio telescope Hypatia
of brilliant syntheses. Vastness is bearable only through love Vangelis a mote of dust
suspended in a sunbeam rings of Uranus vanquish the impossible rings of Uranus?
</p>
<a slot="cta" class="cta" href="./life.rocket.html">Details</a>
<a slot="cta" class="cta" href="./humans.rocket.md">Details</a>
</rocket-card>
</rocket-columns>
</rocket-content-area>

View File

@@ -1,24 +1,24 @@
{
"title": "Spark Rocket Example Template",
"h1": "\n \n \n ",
"h1": "Home",
"headlinesWithId": [
{
"text": "Home",
"id": "home",
"level": 1,
"rawText": "\n \n \n "
"rawText": "@keyframes textAnimIn {\n 0% {\n transform: translate3d(0, -120%, 0);\n }\n\n 100% {\n transform: translate3d(0, 0%, 0);\n }\n }\n\n @keyframes textAnimOut {\n 0% {\n transform: translate3d(0, 0%, 0);\n }\n\n 50% {\n transform: translate3d(0, -20%, 0);\n }\n\n 100% {\n transform: translate3d(0, 120%, 0);\n }\n }\n\n p {\n position: absolute;\n top: 0;\n margin: 0;\n transform: translate3d(0, -120%, 0);\n }\n\n .anim-static {\n transform: translate3d(0, 0, 0);\n }\n\n .anim-in {\n animation: textAnimIn 0.6s 0.3s forwards;\n }\n\n .anim-out {\n animation: textAnimOut 0.6s forwards;\n }\n\n :host {\n position: block;\n position: relative;\n overflow: hidden;\n display: block;\n height: 1.5em;\n }\n div {\n display: flex;\n max-width: 960px;\n margin: 0 auto;\n }\n slot {\n display: block;\n flex: 1;\n }\n \n Welcome\n \n Hello\n \n Aloa\n lit-part-->\n to the world of science."
},
{
"text": "Credit",
"id": "credit",
"level": 2,
"rawText": "\n People credit me for \n \n "
"rawText": "People credit me for \n making the universe interesting,"
},
{
"text": "Knowledge",
"id": "knowledge",
"level": 2,
"rawText": "\n I've known from long ago \n \n "
"rawText": "I've known from long ago \n that the universe was calling me"
},
{
"text": "Sign Up",
@@ -30,7 +30,7 @@
"text": "Say",
"id": "say",
"level": 2,
"rawText": "\n What others \n about us?\n "
"rawText": "What others say \n about us?"
},
{
"text": "FAQ",
@@ -39,7 +39,7 @@
}
],
"name": "Spark Rocket Example Template",
"menuLinkText": "\n \n \n ",
"menuLinkText": "Home",
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.html",
@@ -69,6 +69,70 @@
"description": "Everyone can code a website!",
"needsLoader": true,
"children": [
{
"title": "Aliens",
"h1": "Aliens",
"name": "Aliens",
"menuLinkText": "Aliens",
"url": "/aliens/",
"outputRelativeFilePath": "aliens/index.html",
"sourceRelativeFilePath": "aliens.rocket.md",
"level": 1,
"components": {
"rocket-columns": "@rocket/components/columns.js::RocketColumns",
"rocket-card": "@rocket/components/card.js::RocketCard",
"rocket-content-area": "@rocket/components/content-area.js::RocketContentArea",
"rocket-details": "@rocket/components/details.js::RocketDetails",
"rocket-drawer": "@rocket/components/drawer.js::RocketDrawer",
"rocket-feature-small": "@rocket/components/feature-small.js::RocketFeatureSmall",
"rocket-header": "@rocket/components/header.js::RocketHeader",
"rocket-header-scroll-menu": "@rocket/components/header-scroll-menu.js::RocketHeaderScrollMenu",
"rocket-icon": "@rocket/components/icon.js::RocketIcon",
"rocket-icon-card": "@rocket/components/icon-card.js::RocketIconCard",
"rocket-main": "@rocket/components/main.js::RocketMain",
"rocket-main-docs": "@rocket/components/main-docs.js::RocketMainDocs",
"rocket-opengraph-overview": "@rocket/components/open-graph-overview.js::RocketOpenGraphOverview",
"rocket-rotating-text": "@rocket/components/rotating-text.js::RocketRotatingText",
"rocket-social-link": "@rocket/components/social-link.js::RocketSocialLink",
"rocket-testimonial-small": "@rocket/components/testimonial-small.js::RocketTestimonialSmall",
"inline-notification": "@rocket/components/inline-notification.js::InlineNotification",
"permanent-notification": "@rocket/components/permanent-notification.js::PermanentNotification",
"block-blue": "@rocket/spark/block-blue.js::BlockBlue",
"block-features": "@rocket/spark/block-features.js::BlockFeatures"
}
},
{
"title": "Humans",
"h1": "Humans",
"name": "Humans",
"menuLinkText": "Humans",
"url": "/humans/",
"outputRelativeFilePath": "humans/index.html",
"sourceRelativeFilePath": "humans.rocket.md",
"level": 1,
"components": {
"rocket-columns": "@rocket/components/columns.js::RocketColumns",
"rocket-card": "@rocket/components/card.js::RocketCard",
"rocket-content-area": "@rocket/components/content-area.js::RocketContentArea",
"rocket-details": "@rocket/components/details.js::RocketDetails",
"rocket-drawer": "@rocket/components/drawer.js::RocketDrawer",
"rocket-feature-small": "@rocket/components/feature-small.js::RocketFeatureSmall",
"rocket-header": "@rocket/components/header.js::RocketHeader",
"rocket-header-scroll-menu": "@rocket/components/header-scroll-menu.js::RocketHeaderScrollMenu",
"rocket-icon": "@rocket/components/icon.js::RocketIcon",
"rocket-icon-card": "@rocket/components/icon-card.js::RocketIconCard",
"rocket-main": "@rocket/components/main.js::RocketMain",
"rocket-main-docs": "@rocket/components/main-docs.js::RocketMainDocs",
"rocket-opengraph-overview": "@rocket/components/open-graph-overview.js::RocketOpenGraphOverview",
"rocket-rotating-text": "@rocket/components/rotating-text.js::RocketRotatingText",
"rocket-social-link": "@rocket/components/social-link.js::RocketSocialLink",
"rocket-testimonial-small": "@rocket/components/testimonial-small.js::RocketTestimonialSmall",
"inline-notification": "@rocket/components/inline-notification.js::InlineNotification",
"permanent-notification": "@rocket/components/permanent-notification.js::PermanentNotification",
"block-blue": "@rocket/spark/block-blue.js::BlockBlue",
"block-features": "@rocket/spark/block-features.js::BlockFeatures"
}
},
{
"title": "Life",
"h1": "Life",

View File

@@ -21,7 +21,7 @@ export const layoutData = {
Modern Web<br />
Internet 12<br />
0000 Web<br />
<a href="#">office@modern-web.dev</a><br />
<a href="#">hello@modern-web.dev</a><br />
<a href="#">0000 / 11223344</a>
</div>

View File

@@ -7,7 +7,7 @@
"scripts": {
"build": "rocket build",
"dev": "npm start",
"preview": "rocket preview",
"preview": "rocket preview --open",
"start": "NODE_DEBUG=engine:rendering rocket start --open"
},
"devDependencies": {

View File

@@ -12,6 +12,10 @@ export async function registerCustomElements() {
'rocket-header',
await import('@rocket/components/header.js').then(m => m.RocketHeader),
);
customElements.define(
'launch-blog-preview',
await import('@rocket/launch/blog-preview.js').then(m => m.LaunchBlogPreview),
);
customElements.define(
'launch-blog-overview',
await import('@rocket/launch/blog-overview.js').then(m => m.LaunchBlogOverview),

View File

@@ -1,8 +1,8 @@
{
"title": "Welcome to Rocket",
"h1": "\n \n ",
"name": "\n \n ",
"menuLinkText": "\n \n ",
"h1": "",
"name": "Welcome to Rocket",
"menuLinkText": "Welcome to Rocket",
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",

View File

@@ -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",
@@ -80,7 +81,7 @@
"husky": "^4.3.7",
"lint-staged": "^10.5.3",
"mocha": "^9.1.3",
"node-fetch": "^2.6.7",
"node-fetch": "^3.0.0",
"npm-run-all": "^4.1.5",
"onchange": "^7.1.0",
"prettier": "^2.5.1",

View File

@@ -1,5 +1,21 @@
# check-html-links
## 0.2.4
### Patch Changes
- 97d5fb2: Add external links validation via the flag `--validate-externals`.
You can/should provide an optional `--absolute-base-url` to handle urls starting with it as internal.
```bash
# check external urls
npx check-html-links _site --validate-externals
# check external urls but treat links like https://rocket.modern-web.dev/about/ as internal
npx check-html-links _site --validate-externals --absolute-base-url https://rocket.modern-web.dev
```
## 0.2.3
### Patch Changes

View File

@@ -14,7 +14,7 @@ npm i -D check-html-links
npx check-html-links _site
```
For docs please see our homepage [https://rocket.modern-web.dev/docs/tools/check-html-links/](https://rocket.modern-web.dev/docs/tools/check-html-links/).
For docs please see our homepage [https://rocket.modern-web.dev/tools/check-html-links/overview/](https://rocket.modern-web.dev/tools/check-html-links/overview/).
## Comparison

View File

@@ -1,6 +1,6 @@
{
"name": "check-html-links",
"version": "0.2.3",
"version": "0.2.4",
"publishConfig": {
"access": "public"
},
@@ -37,6 +37,7 @@
"command-line-args": "^5.1.1",
"glob": "^7.0.0",
"minimatch": "^3.0.4",
"node-fetch": "^3.0.0",
"sax-wasm": "^2.0.0",
"slash": "^4.0.0"
},

View File

@@ -6,7 +6,7 @@ import path from 'path';
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import { validateFiles } from './validateFolder.js';
import { prepareFiles, validateFiles } from './validateFolder.js';
import { formatErrors } from './formatErrors.js';
import { listFiles } from './listFiles.js';
@@ -18,7 +18,9 @@ export class CheckHtmlLinksCli {
const mainDefinitions = [
{ name: 'ignore-link-pattern', type: String, multiple: true },
{ name: 'root-dir', type: String, defaultOption: true },
{ name: 'continue-on-error', type: Boolean, defaultOption: false },
{ name: 'continue-on-error', type: Boolean },
{ name: 'validate-externals', type: Boolean },
{ name: 'absolute-base-url', type: String },
];
const options = commandLineArgs(mainDefinitions, {
stopAtFirstUnknown: true,
@@ -29,6 +31,8 @@ export class CheckHtmlLinksCli {
continueOnError: options['continue-on-error'],
rootDir: options['root-dir'],
ignoreLinkPatterns: options['ignore-link-pattern'],
validateExternals: options['validate-externals'],
absoluteBaseUrl: options['absolute-base-url'],
};
}
@@ -43,22 +47,47 @@ export class CheckHtmlLinksCli {
}
async run() {
const { ignoreLinkPatterns, rootDir: userRootDir } = this.options;
const {
ignoreLinkPatterns,
rootDir: userRootDir,
validateExternals,
absoluteBaseUrl,
} = this.options;
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd();
const performanceStart = process.hrtime();
console.log('👀 Checking if all internal links work...');
const files = await listFiles('**/*.html', rootDir);
console.log('Check HTML Links');
const filesOutput =
files.length == 0
? '🧐 No files to check. Did you select the correct folder?'
: `🔥 Found a total of ${chalk.green.bold(files.length)} files to check!`;
? ' 🧐 No files to check. Did you select the correct folder?'
: ` 📖 Found ${chalk.green.bold(files.length)} files containing`;
console.log(filesOutput);
const { errors, numberLinks } = await validateFiles(files, rootDir, { ignoreLinkPatterns });
const { numberLinks, checkLocalFiles, checkExternalLinks } = await prepareFiles(
files,
rootDir,
{
ignoreLinkPatterns,
validateExternals,
absoluteBaseUrl,
},
);
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`);
console.log(` 🔗 ${chalk.green.bold(numberLinks)} internal links`);
if (validateExternals) {
console.log(` 🌐 ${chalk.green.bold(checkExternalLinks.length)} external links`);
}
console.log(' 👀 Checking...');
const { errors } = await validateFiles({
checkLocalFiles,
validateExternals,
checkExternalLinks,
});
const performance = process.hrtime(performanceStart);
/** @type {string[]} */
@@ -70,7 +99,7 @@ export class CheckHtmlLinksCli {
referenceCount += error.usage.length;
}
output = [
`❌ Found ${chalk.red.bold(
` ❌ Found ${chalk.red.bold(
errors.length.toString(),
)} missing reference targets (used by ${referenceCount} links) while checking ${
files.length
@@ -78,7 +107,7 @@ export class CheckHtmlLinksCli {
...formatErrors(errors)
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
` 🕑 Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
];
message = output.join('\n');
if (this.options.printOnError === true) {
@@ -89,7 +118,7 @@ export class CheckHtmlLinksCli {
}
} else {
console.log(
`✅ All internal links are valid. (executed in ${performance[0]}s ${
` ✅ All tested links are valid. (executed in ${performance[0]}s ${
performance[1] / 1000000
}ms)`,
);

View File

@@ -0,0 +1,63 @@
import fetch from 'node-fetch';
/**
* @type {Map<string,boolean>}
*/
const resultsMap = new Map();
/**
*
* @param {string} url
* @param {boolean} result
* @returns {boolean}
*/
const memorizeCheckup = (url, result) => {
resultsMap.set(url, result);
return result;
};
/**
*
* @param {string} url
* @param {string} method
* @returns
*/
const fetchWrap = async (url, method = 'GET') => {
return Promise.race([
fetch(url, { method })
.then(response => response.ok)
.catch(() => false),
new Promise(resolve => setTimeout(resolve, 10000, false)),
]);
};
/**
*
* @param {string} url
* @returns {Promise<boolean>}
*/
const fetchHead = async url => fetchWrap(url, 'HEAD');
/**
*
* @param {string} url - URL object to check
* @returns {Promise<boolean>} true if url is alive or false if not
*/
const checkUrl = async url =>
(fetchHead(url) || fetchWrap(url)).then(result => memorizeCheckup(url, result));
/**
*
* @param {string} link - link string to check
* @returns {Promise<boolean>}
*/
export const checkLink = async link => {
const url = link.startsWith('//') ? `https:${link}` : link;
return resultsMap.get(url) ?? checkUrl(url);
};
/**
* Check an array of links and return an object with
*
* @param {string[]} links Links to check
*/
export const checkLinks = async links => Promise.all(links.map(checkLink));

View File

@@ -15,7 +15,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
const filePath = path.relative(relativeFrom, error.filePath);
if (error.onlyAnchorMissing === true) {
output.push(
`${number}. missing ${chalk.red.bold(
` ${number}. missing ${chalk.red.bold(
`id="${error.usage[0].anchor}"`,
)} in ${chalk.cyanBright(filePath)}`,
);
@@ -24,7 +24,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
const title =
firstAttribute === 'src' || firstAttribute === 'srcset' ? 'file' : 'reference target';
output.push(`${number}. missing ${title} ${chalk.red.bold(filePath)}`);
output.push(` ${number}. missing ${title} ${chalk.red.bold(filePath)}`);
}
const usageLength = error.usage.length;
@@ -34,11 +34,11 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
const clickAbleLink = chalk.cyanBright(`${usagePath}:${usage.line + 1}:${usage.character}`);
const attributeStart = chalk.gray(`${usage.attribute}="`);
const attributeEnd = chalk.gray('"');
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`);
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`);
}
if (usageLength > 3) {
const more = chalk.red((usageLength - 3).toString());
output.push(` ... ${more} more references to this target`);
output.push(` ... ${more} more references to this target`);
}
output.push('');
}

View File

@@ -3,10 +3,10 @@ import fs from 'fs';
import saxWasm from 'sax-wasm';
import minimatch from 'minimatch';
import { createRequire } from 'module';
import { listFiles } from './listFiles.js';
import path from 'path';
import slash from 'slash';
import { listFiles } from './listFiles.js';
import { checkLinks } from './checkLinks.js';
/** @typedef {import('../types/main').Link} Link */
/** @typedef {import('../types/main').LocalFile} LocalFile */
@@ -28,6 +28,9 @@ const parserIds = new SAXParser(SaxEventType.Attribute, streamOptions);
/** @type {Error[]} */
let checkLocalFiles = [];
/** @type {Error[]} */
let checkExternalLinks = [];
/** @type {Error[]} */
let errors = [];
@@ -151,6 +154,26 @@ function addLocalFile(filePath, anchor, usageObj) {
}
}
/**
* @param {string} filePath
* @param {Usage} usageObj
*/
function addExternalLink(filePath, usageObj) {
const foundIndex = checkExternalLinks.findIndex(item => {
return item.filePath === filePath;
});
if (foundIndex === -1) {
checkExternalLinks.push({
filePath,
onlyAnchorMissing: false,
usage: [usageObj],
});
} else {
checkExternalLinks[foundIndex].usage.push(usageObj);
}
}
/**
* @param {string} inValue
*/
@@ -200,11 +223,16 @@ function isNonHttpSchema(url) {
* @param {object} options
* @param {string} options.htmlFilePath
* @param {string} options.rootDir
* @param {string} options.absoluteBaseUrl
* @param {function(string): boolean} options.ignoreUsage
*/
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage, absoluteBaseUrl }) {
for (const hrefObj of links) {
const { value, anchor } = getValueAndAnchor(hrefObj.value);
const { value: rawValue, anchor } = getValueAndAnchor(hrefObj.value);
const value = rawValue.startsWith(absoluteBaseUrl)
? rawValue.substring(absoluteBaseUrl.length)
: rawValue;
const usageObj = {
attribute: hrefObj.attribute,
@@ -229,8 +257,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
} else if (valueFile === '' && anchor !== '') {
addLocalFile(htmlFilePath, anchor, usageObj);
} else if (value.startsWith('//') || value.startsWith('http')) {
// TODO: handle external urls
// external url - we do not handle that (yet)
addExternalLink(htmlFilePath, usageObj);
} else if (value.startsWith('/')) {
const filePath = path.join(rootDir, valueFile);
addLocalFile(filePath, anchor, usageObj);
@@ -244,7 +271,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
}
}
return { checkLocalFiles: [...checkLocalFiles] };
return { checkLocalFiles: [...checkLocalFiles], checkExternalLinks: [...checkExternalLinks] };
}
/**
@@ -283,17 +310,34 @@ async function validateLocalFiles(checkLocalFiles) {
return errors;
}
/**
*
* @param {Error[]} checkExternalLinks
*/
async function validateExternalLinks(checkExternalLinks) {
for await (const localFileObj of checkExternalLinks) {
const links = localFileObj.usage.map(usage => usage.value);
const results = await checkLinks(links);
localFileObj.usage = localFileObj.usage.filter((link, index) => !results[index]);
if (localFileObj.usage.length > 0) {
errors.push(localFileObj);
}
}
return errors;
}
/**
* @param {string[]} files
* @param {string} rootDir
* @param {Options} opts?
*/
export async function validateFiles(files, rootDir, opts) {
export async function prepareFiles(files, rootDir, opts) {
await parserReferences.prepareWasm(saxWasmBuffer);
await parserIds.prepareWasm(saxWasmBuffer);
errors = [];
checkLocalFiles = [];
checkExternalLinks = [];
idCache = new Map();
let numberLinks = 0;
@@ -309,11 +353,27 @@ export async function validateFiles(files, rootDir, opts) {
for (const htmlFilePath of files) {
const { links } = await extractReferences(htmlFilePath);
numberLinks += links.length;
await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage });
await resolveLinks(links, {
htmlFilePath,
rootDir,
ignoreUsage,
absoluteBaseUrl: opts?.absoluteBaseUrl,
});
}
await validateLocalFiles(checkLocalFiles);
return { checkLocalFiles, checkExternalLinks, numberLinks };
}
return { errors: errors, numberLinks: numberLinks };
/**
* @param {*} param0
* @returns
*/
export async function validateFiles({ checkLocalFiles, validateExternals, checkExternalLinks }) {
await validateLocalFiles(checkLocalFiles);
if (validateExternals) {
await validateExternalLinks(checkExternalLinks);
}
return { errors };
}
/**
@@ -323,6 +383,14 @@ export async function validateFiles(files, rootDir, opts) {
export async function validateFolder(inRootDir, opts) {
const rootDir = path.resolve(inRootDir);
const files = await listFiles('**/*.html', rootDir);
const { errors } = await validateFiles(files, rootDir, opts);
const { checkLocalFiles, checkExternalLinks } = await prepareFiles(files, rootDir, opts);
const { errors } = await validateFiles({
checkLocalFiles,
validateExternals: opts?.validateExternals,
checkExternalLinks,
});
return errors;
}

View File

@@ -1,5 +1,9 @@
<!-- ignore known subsystems -->
<a href="/docs/"></a>
<a href="/developer/getting-started.html#js"></a>
<a href="/developer/language-guides/"></a>
<a href="/developer/javascript.html"></a>
<!-- valid -->
<a href="//rocket.modern-web.dev/"></a>
<a href="http://rocket.modern-web.dev/"></a>
<a href="https://rocket.modern-web.dev/"></a>
<!-- invalid -->
<a href="//rocket.modern-web.dev/unexists-page/"></a>
<a href="http://rocket.modern-web.dev/unexists-page/"></a>
<a href="https://rocket.modern-web.dev/unexists-page/"></a>

View File

@@ -5,8 +5,5 @@
<a href="./page.html"></a>
<a href=" ./page.html "></a>
<a href=" /page.html "></a>
<a href="//domain.com/something/"></a>
<a href="http://domain.com/something/"></a>
<a href="https://domain.com/something/"></a>
<a href=""></a>
<a href=":~:text=put%20your%20labels%20above%20your%20inputs">Sign-in form best practices</a>

View File

@@ -0,0 +1,2 @@
<a href="about.html">About</a>
<a href="http://localhost/about.html">About Absolute</a>

View File

@@ -20,19 +20,19 @@ describe('formatErrors', () => {
const result = await executeAndFormat('fixtures/test-case');
expect(result.trim().split('\n')).to.deep.equal([
'1. missing id="my-teams" in fixtures/test-case/price/index.html',
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
'',
'2. missing file fixtures/test-case/about/images/team.png',
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
' 2. missing file fixtures/test-case/about/images/team.png',
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
'',
'3. missing reference target fixtures/test-case/aboot',
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
' ... 2 more references to this target',
' 3. missing reference target fixtures/test-case/aboot',
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
' ... 2 more references to this target',
'',
'4. missing reference target fixtures/test-case/prce',
' from fixtures/test-case/index.html:1:9 via href="./prce"',
' 4. missing reference target fixtures/test-case/prce',
' from fixtures/test-case/index.html:1:9 via href="./prce"',
]);
});
});

View File

@@ -66,6 +66,74 @@ describe('validateFolder', () => {
]);
});
it('validates external links', async () => {
const { errors, cleanup } = await execute('fixtures/external-link', {
validateExternals: true,
});
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/external-link/index.html',
onlyAnchorMissing: false,
usage: [
{
attribute: 'href',
value: '//rocket.modern-web.dev/unexists-page/',
file: 'fixtures/external-link/index.html',
line: 6,
character: 9,
anchor: '',
},
{
attribute: 'href',
value: 'http://rocket.modern-web.dev/unexists-page/',
file: 'fixtures/external-link/index.html',
line: 7,
character: 9,
anchor: '',
},
{
attribute: 'href',
value: 'https://rocket.modern-web.dev/unexists-page/',
file: 'fixtures/external-link/index.html',
line: 8,
character: 9,
anchor: '',
},
],
},
]);
});
it('validates links with own absolute base url as internal', async () => {
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
validateExternals: true,
absoluteBaseUrl: 'http://localhost',
});
expect(cleanup(errors)).to.deep.equal([]);
});
it('validates all full urls if there is no absoluteBaseUrl provided', async () => {
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
validateExternals: true,
});
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-own-absolute-base-path/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
character: 9,
file: 'fixtures/internal-own-absolute-base-path/index.html',
line: 1,
value: 'http://localhost/about.html',
},
],
},
]);
});
it('groups multiple usage of the same missing file', async () => {
const { errors, cleanup } = await execute('fixtures/internal-links-to-same-file');
expect(cleanup(errors)).to.deep.equal([

View File

@@ -26,13 +26,15 @@ export interface Error {
usage: Usage[];
}
interface Options {
export interface Options {
ignoreLinkPatterns: string[] | null;
validateExternals: boolean;
absoluteBaseUrl: string;
}
export interface CheckHtmlLinksCliOptions {
export interface CheckHtmlLinksCliOptions extends Options {
printOnError: boolean;
rootDir: string;
ignoreLinkPatterns: string[] | null;
continueOnError: boolean;
absoluteBaseUrl: string;
}

View File

@@ -1,5 +1,47 @@
# @rocket/cli
## 0.20.4
### Patch Changes
- 68e05f4: `rocket lint` can now validate external links.
Example:
```
rocket lint --validate-externals
```
- Updated dependencies [97d5fb2]
- check-html-links@0.2.4
## 0.20.3
### Patch Changes
- a48dcd8: Introducing `rocket lint` to verify if all your links are correct.
There are two modes:
```bash
# check existing production build in _site (need to execute "rocket build" before)
rocket lint
# run a fast html only build and then check it
rocket lint --build-html
```
- Updated dependencies [0ed3d6d]
- @rocket/engine@0.2.7
## 0.20.2
### Patch Changes
- 39206a1: `rocket start` now outputs to `_site-dev` instead of `_site`.
- 39206a1: `rocket start` clears only its output folder (defaults to `_site-dev`)
- cbfb0f9: Add `rocket preview` command to enable fast checking of the production build
## 0.20.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/cli",
"version": "0.20.1",
"version": "0.20.4",
"publishConfig": {
"access": "public"
},
@@ -53,8 +53,8 @@
],
"dependencies": {
"@rocket/building-rollup": "^0.4.0",
"@rocket/engine": "^0.2.6",
"@web/rollup-plugin-copy": "^0.3.0",
"@rocket/engine": "^0.2.7",
"check-html-links": "^0.2.4",
"colorette": "^2.0.16",
"commander": "^9.0.0",
"fs-extra": "^9.0.1",

View File

@@ -1,78 +1,18 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { Engine } from '@rocket/engine/server';
import { gatherFiles } from '@rocket/engine';
import { fromRollup } from '@web/dev-server-rollup';
import { rollup } from 'rollup';
import path from 'path';
import { rollupPluginHTML } from '@web/rollup-plugin-html';
import { createMpaConfig, createServiceWorkerConfig } from '@rocket/building-rollup';
import { adjustPluginOptions } from 'plugins-manager';
import { existsSync } from 'fs';
import { readFile, unlink, writeFile } from 'fs/promises';
import { readFile, writeFile } from 'fs/promises';
import puppeteer from 'puppeteer';
/**
* @param {object} config
*/
async function buildAndWrite(config) {
const bundle = await rollup(config);
if (Array.isArray(config.output)) {
await bundle.write(config.output[0]);
await bundle.write(config.output[1]);
} else {
await bundle.write(config.output);
}
}
async function productionBuild(config) {
const defaultSetupPlugins = [];
if (config.pathPrefix) {
defaultSetupPlugins.push(
adjustPluginOptions(rollupPluginHTML, { absolutePathPrefix: config.pathPrefix }),
);
}
const mpaConfig = createMpaConfig({
input: '**/*.html',
output: {
dir: config.outputDir,
},
// custom
rootDir: path.resolve(config.outputDevDir),
absoluteBaseUrl: config.absoluteBaseUrl,
setupPlugins: [
...defaultSetupPlugins,
...config.setupDevServerAndBuildPlugins,
...config.setupBuildPlugins,
],
});
const finalConfig =
typeof config.adjustBuildOptions === 'function'
? config.adjustBuildOptions(mpaConfig)
: mpaConfig;
await buildAndWrite(finalConfig);
const { serviceWorkerSourcePath } = config;
if (existsSync(serviceWorkerSourcePath)) {
const serviceWorkerConfig = createServiceWorkerConfig({
input: serviceWorkerSourcePath,
output: {
file: path.join(path.resolve(config.outputDir), config.serviceWorkerName),
},
});
await buildAndWrite(serviceWorkerConfig);
}
}
import { buildHtml } from './build/buildHtml.js';
import { buildOpenGraphImages } from './build/buildOpenGraphImages.js';
import { buildJavaScriptOptimizedOutput } from './build/buildJavaScriptOptimizedOutput.js';
export class RocketBuild {
/**
* @param {import('commander').Command} program
* @param {import('./RocketCli.js').RocketCli} cli
*/
async setupCommand(program, cli) {
this.cli = cli;
@@ -87,30 +27,30 @@ export class RocketBuild {
}
async build() {
await this.cli.events.dispatchEventDone('build-start');
this.engine = new Engine();
this.engine.setOptions({
docsDir: this.cli.options.inputDir,
outputDir: this.cli.options.outputDevDir,
setupPlugins: this.cli.options.setupEnginePlugins,
longFileHeaderWidth: this.cli.options.longFileHeaderWidth,
longFileHeaderComment: this.cli.options.longFileHeaderComment,
renderMode: 'production',
clearOutputDir: this.cli.options.clearOutputDir,
});
console.log('Engine building...');
await this.engine.build({ autoStop: this.cli.options.buildAutoStop });
if (this.cli.options.buildOpenGraphImages) {
console.log('Generating Open Graph Images...');
await this.buildOpenGraphImages();
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.outputDir !== 'string') {
return;
}
if (this.cli.options.buildOptimize) {
await this.cli.events.dispatchEventDone('build-start');
// 1. build html
this.engine = await buildHtml(this.cli);
// 2. build open graph images
if (this.cli.options.buildOpenGraphImages) {
console.log('Generating Open Graph Images...');
await buildOpenGraphImages(this.cli);
}
// 3. build optimized output
if (this.cli.options.buildOptimize && this.engine) {
console.log('Optimize Production Build...');
await productionBuild(this.cli.options);
await this.engine.copyPublicFilesTo(this.cli.options.outputDir);
await buildJavaScriptOptimizedOutput(this.cli, this.engine);
}
// 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
@@ -128,87 +68,4 @@ export class RocketBuild {
await this.cli.events.dispatchEventDone('build-end');
}
async buildOpenGraphImages() {
const openGraphFiles = await gatherFiles(this.cli.options.outputDevDir, {
fileEndings: ['.opengraph.html'],
});
if (openGraphFiles.length === 0) {
return;
}
// TODO: enable URL support in the Engine and remove this "workaround"
if (
typeof this.cli.options.inputDir !== 'string' ||
typeof this.cli.options.outputDevDir !== 'string'
) {
return;
}
const withWrap = this.cli.options.setupDevServerAndBuildPlugins
? this.cli.options.setupDevServerAndBuildPlugins.map(modFunction => {
modFunction.wrapPlugin = fromRollup;
return modFunction;
})
: [];
this.engine = new Engine();
this.engine.setOptions({
docsDir: this.cli.options.inputDir,
outputDir: this.cli.options.outputDevDir,
setupPlugins: this.cli.options.setupEnginePlugins,
open: false,
clearOutputDir: false,
adjustDevServerOptions: this.cli.options.adjustDevServerOptions,
setupDevServerMiddleware: this.cli.options.setupDevServerMiddleware,
setupDevServerPlugins: [...this.cli.options.setupDevServerPlugins, ...withWrap],
});
try {
await this.engine.start();
const browser = await puppeteer.launch();
const page = await browser.newPage();
// In 2022 Twitter & Facebook recommend a size of 1200x628 - we capture with 2 dpr for retina displays
await page.setViewport({
width: 1200,
height: 628,
deviceScaleFactor: 2,
});
for (const openGraphFile of openGraphFiles) {
const relUrl = path.relative(this.cli.options.outputDevDir, openGraphFile);
const imagePath = openGraphFile.replace('.opengraph.html', '.opengraph.png');
const htmlPath = openGraphFile.replace('.opengraph.html', '.html');
const relImageUrl = path.basename(imagePath);
let htmlString = await readFile(htmlPath, 'utf8');
if (!htmlString.includes('<meta property="og:image"')) {
if (htmlString.includes('</head>')) {
htmlString = htmlString.replace(
'</head>',
[
' <meta property="og:image:width" content="2400">',
' <meta property="og:image:height" content="1256">',
` <meta property="og:image" content="./${relImageUrl}">`,
' </head>',
].join('\n'),
);
}
}
const url = `http://localhost:${this.engine.devServer.config.port}/${relUrl}`;
await page.goto(url, { waitUntil: 'networkidle0' });
await page.screenshot({ path: imagePath });
await unlink(openGraphFile);
await writeFile(htmlPath, htmlString);
}
await browser.close();
await this.engine.stop();
} catch (e) {
console.log('Could not start dev server to generate open graph images');
console.error(e);
}
}
}

View File

@@ -2,8 +2,9 @@
import { Command } from 'commander';
import { RocketStart } from './RocketStart.js';
import { RocketBuild } from './RocketBuild.js';
import { RocketInit } from './RocketInit.js';
import { RocketLint } from './RocketLint.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: '',
@@ -53,6 +54,10 @@ export class RocketCli {
absoluteBaseUrl: '',
clearOutputDir: true,
lint: {
buildHtml: false,
},
// /** @type {{[key: string]: ImagePreset}} */
// imagePresets: {
// responsive: {
@@ -93,6 +98,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 +130,6 @@ export class RocketCli {
}
async prepare() {
await this.clearOutputDirs();
if (!this.options.presets) {
return;
}
@@ -177,9 +184,9 @@ export class RocketCli {
let pluginsMeta = [
{ plugin: RocketStart, options: {} },
{ plugin: RocketBuild, options: {} },
{ plugin: RocketInit, options: {} },
// { plugin: RocketLint },
{ plugin: RocketLint, options: {} },
{ plugin: RocketUpgrade, options: {} },
{ plugin: RocketPreview, options: {} },
];
if (Array.isArray(this.options.setupCliPlugins)) {
@@ -230,10 +237,13 @@ export class RocketCli {
}
}
async clearOutputDirs() {
async clearOutputDir() {
if (this.options.outputDir && existsSync(this.options.outputDir)) {
await rm(this.options.outputDir, { recursive: true, force: true });
}
}
async clearOutputDevDir() {
if (this.options.outputDevDir && existsSync(this.options.outputDevDir)) {
await rm(this.options.outputDevDir, { recursive: true, force: true });
}

View File

@@ -1,86 +0,0 @@
import path from 'path';
import fs from 'fs-extra';
import { fileURLToPath } from 'url';
export class RocketInit {
/**
* @param {import('commander').Command} program
* @param {import('./RocketCli.js').RocketCli} cli
*/
async setupCommand(program, cli) {
this.cli = cli;
program
.command('init')
.option(
'-o, --output-dir <path>',
'path to where to put the source files [default to current directory]',
)
.action(async cliOptions => {
// cli.setOptions(cliOptions);
this.outputDir = cliOptions.outputDir ? path.resolve(cliOptions.outputDir) : process.cwd();
await this.init();
});
}
async init() {
if (!this.outputDir) {
return;
}
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const initFilesDir = path.join(moduleDir, 'init-files');
const packageJsonPath = path.join(this.outputDir, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
await fs.writeJson(packageJsonPath, {});
}
await fs.copy(initFilesDir, this.outputDir, {
errorOnExist: true,
filter: file => !(file.endsWith('_gitignore') || file.endsWith('README.md')),
});
const packageJson = await fs.readJson(packageJsonPath);
await fs.writeJson(
packageJsonPath,
{
...packageJson,
type: 'module',
scripts: {
...packageJson.scripts,
start: 'NODE_DEBUG=engine:rendering rocket start --open',
build: 'NODE_DEBUG=engine:rendering rocket build',
},
imports: {
'#images/*': './docs/__shared/*',
},
exports: {
'.': './src/index.js',
},
},
{ spaces: 2 },
);
const gitignorePath = path.join(this.outputDir, '.gitignore');
await fs.ensureFile(gitignorePath);
await fs.appendFile(
gitignorePath,
await fs.readFile(path.join(initFilesDir, '_gitignore'), 'utf8'),
);
const readmePath = path.join(this.outputDir, 'README.md');
await fs.ensureFile(readmePath);
await fs.appendFile(
readmePath,
await fs.readFile(path.join(initFilesDir, '_gitignore'), 'utf8'),
);
console.log('All files have been created 🎉');
console.log('Start developing by running `npm start`');
process.exit(0);
}
}

View File

@@ -1,81 +1,87 @@
// /* eslint-disable */
// // @ts-nocheck
/* eslint-disable @typescript-eslint/ban-ts-comment */
// /** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */
// @ts-ignore
import { CheckHtmlLinksCli } from 'check-html-links';
import { bold, gray } from 'colorette';
import { existsSync } from 'fs';
import path from 'path';
import { buildHtml } from './build/buildHtml.js';
// import { CheckHtmlLinksCli } from 'check-html-links';
export class RocketLint {
options = {
buildHtml: false,
};
// export class RocketLint {
// static pluginName = 'RocketLint';
// commands = ['start', 'build', 'lint'];
/**
* @param {import('commander').Command} program
* @param {import('./RocketCli.js').RocketCli} cli
*/
async setupCommand(program, cli) {
this.cli = cli;
this.active = true;
// /**
// * @param {RocketCliOptions} config
// */
// setupCommand(config) {
// if (config.command === 'lint') {
// config.watch = false;
// }
// return config;
// }
program
.command('lint')
.option('-i, --input-dir <path>', 'path to where to search for source files')
.option('-b, --build-html', 'do a quick html only build and then check links')
.option('-e, --validate-externals', 'validate external links')
.action(async options => {
const { cliOptions, ...lintOptions } = options;
cli.setOptions({
...cliOptions,
lint: lintOptions,
});
this.options = { ...this.options, ...cli.options.lint };
cli.activePlugin = this;
// /**
// * @param {object} options
// * @param {RocketCliOptions} options.config
// * @param {any} options.argv
// */
// async setup({ config, argv, eleventy }) {
// this.__argv = argv;
// this.config = {
// lintInputDir: config.outputDevDir,
// lintExecutesEleventyBefore: true,
// ...config,
// };
// this.eleventy = eleventy;
// }
await this.lint();
});
}
// async lintCommand() {
// if (this.config.lintExecutesEleventyBefore) {
// await this.eleventy.write();
// // updated will trigger linting
// } else {
// await this.__lint();
// }
// }
async lint() {
if (!this.cli) {
return;
}
// async __lint() {
// if (this.config?.pathPrefix) {
// console.log('INFO: RocketLint currently does not support being used with a pathPrefix');
// 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.outputDevDir !== 'string' ||
typeof this.cli.options.outputDir !== 'string'
) {
return;
}
// const checkLinks = new CheckHtmlLinksCli();
// checkLinks.setOptions({
// ...this.config.checkLinks,
// rootDir: this.config.lintInputDir,
// printOnError: false,
// continueOnError: true,
// });
if (this.options.buildHtml) {
await buildHtml(this.cli);
}
// const { errors, message } = await checkLinks.run();
// if (errors.length > 0) {
// if (this.config.command === 'start') {
// console.log(message);
// } else {
// throw new Error(message);
// }
// }
// }
const folderToCheck = this.options.buildHtml
? this.cli.options.outputDevDir
: this.cli.options.outputDir;
// async postCommand() {
// if (this.config.watch === false) {
// await this.__lint();
// }
// }
const rootIndexHtml = path.join(folderToCheck, 'index.html');
if (!existsSync(rootIndexHtml)) {
console.log(`${bold(`👀 Linting 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;
}
// async updated() {
// if (this.config.watch === true) {
// await this.__lint();
// }
// }
// }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { buildHtml: _drop, ...userCheckHtmlLinksOptions } = this.options;
const checkLinks = new CheckHtmlLinksCli();
checkLinks.setOptions({
rootDir: folderToCheck,
printOnError: true,
continueOnError: false,
absoluteBaseUrl: this.cli.options.absoluteBaseUrl,
...userCheckHtmlLinksOptions,
});
await checkLinks.run();
}
}

View 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();
}
}
}

View File

@@ -31,11 +31,12 @@ export class RocketStart {
if (!this.cli) {
return;
}
await this.cli.clearOutputDevDir();
// TODO: enable URL support in the Engine and remove this "workaround"
if (
typeof this.cli.options.inputDir !== 'string' ||
typeof this.cli.options.outputDir !== 'string'
typeof this.cli.options.outputDevDir !== 'string'
) {
return;
}
@@ -50,7 +51,7 @@ export class RocketStart {
this.engine = new Engine();
this.engine.setOptions({
docsDir: this.cli.options.inputDir,
outputDir: this.cli.options.outputDir,
outputDir: this.cli.options.outputDevDir,
setupPlugins: this.cli.options.setupEnginePlugins,
open: this.cli.options.open,
longFileHeaderWidth: this.cli.options.longFileHeaderWidth,

View File

@@ -0,0 +1,28 @@
import { Engine } from '@rocket/engine/server';
/**
* @param {import('../RocketCli.js').RocketCli} cli
* @returns
*/
export async function buildHtml(cli) {
// TODO: enable URL support in the Engine and remove this typescript "workaround"
if (typeof cli.options.inputDir !== 'string' || typeof cli.options.outputDevDir !== 'string') {
return;
}
await cli.clearOutputDevDir();
const engine = new Engine();
engine.setOptions({
docsDir: cli.options.inputDir,
outputDir: cli.options.outputDevDir,
setupPlugins: cli.options.setupEnginePlugins,
longFileHeaderWidth: cli.options.longFileHeaderWidth,
longFileHeaderComment: cli.options.longFileHeaderComment,
renderMode: 'production',
clearOutputDir: cli.options.clearOutputDir,
});
console.log('Engine building...');
await engine.build({ autoStop: cli.options.buildAutoStop });
return engine;
}

View File

@@ -0,0 +1,88 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import path from 'path';
import { existsSync } from 'fs';
import { rollup } from 'rollup';
// @ts-ignore
import { createMpaConfig, createServiceWorkerConfig } from '@rocket/building-rollup';
// import { rollupPluginHTML } from '@web/rollup-plugin-html';
// import { adjustPluginOptions } from 'plugins-manager';
/**
* @param {import('rollup').RollupOptions} config
*/
async function buildAndWrite(config) {
if (!config.output) {
return;
}
const bundle = await rollup(config);
if (Array.isArray(config.output)) {
await bundle.write(config.output[0]);
await bundle.write(config.output[1]);
} else {
await bundle.write(config.output);
}
}
/**
* @param {import('../RocketCli.js').RocketCli} cli
* @param {import('@rocket/engine/server').Engine} engine
* @returns
*/
export async function buildJavaScriptOptimizedOutput(cli, engine) {
const config = cli.options;
// 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 config.outputDir !== 'string' || typeof config.outputDevDir !== 'string') {
return;
}
await cli.clearOutputDir();
// TODO: pathPrefix is currently not supported
// const defaultSetupPlugins = [];
// if (config.pathPrefix) {
// defaultSetupPlugins.push(
// adjustPluginOptions(rollupPluginHTML, { absolutePathPrefix: config.pathPrefix }),
// );
// }
const mpaConfig = createMpaConfig({
input: '**/*.html',
output: {
dir: config.outputDir,
},
// custom
rootDir: path.resolve(config.outputDevDir),
absoluteBaseUrl: config.absoluteBaseUrl,
setupPlugins: [
// ...defaultSetupPlugins,
...config.setupDevServerAndBuildPlugins,
...config.setupBuildPlugins,
],
});
const finalConfig =
typeof config.adjustBuildOptions === 'function'
? config.adjustBuildOptions(mpaConfig)
: mpaConfig;
await buildAndWrite(finalConfig);
const { serviceWorkerSourcePath } = config;
if (existsSync(serviceWorkerSourcePath)) {
const serviceWorkerConfig = createServiceWorkerConfig({
input: serviceWorkerSourcePath,
output: {
file: path.join(path.resolve(config.outputDir), config.serviceWorkerName),
},
});
await buildAndWrite(serviceWorkerConfig);
}
// copy static files over
await engine.copyPublicFilesTo(config.outputDir);
}

View File

@@ -0,0 +1,95 @@
import { gatherFiles } from '@rocket/engine';
import { Engine } from '@rocket/engine/server';
import { fromRollup } from '@web/dev-server-rollup';
import { readFile, unlink, writeFile } from 'fs/promises';
import puppeteer from 'puppeteer';
import path from 'path';
/**
* @param {import('../RocketCli.js').RocketCli} cli
* @returns
*/
export async function buildOpenGraphImages(cli) {
const openGraphFiles = await gatherFiles(cli.options.outputDevDir, {
fileEndings: ['.opengraph.html'],
});
if (openGraphFiles.length === 0) {
return;
}
// TODO: enable URL support in the Engine and remove this typescript "workaround"
if (typeof cli.options.inputDir !== 'string' || typeof cli.options.outputDevDir !== 'string') {
return;
}
const withWrap = cli.options.setupDevServerAndBuildPlugins
? cli.options.setupDevServerAndBuildPlugins.map(modFunction => {
modFunction.wrapPlugin = fromRollup;
return modFunction;
})
: [];
const engine = new Engine();
engine.setOptions({
docsDir: cli.options.inputDir,
outputDir: cli.options.outputDevDir,
setupPlugins: cli.options.setupEnginePlugins,
open: false,
clearOutputDir: false,
adjustDevServerOptions: cli.options.adjustDevServerOptions,
setupDevServerMiddleware: cli.options.setupDevServerMiddleware,
setupDevServerPlugins: [...cli.options.setupDevServerPlugins, ...withWrap],
});
try {
await engine.start();
if (!engine?.devServer?.config.port) {
return;
}
const browser = await puppeteer.launch();
const page = await browser.newPage();
// In 2022 Twitter & Facebook recommend a size of 1200x628 - we capture with 2 dpr for retina displays
await page.setViewport({
width: 1200,
height: 628,
deviceScaleFactor: 2,
});
for (const openGraphFile of openGraphFiles) {
const relUrl = path.relative(cli.options.outputDevDir, openGraphFile);
const imagePath = openGraphFile.replace('.opengraph.html', '.opengraph.png');
const htmlPath = openGraphFile.replace('.opengraph.html', '.html');
const relImageUrl = path.basename(imagePath);
let htmlString = await readFile(htmlPath, 'utf8');
if (!htmlString.includes('<meta property="og:image"')) {
if (htmlString.includes('</head>')) {
htmlString = htmlString.replace(
'</head>',
[
' <meta property="og:image:width" content="2400">',
' <meta property="og:image:height" content="1256">',
` <meta property="og:image" content="./${relImageUrl}">`,
' </head>',
].join('\n'),
);
}
}
const url = `http://localhost:${engine.devServer.config.port}/${relUrl}`;
await page.goto(url, { waitUntil: 'networkidle0' });
await page.screenshot({ path: imagePath });
await unlink(openGraphFile);
await writeFile(htmlPath, htmlString);
}
await browser.close();
await engine.stop();
} catch (e) {
console.log('Could not start dev server to generate open graph images');
console.error(e);
}
}

View 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) {
//
}
}

View File

@@ -1,10 +0,0 @@
{
"gitdoc.enabled": false,
"typescript.tsdk": "node_modules/typescript/lib",
"files.exclude": {
"**/*-mdjs-generated.js": true,
},
"search.exclude": {
"**/*-mdjs-generated.js": true,
}
}

View File

@@ -1,9 +0,0 @@
# Welcome to Rocket
Get started with
```bash
npm start
```
Create new pages by calling them `my-page.rocket.md` and by putting them into `docs/`.

View File

@@ -1,8 +0,0 @@
## npm
node_modules
npm-debug.log
## Rocket ignore files
*-mdjs-generated.js
_site
_site-dev

View File

@@ -1,6 +0,0 @@
import { rocketLaunch } from '@rocket/launch';
export default /** @type {import('@rocket/cli').RocketCliOptions} */ ({
absoluteBaseUrl: 'http://localhost:8080',
presets: [rocketLaunch()],
});

View File

@@ -1,29 +0,0 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--guides/10--first-pages/10--getting-started.rocket.md';
import { pageTree, layout, html } from '../../recursive.data.js';
export { pageTree, layout, html };
/* END - Rocket auto generated - do not touch */
```
# Getting Started
Rocket has the following prerequisites:
- [Node 14+](https://nodejs.org/en/)
Make sure they are installed before proceeding.
## Setup
The fastest way to get started is by using an existing preset like the launch preset.
<inline-notification type="warning">
If you don't want to use the `module` package type, make sure to rename the generated config file to `rocket.config.mjs`.
</inline-notification>
```js script
import '@rocket/launch/inline-notification/inline-notification.js';
```

View File

@@ -1,24 +0,0 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--guides/10--first-pages/20--custom-layout.rocket.md';
import { pageTree } from '../../recursive.data.js';
export { pageTree };
/* END - Rocket auto generated - do not touch */
import { html } from 'lit';
export const layout = data => html`
<html>
<head></head>
<body>
<p>A FULLY custom layout</p>
${data.content()}
<a href="../index.rocket.md">Go back to Guides</a>
</body>
</html>
`;
```
# Custom Layout
Here is my markdown content.

View File

@@ -1,43 +0,0 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--guides/10--first-pages/20--images.rocket.md';
import { pageTree, layout, html } from '../../recursive.data.js';
export { pageTree, layout, html };
/* END - Rocket auto generated - do not touch */
```
# Images
We can add a private package import to your
👉 `package.json`
```json
"imports": {
"#images/*": "./docs/__shared/*"
},
```
With that we can then use `resolve:[[npm resolve name]]` in our urls.
```md
![rocket image](resolve:#images/rocket-image.jpg)
```
<div style="width: 50%">
![rocket image](resolve:#images/rocket-image.jpg)
</div>
You can also include images from dependencies.
```md
![astronaut](resolve:@rocket/launch/assets/404/astronaut.svg)
```
<div style="width: 50%">
![astronaut](resolve:@rocket/launch/assets/404/astronaut.svg)
</div>

View File

@@ -1,12 +0,0 @@
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--guides/10--first-pages/index.rocket.js';
import { pageTree, layout } from '../../recursive.data.js';
export { pageTree, layout };
/* END - Rocket auto generated - do not touch */
import { html } from 'lit';
export default () => html`
<h1>First Pages</h1>
<meta name="menu:exclude" content="true" />
`;

View File

@@ -1,17 +0,0 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--guides/index.rocket.md';
import { pageTree, layout, html } from '../recursive.data.js';
export { pageTree, layout, html };
/* END - Rocket auto generated - do not touch */
import { ChildListMenu } from '@rocket/engine';
```
# Learning Rocket
<meta name="menu:link.text" content="Guides">
Rocket helps you generate static pages from Markdown files while giving you the flexibility to sprinkle in some JavaScript where needed.
${pageTree.renderMenu(new ChildListMenu(), sourceRelativeFilePath)}

View File

@@ -1,16 +0,0 @@
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '20--docs/index.rocket.js';
import { pageTree, layout, html } from '../recursive.data.js';
export { pageTree, layout, html };
/* END - Rocket auto generated - do not touch */
import { ChildListMenu } from '@rocket/engine';
export default () => html`
<meta name="menu:link.text" content="Docs" />
<h1>Documentation</h1>
<p>Here you will find all the details for each of the packages/systems we offer.</p>
${pageTree.renderMenu(new ChildListMenu(), sourceRelativeFilePath)}
`;

View File

@@ -1,21 +0,0 @@
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '20--docs/star-wars.rocket.js';
import { pageTree, layout, html } from '../recursive.data.js';
export { pageTree, layout, html };
/* END - Rocket auto generated - do not touch */
import cache from '@11ty/eleventy-cache-assets';
const films = await cache('https://swapi.dev/api/films/', {
duration: '1d',
type: 'json',
});
export default () => html`
<h1>Star Wars</h1>
<h2>Films:</h2>
<ul>
${films.results.map(film => html`<li>${film.title} (${film.release_date})</li>`)}
</ul>
`;

View File

@@ -1,11 +0,0 @@
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '404.html.rocket.js';
import { pageTree, html } from './recursive.data.js';
export { pageTree, html };
/* END - Rocket auto generated - do not touch */
import { Layout404 } from '@rocket/launch';
export const layout = new Layout404();
export default () => '';

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,45 +0,0 @@
export const footerMenu = [
{
name: 'Discover',
children: [
{
text: 'Blog',
href: '/blog/',
},
{
text: 'Help and Feedback',
href: 'https://github.com/modernweb-dev/rocket/issues',
},
],
},
{
name: 'Follow',
children: [
{
text: 'GitHub',
href: 'https://github.com/modernweb-dev/rocket',
},
{
text: 'Twitter',
href: 'https://twitter.com/modern_web_dev',
},
{
text: 'Slack',
href: '/about/slack/',
},
],
},
{
name: 'Support',
children: [
{
text: 'Sponsor',
href: '/about/sponsor/',
},
{
text: 'Contribute',
href: 'https://github.com/modernweb-dev/rocket/blob/main/CONTRIBUTING.md',
},
],
},
];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -1,52 +0,0 @@
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = 'index.rocket.js';
import { pageTree } from './recursive.data.js';
export { pageTree };
/* END - Rocket auto generated - do not touch */
import { html } from 'lit';
import { LayoutHome } from '@rocket/launch';
import { footerMenu } from './__shared/footerMenu.js';
export const layout = new LayoutHome({
pageTree,
footerMenu,
slogan: 'The modern web setup for static sites with a sprinkle of JavaScript.',
callToActionItems: [
{ text: 'Follow Guides', href: '/guides/' },
{ text: 'Browse Docs', href: '/docs/' },
],
background: '/home-background.svg',
reasonHeader: 'Why Rocket?',
reasons: [
{
header: 'Small',
text: 'No overblown tools or frontend frameworks, add JavaScript and/or Web Components only on pages where needed.',
},
{
header: 'Pre-Rendered',
text: 'Statically generated content means less JavaScript to ship and process.',
},
{
header: 'Zero Configuration',
text: 'Automatic code splitting, filesystem based routing, and JavaScript in Markdown.',
},
{
header: 'Meta Framework',
text: html`Build on top of giants like <a href="https://www.11ty.dev/">Eleventy</a>,
<a href="https://rollupjs.org/">Rollup</a>, and
<a href="https://www.modern-web.dev/">Modern Web</a>.`,
},
{
header: 'Powerful Default Template',
text: 'Provide content and you are ready to go.',
},
{
header: 'Ready for Production',
text: 'Optimized for a smaller build size, faster dev compilation and dozens of other improvements.',
},
],
});
export default () => '';

View File

@@ -1,121 +0,0 @@
{
"title": "Rocket | Rocket",
"h1": "Rocket",
"name": "Rocket",
"menuLinkText": "Rocket",
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0,
"children": [
{
"title": "Learning Rocket | Rocket",
"h1": "Learning Rocket",
"headlinesWithId": [
{
"text": "Learning Rocket",
"id": "learning-rocket",
"level": 1
}
],
"menuLinkText": "Guides",
"name": "Learning Rocket",
"url": "/guides/",
"outputRelativeFilePath": "guides/index.html",
"sourceRelativeFilePath": "10--guides/index.rocket.md",
"level": 1,
"children": [
{
"title": "First Pages | Rocket",
"h1": "First Pages",
"menuNoLink": true,
"name": "First Pages",
"menuLinkText": "First Pages",
"url": "/guides/first-pages/",
"outputRelativeFilePath": "guides/first-pages/index.html",
"sourceRelativeFilePath": "10--guides/10--first-pages/index.rocket.js",
"level": 2,
"children": [
{
"title": "Getting Started | Rocket",
"h1": "Getting Started",
"headlinesWithId": [
{
"text": "Getting Started",
"id": "getting-started",
"level": 1
},
{
"text": "Setup",
"id": "setup",
"level": 2
}
],
"name": "Getting Started",
"menuLinkText": "Getting Started",
"url": "/guides/first-pages/getting-started/",
"outputRelativeFilePath": "guides/first-pages/getting-started/index.html",
"sourceRelativeFilePath": "10--guides/10--first-pages/10--getting-started.rocket.md",
"level": 3
},
{
"h1": "Custom Layout",
"headlinesWithId": [
{
"text": "Custom Layout",
"id": "custom-layout",
"level": 1
}
],
"name": "Custom Layout",
"menuLinkText": "Custom Layout",
"url": "/guides/first-pages/custom-layout/",
"outputRelativeFilePath": "guides/first-pages/custom-layout/index.html",
"sourceRelativeFilePath": "10--guides/10--first-pages/20--custom-layout.rocket.md",
"level": 3
},
{
"title": "Images | Rocket",
"h1": "Images",
"headlinesWithId": [
{
"text": "Images",
"id": "images",
"level": 1
}
],
"name": "Images",
"menuLinkText": "Images",
"url": "/guides/first-pages/images/",
"outputRelativeFilePath": "guides/first-pages/images/index.html",
"sourceRelativeFilePath": "10--guides/10--first-pages/20--images.rocket.md",
"level": 3
}
]
}
]
},
{
"title": "Documentation | Rocket",
"menuLinkText": "Docs",
"h1": "Documentation",
"name": "Documentation",
"url": "/docs/",
"outputRelativeFilePath": "docs/index.html",
"sourceRelativeFilePath": "20--docs/index.rocket.js",
"level": 1,
"children": [
{
"title": "Star Wars | Rocket",
"h1": "Star Wars",
"name": "Star Wars",
"menuLinkText": "Star Wars",
"url": "/docs/star-wars/",
"outputRelativeFilePath": "docs/star-wars/index.html",
"sourceRelativeFilePath": "20--docs/star-wars.rocket.js",
"level": 2
}
]
}
]
}

View File

@@ -1,16 +0,0 @@
import { PageTree } from '@rocket/engine';
import { LayoutSidebar } from '@rocket/launch';
import { footerMenu } from './__shared/footerMenu.js';
import { html } from 'lit';
export const pageTree = new PageTree({
inputDir: new URL('./', import.meta.url),
outputDir: new URL('../_site', import.meta.url),
});
await pageTree.restore();
export const layout = new LayoutSidebar({ pageTree, footerMenu });
export { html };
// export const openGraphLayout = new OpenGraphLayoutLogo();

View File

@@ -1,33 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"outDir": "./dist-types",
"rootDir": ".",
"composite": true,
"allowJs": true,
"checkJs": true,
"emitDeclarationOnly": true,
"target": "esnext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"downlevelIteration": true,
"strict": true,
"moduleResolution": "Node",
"typeRoots": ["@types", "./types"],
"types": ["node"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": false,
"forceConsistentCasingInFileNames": true,
"lib": [
"DOM",
"DOM.Iterable",
"ES6",
"ES2017",
// Allows array.flatMap. import `array-flat-polyfill` to cover node10
"ES2019.array",
"ScriptHost"
]
}
}

View 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.',
),
);
}

View File

@@ -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

View 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('');
});
});

View File

@@ -0,0 +1 @@
!__output

View File

@@ -0,0 +1 @@
so preview does not stop

View File

@@ -47,6 +47,11 @@ export interface FullRocketCliOptions extends Pick<FullRocketPreset, PresetKeys>
// rarely used
configFile: string;
outputDevDir: URL | string;
lint: {
buildHtml: boolean;
[key: string]: any;
};
}
export type RocketCliOptions = Partial<FullRocketCliOptions>;

View File

@@ -1,5 +1,11 @@
# @rocket/create
## 0.1.1
### Patch Changes
- 79e6f0d: Update error message to include an action to resolve a possible [digit error](https://github.com/Rich-Harris/degit/issues/313).
## 0.1.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/create",
"version": "0.1.0",
"version": "0.1.1",
"publishConfig": {
"access": "public"
},

View File

@@ -152,7 +152,11 @@ export class CreateCli {
await gitHandler.clone('./' + newFolderPath);
cloneError = false;
} catch (e) {
console.log(`${red('✖')} Could not apply the template - maybe the url is wrong?`);
console.log(
`${red(
'✖',
)} Could not apply the template - maybe the url is wrong? If this problem persists, you could try clearing your .degit cache, usually located at ~/.degit`,
);
console.log(`${red('>')} ${e.message}`);
const response = await prompts({
type: 'text',

View File

@@ -1,5 +1,22 @@
# @rocket/engine
## 0.2.7
### Patch Changes
- 0ed3d6d: Adjust urls containing url fragments
```html
<!-- user writes -->
<a href="./about.rocket.js#some-id"></a>
<!-- rocket outputs -->
<!-- before -->
<a href="./about.rocket.js#some-id"></a>
<!-- after -->
<a href="/about/#some-id"></a>
```
## 0.2.6
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/engine",
"version": "0.2.6",
"version": "0.2.7",
"publishConfig": {
"access": "public"
},

View File

@@ -19,32 +19,39 @@ import { parser, SaxEventType } from '../web-menu/sax-parser.js';
* @returns
*/
async function defaultAdjustAssetUrl({
url,
url: fullUrl,
sourceFilePath,
sourceRelativeFilePath,
outputFilePath,
}) {
let url = fullUrl;
let fragment = '';
if (!url.startsWith('resolve:#') && url.includes('#')) {
const urlParts = url.split('#');
url = urlParts[0];
fragment = `#${urlParts[1]}`;
}
if (url.startsWith('resolve:')) {
const bareImport = url.substring(8);
const requireOfSource = createRequire(sourceFilePath);
const resolvedPath = requireOfSource.resolve(bareImport);
const rel = path.relative(path.dirname(outputFilePath), resolvedPath);
return rel;
return rel + fragment;
}
if (url.match(/^[a-z]+:/) || url.startsWith('//')) {
return url;
return url + fragment;
}
if (isRocketPageFile(url)) {
const dir = path.dirname(sourceRelativeFilePath);
return sourceRelativeFilePathToUrl(path.join(dir, url));
return sourceRelativeFilePathToUrl(path.join(dir, url)) + fragment;
}
if (url.startsWith('./') || url.startsWith('../')) {
return path.relative(
path.dirname(outputFilePath),
path.join(path.dirname(sourceFilePath), url),
return (
path.relative(path.dirname(outputFilePath), path.join(path.dirname(sourceFilePath), url)) +
fragment
);
}
return url;
return url + fragment;
}
export class AdjustAssetUrls {

View File

@@ -1,5 +1,6 @@
import chai from 'chai';
import { AdjustAssetUrls } from '@rocket/engine';
import { expectThrowsAsync } from './test-helpers.js';
const { expect } = chai;
@@ -48,7 +49,7 @@ describe('AdjustAssetUrls', () => {
);
});
it('ignores <a href="#foo"></a>', async () => {
it('does not adjust <a href="#foo"></a>', async () => {
const adjust = new AdjustAssetUrls();
expect(await adjust.transform('<a href="#foo">go</a>', options)).to.equal(
'<a href="#foo">go</a>',
@@ -104,4 +105,43 @@ describe('AdjustAssetUrls', () => {
}),
).to.equal('<a href="/">go</a>');
});
it('adjust <a href="./about.rocket.js#some-id"></a>', async () => {
const adjust = new AdjustAssetUrls();
expect(await adjust.transform('<a href="./about.rocket.js#some-id">go</a>', options)).to.equal(
'<a href="/about/#some-id">go</a>',
);
expect(
await adjust.transform('<a href="./about.rocket.js#some-id">go</a>', {
sourceRelativeFilePath: 'components/index.rocket.js',
outputFilePath: '/my/path/to/__output/components/index.html',
}),
).to.equal('<a href="/components/about/#some-id">go</a>');
expect(
await adjust.transform('<a href="./about.rocket.js#some-id">go</a>', {
sourceRelativeFilePath: 'components.rocket.js',
outputFilePath: '/my/path/to/__output/components/index.html',
}),
).to.equal('<a href="/about/#some-id">go</a>');
expect(
await adjust.transform('<a href="./index.rocket.js#some-id">go</a>', {
sourceRelativeFilePath: 'about.rocket.js',
outputFilePath: '/my/path/to/__output/about/index.html',
}),
).to.equal('<a href="/#some-id">go</a>');
});
it('still resolves private imports <img src="resolve:#src/logo.svg" />', async () => {
const adjust = new AdjustAssetUrls();
// we check for the resolve throw as this private import does not exists =>
// which means if a not resolve related error in our code happens the test fails
await expectThrowsAsync(
() => adjust.transform('<img src="resolve:#src/logo.svg" />', options),
{
errorMatch: /Cannot find module '#src\/logo\.svg'/,
},
);
});
});

View File

@@ -13,6 +13,17 @@ const { expect } = chai;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Expect a throw in an async function
*
* @example
* await expectThrowsAsync(() => myAsyncFunction(), {
* errorMatch: /exact throw message/,
* });
* @example
* await expectThrowsAsync(() => myAsyncFunction(), {
* errorMatch: /check throw message with a regex/,
* });
*
* @param {function} method
* @param {string} errorMessage
*/

View File

@@ -1,5 +1,34 @@
# @rocket/launch
## 0.21.3
### Patch Changes
- 660f64c: Change the default gitBranch to main
- Updated dependencies [68e05f4]
- @rocket/cli@0.20.4
## 0.21.2
### Patch Changes
- 87c10ec: Work without JavaScript if Declarative Shadow Dom (DSD) is supported by browser
- d7e461c: Replace Layout Options `logoSrc` and `logoAlt` strings with a `logoSmall` TemplateResult
```diff
- logoSrc: '/icon.svg',
- logoAlt: 'Rocket Logo',
+ logoSmall: html`
+ <img src="resolve:@rocket/launch/assets/rocket-logo-light.svg" alt="Rocket" width="250" height="67.87" />
+ `,
```
- a12adf2: Add padding above slogan an home page
- Updated dependencies [a48dcd8]
- Updated dependencies [0ed3d6d]
- @rocket/cli@0.20.3
- @rocket/engine@0.2.7
## 0.21.1
### Patch Changes

View File

@@ -0,0 +1,12 @@
<svg width="105" height="101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m19.91 91.438 12.445-4.705-8.09-7.465-4.355 12.17Z" fill="#FCB736"/>
<path d="M53.778 41.196c-1.6-5.176 1.444-10.652 6.844-12.192 5.378-1.54 11.067 1.39 12.667 6.588a9.426 9.426 0 0 1 .277 4.512 9.595 9.595 0 0 1-1.86 4.151 10.076 10.076 0 0 1-3.6 2.902 10.485 10.485 0 0 1-4.573 1.033c-4.4 0-8.444-2.738-9.755-6.994ZM68.666 16.62c-9.066 2.952-19.088 8.085-27.022 16.042-6.4 6.416-11.311 12.47-15 17.71a28.068 28.068 0 0 0-3.889.32A23.592 23.592 0 0 0 18.6 51.72C9.533 54.928-.49 63.42.133 65.11c.6 1.69 6.467-1.433 11.467-1.818 2.91-.236 5.178.042 6.91.492-.577 1.154-1.066 2.138-1.421 2.93-.378.791-.645 1.369-.823 1.732l18.09 16.705c.733-.385 2.377-1.112 4.733-2.353.266.813.51 1.776.644 2.866.156 1.091.222 2.332.156 3.743-.2 4.813-3.2 10.588-1.423 11.101 1.778.514 10.178-9.475 13.134-18.33.91-2.695 1.133-5.433 1.066-7.786 5.29-3.785 11.356-8.726 17.756-15.164 7.933-7.957 12.844-17.796 15.533-26.65 2.356-7.765 2.978-14.738 2.4-19.037a22.605 22.605 0 0 0-2.155-.085c-4.511-.022-10.756.962-17.534 3.165Z" fill="url(#a)"/>
<path d="m12.4 89.535 15.911-6.545-9.777-9.026-6.134 15.57Z" fill="#D08A06"/>
<path d="m10.488 82.733 12.467-4.684-8.089-7.465-4.378 12.149ZM90.644 16.406c.044 2.823-.267 6.096-.912 9.54 7.712 10.544 10.889 24.083 7.578 37.623-5.778 23.591-29.178 38.756-53.622 35.612-.133.15-.267.278-.4.428 27.045 4.834 53.556-11.614 59.911-37.666 4.111-16.897-1.155-33.752-12.555-45.537ZM2.82 42.158C8.955 17.048 35.088 1.477 61.177 7.401c5 1.134 9.644 2.973 13.822 5.39 3.6-.791 7.067-1.262 10.067-1.347-5.822-4.492-12.756-7.828-20.511-9.582C36.665-4.47 8.733 12.172 2.177 39.014a47.973 47.973 0 0 0-.511 20.619c.111-.107.222-.193.333-.3-.8-5.56-.6-11.378.822-17.175Z" fill="#FCB736"/>
<defs>
<linearGradient id="a" x1="26.5" y1="57" x2="89" y2="13" gradientUnits="userSpaceOnUse">
<stop stop-color="#D21B1D"/>
<stop offset="1" stop-color="#C9181A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/launch",
"version": "0.21.1",
"version": "0.21.3",
"publishConfig": {
"access": "public"
},
@@ -46,9 +46,9 @@
"preset"
],
"dependencies": {
"@rocket/cli": "^0.20.1",
"@rocket/cli": "^0.20.4",
"@rocket/components": "^0.2.0",
"@rocket/engine": "^0.2.6",
"@rocket/engine": "^0.2.7",
"@webcomponents/template-shadowroot": "^0.1.0",
"lit": "^2.3.0",
"workbox-window": "^6.1.5"

View File

@@ -45,10 +45,19 @@ export class LayoutMain extends Layout {
headerDarkBackground: false,
dsdPending: true,
siteName: 'Rocket',
logoSrc: '/icon.svg',
logoAlt: 'Rocket Logo',
logoSmall: html`
<picture>
<!-- <source srcset="resolve:@rocket/launch/assets/rocket-logo-dark.svg" media="(prefers-color-scheme: dark)"> -->
<img
src="resolve:@rocket/launch/assets/rocket-logo-light.svg"
alt="Rocket Logo"
width="250"
height="67.87"
/>
</picture>
`,
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
gitBranch: 'next',
gitBranch: 'main',
description: 'A static site generator for modern web development',
socialLinks: [
{
@@ -137,17 +146,9 @@ export class LayoutMain extends Layout {
<link rel="stylesheet" href="resolve:@rocket/launch/css/markdown.css" />
`,
head__50: html`
<style>
body[dsd-pending] {
display: none;
}
</style>
`,
header__10: html`
<a class="logo-link" href="/" slot="logo">
<img src="/icon.svg" alt="${this.options.logoAlt}" />
${this.options.logoSmall}
<span>${this.options.siteName}</span>
</a>
`,
@@ -171,7 +172,7 @@ export class LayoutMain extends Layout {
drawer__10: html`
<a class="logo-link" href="/">
<img src="${this.options.logoSrc}" alt="${this.options.logoAlt}" />
${this.options.logoSmall}
<span>${this.options.siteName}</span>
</a>
`,

View File

@@ -3,6 +3,6 @@ export const defaultGlobalOptions = {
logoSrc: '/icon.svg',
logoAlt: 'Rocket Logo',
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
gitBranch: 'master',
gitBranch: 'main',
description: 'A static site generator for modern web development',
};

View File

@@ -122,6 +122,7 @@ export class LaunchHome extends LitElement {
.page-slogan {
font-size: 18px;
padding-top: 25px;
}
.reason-header {

View File

@@ -7,8 +7,7 @@ export interface LayoutSidebarOptions extends LayoutOptions {
[key: TemplateValueKey<'drawer'>]: TemplateValue;
siteName: string;
logoSrc: string;
logoAlt: string;
logoSmall: TemplateValue;
gitSiteUrl: string;
gitBranch: string;
description: string;

View File

@@ -93,4 +93,4 @@ If something is missing in the documentation or if you found some part confusing
## Credit
This getting started guide was originally based off of [astro](https://astro.build/) getting started guide.
This getting started guide was originally based off the [astro](https://astro.build/) getting started guide.

View File

@@ -44,11 +44,11 @@ Before we get started we need to engage the engines via
npm start
```
This with start rocket in development mode and you will see your site running in your browser.
This will start rocket in development mode and you will see your site running in your browser.
## What is a page?
A page is a file that ends either with `*.rocket.js`, `*.rocket.md` or `*.rocket.html` and is located in the input directory (`site/pages` by default). Pages will make up the majority or your website.
A page is a file that ends either with `*.rocket.js`, `*.rocket.md` or `*.rocket.html` and is located in the input directory (`site/pages` by default). Pages will make up the majority of your website.
The simplest way to get started is to create a file

View File

@@ -53,7 +53,7 @@ e.g.
## Recommended Project Structure
Even if there is no enforce project structure it still makes sense to follow some common best practices.
Even if there is no enforced project structure it still makes sense to follow some common best practices.
- `site/pages/*` - All the pages of your website (e.g. all `*.rocket.{js,md,html}` files)
- `site/pages/about/_assets/*` - Keep assets related to pages close to the page itself (e.g. images, videos, ...)
@@ -116,4 +116,4 @@ Rocket has complete control over how these files get processed, optimized, and b
For most users, the majority of your files will live inside of the `site/pages/` and `site/src/` directory so that Rocket can optimize them in your final build. By contrast, the `site/public/` directory is the place for any files to live outside of the Rocket build process.
If you put a file into the public folder, it will not be processed by Rocket. Instead it will be copied into the build folder untouched. This can be useful for specific file like `robots.txt` or `site.webmanifest` or sometimes for assets like images that you need in a specific location.
If you put a file into the public folder, it will not be processed by Rocket. Instead it will be copied into the build folder untouched. This can be useful for specific files like `robots.txt` or `site.webmanifest` or sometimes for assets like images that you need in a specific location.

View File

@@ -30,7 +30,7 @@ export const description = `An intro to Rocket pages, which is the actual websit
# Pages
**Pages** are a the only files that get rendered into the output folder.
**Pages** are the only files that get rendered into the output folder.
3 types of pages are supported:
@@ -38,11 +38,11 @@ export const description = `An intro to Rocket pages, which is the actual websit
- `*.rocket.html` - A page with HTML content.
- `*.rocket.js` - A page with JavaScript content.
Feel free to choose a format for each pages that best suits its content.
Feel free to choose a format for each page that best suits its content.
## File-based Routing
Rockets uses Pages to do something called **file-based routing.** Every file in your `pages` directory becomes a page on your site, using the file name to decide the final route.
Rocket uses Pages to do something called **file-based routing.** Every file in your `pages` directory becomes a page on your site, using the file name to decide the final route.
```
site/pages/index.rocket.md -> mysite.com/
@@ -72,7 +72,7 @@ export default () => `
👆 This is the JavaScript version which all other formats automatically convert to.
However for you your convenience you can also write your page in HTML and it will be automatically converted into the JavaScript version above.
However for your convenience you can also write your page in HTML and it will be automatically converted into the JavaScript version above.
👉 `site/pages/index.rocket.html`
@@ -104,8 +104,8 @@ Or you can write markdown.
Markdown and HTML files are automatically converted to their JavaScript equivalents.
That is also how they get their template literal super powers.
Those converted files only life for a fraction of a second and are deleted after the page is done rendering.
If you would like to keep them for debugging or out of curiosity you can export the following flag to prefect the cleanup.
Those converted files only live for a fraction of a second and are deleted after the page is done rendering.
If you would like to keep them for debugging or out of curiosity you can export the following flag to prevent the cleanup.
```js
export const keepConvertedFiles = true;

View File

@@ -90,7 +90,7 @@ For that reasons you can override the layout via exporting `layout` on a specifi
In the above example we have 4 layouts we define in different places.
1. Default layout use by all pages unless overwritten in `recursive.data.js`
1. Default layout used by all pages unless overwritten in `recursive.data.js`
2. Explicitly specifying a layout `index.rocket.js`
3. use `blog/local.data.js` to set a layout for all pages in `blog`
4. use `docs/codelabs/local.data.js` to set a layout for all pages in `codelabs`

View File

@@ -28,11 +28,11 @@ export const needsLoader = true;
# Components
Components in Rocket are the "just" the web standard [web components](https://developer.mozilla.org/en-US/docs/Web/Web_Components). They are used to create reusable components that can be used in any web page.
Components in Rocket are "just" the web standard [web components](https://developer.mozilla.org/en-US/docs/Web/Web_Components). They are used to create reusable components that can be used in any web page.
<inline-notification>
Web component only live within the html body. For content within the head or a full html page please see [layouts](./50--layouts.rocket.md).
Web components only live within the html body. For content within the head or a full html page please see [layouts](./50--layouts.rocket.md).
</inline-notification>
@@ -84,7 +84,7 @@ We can now put this code in Rocket JavaScript, Markdown or Html pages.
<rocket-greeting>Go</rocket-greeting>
````
will result a server rendered output that does not load ANY JavaScript
will result in a server rendered output that does not load ANY JavaScript
```
Hello World
@@ -124,7 +124,7 @@ Note the empty lines between html & markdown. They are necessary as this is how
## Manually Loading Components
We can define as many components as we want within a page but typically it's best to define them in a separate files.
We can define as many components as we want within a page but typically it's best to define them in separate files.
So we will move our component code into a new file 👉 `/site/src/components/rocket-greeting.js`
@@ -257,8 +257,8 @@ export const components = {
### Hydration
Component that do not have any interactivity will never need to be hydrated so they may be imported statically on the server side.
All other component should be handled via the `components` object to enable handling of loading and registration.
Components that do not have any interactivity will never need to be hydrated so they may be imported statically on the server side.
All other components should be handled via the `components` object to enable handling of loading and registration.
Doing so enables hydration based on attributes on the component.

View File

@@ -28,7 +28,7 @@ export const needsLoader = true;
# Layouts
Layout are special in that sense that they output the full html page (including html, head, body, etc).
Layouts are special in the sense that they output the full html page (including html, head, body, etc).
The simplest layout you can make is
@@ -48,7 +48,7 @@ export const layout = data => html`
`;
```
and that will work fine however now every page will have the same `title` in the head.
and that will work fine, however, now every page will have the same `title` in the head.
If we now have the following markdown file:
@@ -95,7 +95,7 @@ For more details see the following [lit issue](https://github.com/lit/lit/issues
</inline-notification>
In order to provide this `data.title` we now need to export is within the page.
In order to provide this `data.title` we now need to export it within the page.
The code could look something like this.
````md
@@ -151,7 +151,7 @@ const layoutB =
<inline-notification type="warning">
Partial html is not supported in [lit](http://lit.dev) as it uses the browser build in html parser which try to "auto correct" your html by closing tags.
Partial html is not supported in [lit](http://lit.dev) as it uses the browser build in html parser which trys to "auto correct" your html by closing tags.
e.g. this
<!-- prettier-ignore-start -->

View File

@@ -30,7 +30,7 @@ Rocket uses **file-based routing** to generate your build URLs based on the file
## Static routes
Rocket Pages are Markdown (`rocket.md`), HTML (`rocket.html`) and JavaScript (`rocket.js`) in the `site/pages` directory become pages on your website. Each pages route is decided based on its filename and path within the `site/pages` directory. This means that there is no separate "routing config" to maintain in an Rocket project.
Rocket Pages are Markdown (`rocket.md`), HTML (`rocket.html`) and JavaScript (`rocket.js`) in the `site/pages` directory become pages on your website. Each pages route is decided based on its filename and path within the `site/pages` directory. This means that there is no separate "routing config" to maintain in a Rocket project.
```bash
# Example: Static routes

View File

@@ -76,7 +76,7 @@ This will result in something like this:
</nav>
```
Rocket comes with multiple build in menus you can see [below](#menu-types).
Rocket comes with multiple built-in menus you can see [below](#menu-types).
## Menu Data
@@ -107,11 +107,11 @@ Now the menu will be called "Docs".
Within a menu the text of the links is defined by the following priority:
1. menuLinkText => `export const menuLinkText = 'Page Title In Menu';`
2. h1 => first `<h1>` in the page)
2. h1 => first `<h1>` in the page
3. title => html title tag
4. sourceRelativeFilePath => fallback if no other option is available
You can influence that data that gets provided to the menu by setting exports.
You can influence the data that gets provided to the menu by setting exports.
### link-text="..."
@@ -168,7 +168,7 @@ Sometimes there is a need to completely exclude a page from the pageTree.
Pages with this flag will not exist at all in the pageTree - therefore you will not be able to access them for "anything" not even in a sitemap or an update feed.
Pages that have sub pages can NOT use this flag as it would mean those sub pages would not have a parent page.
Typical use case are utility pages that are not meant to be accessed by typical users.
Typical use cases are utility pages that are not meant to be accessed by typical users.
```js
export const menuExclude = true;
@@ -245,7 +245,7 @@ In case you want to take full control over the order you can apply the following
- The menu order and file system order will no longer match
- But no numbers in folder / filenames
3. Instantiate a new PageTree and providing your own `modelComparatorFn`
3. Instantiate a new PageTree and provide your own `modelComparatorFn`
```js
function modelComparatorFn(a, b) {
@@ -261,7 +261,7 @@ In case you want to take full control over the order you can apply the following
## Page Tree
The data of the page tree gets saves as a JSON file in the root of the `pages` directory.
The data of the page tree gets saved as a JSON file in the root of the `pages` directory.
It typically looks something like this:
@@ -440,7 +440,7 @@ import {
5. **NextMenu**
- shows the next page in the tree
- is either the first child or he next sibling
- is either the first child or the next sibling
```html
<web-menu name="next">

View File

@@ -31,7 +31,7 @@ export const needsLoader = true;
# Hydration
By default all components are only rendered on the server.
This however means those components are "static" and can not add interactivity.
However, this means those components are "static" and can not add interactivity.
To add interactivity to your component you can either render it only on the client or you can render on the server and `hydrate` it on the client.
@@ -69,9 +69,9 @@ The automatic loading/hydration only works if you [register components to the ro
## Server Loading
Server Loading has almost zero impact on the client side page performance and is therefore fastest possible solution available.
Server Loading has almost zero impact on the client side page performance and is therefore the fastest possible solution available.
Loading and rendering on the server is the default and for that reasons does not need to be specified.
Loading and rendering on the server is the default and for that reason does not need to be specified.
```html
<blog-header></blog-header>
@@ -263,7 +263,7 @@ Results in `server rendering` of all `my-component`s.
Why theses impacts?
1. client - if you eagerly load & render the component on the client then bloating the html of some components by server rendering does not bring any benefit
2. hydrate - hydration means that all component with the same tag name will be hydrated on the client - you can not keep a server only variation of a component
2. hydrate - hydration means that all components with the same tag name will be hydrated on the client - you can not keep a server only variation of a component
3. server - almost zero client side impact
<inline-notification>

View File

@@ -103,7 +103,7 @@ If your package is `"type": "module"` then you can also add exports.
}
```
Once you have an export it also enables to "self" reference your package.
Once you have an export it also enables you to "self" reference your package.
Combine this with the `:resolve` function means that you can reference assets always with the same path.
Completely unrelated to the path of the file itself.

View File

@@ -54,7 +54,7 @@ For settings that may be overridden within a page, use the [**data cascade**](./
</inline-notification>
Rocket is primarily build around plugins for each of its systems.
Rocket is primarily built around plugins for each of its systems.
Given that rocket comes with a list of default plugins and that presets can add plugins it's very important to not "just" set a new "plugins" property.
@@ -65,7 +65,7 @@ New plugins can be added and all default plugins can be adjusted or even removed
```js
/** @type {import('rocket/cli').RocketCliConfig} */
export default {
// add a rollup plugins to the web dev server (will be wrapped with @web/dev-server-rollup) AND the rollup build (e.g. enable json importing)
// add a rollup plugin to the web dev server (will be wrapped with @web/dev-server-rollup) AND the rollup build (e.g. enable json importing)
setupDevServerAndBuildPlugins: [],
// add a plugin to the web dev server (will not be wrapped) (e.g. esbuild for TypeScript)
@@ -139,7 +139,7 @@ For some projects you might want to enable non-standard behaviors like importing
import data from './data.json';
```
You can accomplish this with Rollup and dev server plugins. Make sure to add both the dev-server plugin as well as the Rollup plugin, so that the behaviors is the same during development as it is in the production build.
You can accomplish this with Rollup and dev server plugins. Make sure to add both the dev-server plugin as well as the Rollup plugin, so that the behavior is the same during development as it is in the production build.
For these cases you can use `setupDevServerAndBuildPlugins`, which will automatically add the plugin for you to both Rollup and dev-server:
@@ -186,7 +186,7 @@ export default /** @type {import('rocket/cli').RocketCliOptions} */ ({
### BuildOptions (rollup)
For example if you wanna add an `acron` plugin to rollup
For example if you wanna add an `acorn` plugin to rollup
```js
import { importAssertions } from 'acorn-import-assertions';

View File

@@ -34,7 +34,7 @@ export const subTitle =
Having a nice preview image for social media can be very helpful.
For that reason Rocket has a specific functionality to generate a preview image.
This functionality is disable by default. You can enable it by exporting a `openGraphLayout` function/class instance.
This functionality is disabled by default. You can enable it by exporting a `openGraphLayout` function/class instance.
The functionality is the same as normal [Layouts](../20--basics/50--layouts.rocket.md).
@@ -53,7 +53,7 @@ The functionality is the same as normal [Layouts](../20--basics/50--layouts.rock
site/pages/sitemap.xml.rocket.js -> open graph only happens for html files
```
2. During the the build phase is does the following steps
2. During the build phase it does the following steps
3. If there is a `filename.opengraph.html` file it will open that file in puppeteer
4. It sets its screen size to 1200×628px and takes a screenshot with DPR or 2 (e.g. the image will have 2400x1228px)
5. Adjusts the "output file" e.g. `index.html` by injecting
@@ -66,13 +66,13 @@ The functionality is the same as normal [Layouts](../20--basics/50--layouts.rock
if no `<meta property="og:image"` is present
6. deletes the `filename.opengraph.html` file
6. Deletes the `filename.opengraph.html` file
## How to use it
Add an `openGraphLayout` export to our page.
Generally it's probably bet if we put this in our root `recursive.data.js` file which means that every page will have this functionality.
Generally it's probably best if we put this in our root `recursive.data.js` file which means that every page will have this functionality.
If a certain page needs a different layout we can override it.
👉 `recursive.data.js`
@@ -244,7 +244,7 @@ export const subTitle = 'From zero to hero';
## Creating an Overview
Once you have defined an openGraphLayout for all you pages it makes sense to check them.
Once you have defined an openGraphLayout for all your pages it makes sense to check them.
This however can be a time consuming process.
So it makes sense to use an overview over all the open graph images we will have.
@@ -312,7 +312,7 @@ export const layout = data => html`
`;
```
There is quite some much in there so let's go though the code.
There is quite a lot in there so let's go though the code.
```js
import { pageTree } from '#src/layouts/layoutData.js';

View File

@@ -39,7 +39,7 @@ Rocket does handle content images automatically by
- producing multiple sizes so users download images that are meant for their resolution
- outputting multiple formats so the device can choose the best image format it supports
And the best thing about is you don't need to do anything.
And the best thing is you don't need to do anything.
## Usage
@@ -110,11 +110,11 @@ will result in
## Adjusting options
Under the hood it is using [11ty/image](https://www.11ty.dev/docs/plugins/image/) and all it's options are available.
Under the hood it is using [11ty/image](https://www.11ty.dev/docs/plugins/image/) and all its options are available.
<inline-notification type="tip">
If you are using a layout preset like `@rocket/launch` then you probably don't want to touch/change these options as the preset will set it for you accordion to its layout needs.
If you are using a layout preset like `@rocket/launch` then you probably don't want to touch/change these options as the preset will set it for you according to its layout needs.
The default preset for regular markdown content is available as `responsive`.
@@ -282,7 +282,7 @@ export default {
## Default widths
In order to understand the need for having a single image in multiple resolutions we need to understand the our website is served to many different environments and each may come with its own specific device pixel ratio (DPR). The device pixel ratio is the ratio between physical pixels and logical pixels. For instance, the Galaxy S20 report a device pixel ratio of 3, because the physical linear resolution is triple the logical linear resolution.
In order to understand the need for having a single image in multiple resolutions we need to understand that our website is served to many different environments and each may come with its own specific device pixel ratio (DPR). The device pixel ratio is the ratio between physical pixels and logical pixels. For instance, the Galaxy S20 report a device pixel ratio of 3, because the physical linear resolution is triple the logical linear resolution.
Physical resolution: 1440 x 3200
Logical resolution: 480 x 1067

View File

@@ -34,6 +34,52 @@ const { resolve } = createRequire(new URL('.', import.meta.url));
A few things are usually needed before going live "for real".
## Make sure all links are correct
When you launch a website you don't want the first feedback to be "that link doesn't work".
To prevent this we want to execute `rocket lint` before going live.
It will make sure all internal links are correct by using [check-html-links](../../30--tools/40--check-html-links/10--overview.rocket.md).
Typically we deploy via a Continuous Integration system like GitHub Actions or Netlify Deploy.
We can also integrate the lint command into that process.
```
rocket build
rocket lint
```
### Fixing broken links
If found a couple of broken links on your page and you want to fix them and verify that they are now correct it might be a little time consuming to create a full production build every time.
The reason is that a production build is doing a lot of things
1. Generate HTML
2. Generate & Inject Open Graph Images
3. Optimize Images (not available yet)
4. Optimize JavaScript
But there is a way around this. We can use an optional flag `--build-html` which means it will run only (1) and then lint that (non-optimized) HTML output.
So for a more time efficient way of validating link use
```bash
rocket lint --build-html
```
Note: We can do this as 2-4 generally does not impact links/references (as long as the optimizations scripts do not have related bugs)
### Check external links
`rocket lint` can also check external links.
This will do a HEAD/GET request to every external link which means if all is green that the website only links to still active websites.
It however also means that it will do a lot of network requests which will take a while.
Use with care.
```bash
rocket lint --validate-externals
```
## Add a Not Found Page
When a user enters a URL that does not exist, a "famous" 404 Page Not Found error occurs.

View File

@@ -35,7 +35,7 @@ export const subTitle = 'Use web fonts to ensure a unique and consistent experie
Using web fonts can be tricky and as there are so many considerations
- Use a [Variable Font](https://web.dev/variable-fonts/)?
- How to [reduced the size](https://web.dev/reduce-webfont-size/)?
- How to [reduce the size](https://web.dev/reduce-webfont-size/)?
- How to avoid [a layout shift as the font is loaded](https://web.dev/preload-optional-fonts/)?
- ...
@@ -44,7 +44,7 @@ Here is a quick summary of what you should do as of 2022.
1. Use a variable font
This means only ONE font file needs to be download for all the different weights and widths.
This file is usually bigger then one weight of a font but smaller then multiple weights font files.
This file is usually bigger than one weight of a font but smaller than multiple weights font files.
Not many fonts are "easily" accessible as a variable font. Often you need to manually [convert](https://convertio.co/ttf-woff/) a variable font ttf file to a web woff2 file.
@@ -75,7 +75,7 @@ Here is a quick summary of what you should do as of 2022.
3. Use optional fonts
In combination with 2. this means there will be NO layout shift at all. Nothing will be display until the font is loaded or a timeout (usually 100ms) is reached. If there is a timeout then for this page visit a fallback font will be used.
In combination with 2. this means there will be NO layout shift at all. Nothing will be displayed until the font is loaded or a timeout (usually 100ms) is reached. If there is a timeout then for this page visit a fallback font will be used.
On the next page load the font will be cached and directly rendered with the web font.
```css

View File

@@ -53,7 +53,7 @@ export const layout = new LayoutSidebar();
```
You should also define it as a preset in the configuration so that it can copy some default public files.
(this step is not requires but it is recommended)
(this step is not required but it is recommended)
👉 `config/rocket.config.js`

View File

@@ -79,7 +79,7 @@ The registration happens via
<script type="module" src="resolve:@rocket/launch/js/register-service-worker.js"></script>
```
Below you find its implementation
Below you'll find its implementation
```js server
const serviceWorkerRegistrationCode = await inlineFile(

Some files were not shown because too many files have changed in this diff Show More