fix(engine): if a component has an error be sure to always show it

This commit is contained in:
Thomas Allmer
2022-06-23 13:41:13 +02:00
parent f06ea2117b
commit 445daf67e3
36 changed files with 214 additions and 36 deletions

View File

@@ -112,6 +112,9 @@ export class Engine {
const result = await this.renderFile({ sourceFilePath, rocketHeader });
await pageTree.add(result.sourceRelativeFilePath);
await cleanupAutoGeneratedFiles(result);
if (result.passOnError) {
throw result.passOnError;
}
}
await pageTree.save();
@@ -431,10 +434,15 @@ export class Engine {
inputDir: this.docsDir,
outputDir: this.outputDir,
renderMode: this.options.renderMode || 'development',
needsLoader: rocketHeader ? rocketHeader.needsLoader : false,
throwOnError,
};
let result = await renderViaWorker(renderOptions);
if (result.passOnError) {
if (throwOnError) {
throw result.passOnError;
}
return result;
}
if (rocketHeader) {
const { needsAnotherRenderingPass } = await rocketHeader.syncComponents({
@@ -444,7 +452,6 @@ export class Engine {
openGraphHtml: result.openGraphHtml,
});
if (needsAnotherRenderingPass) {
renderOptions.needsLoader = rocketHeader.needsLoader;
renderOptions.throwOnError = true;
result = await renderViaWorker(renderOptions);
} else if (result.passOnError) {

View File

@@ -81,7 +81,6 @@ export class RocketHeader {
/** @type {String[]} */
componentDefinitions = [];
sourceFileContent = '';
needsLoader = false;
/**
*
@@ -123,12 +122,6 @@ export class RocketHeader {
dataCascade.push(line);
}
if (captureComponents === true) {
if (
line.trim() === '// hydrate-able components' ||
line.trim() === '// client-only components'
) {
this.needsLoader = true;
}
componentDefinitions.push(line);
}
@@ -483,6 +476,9 @@ export class RocketHeader {
clientComponents.unshift(' // client-only components');
}
const needsLoader = hydrateAbleComponents.length > 0 || clientComponents.length > 0;
const needsLoaderExport = needsLoader ? ['export const needsLoader = true;'] : [];
if (
serverOnlyComponents.length > 0 ||
serverOnlyOpenGraphOnlyComponents.length > 0 ||
@@ -496,6 +492,7 @@ export class RocketHeader {
...hydrateAbleComponents,
...clientComponents,
'}',
...needsLoaderExport,
];
} else {
this.componentDefinitions = [];
@@ -503,12 +500,11 @@ export class RocketHeader {
await this.set();
const result = await this.save();
if (hydrateAbleComponents.length > 0 || clientComponents.length > 0) {
this.needsLoader = true;
if (needsLoader) {
await this.saveHydrationFile({ outputFilePath, componentsWithStrategy });
} else {
this.needsLoader = false;
}
return result;
}

View File

@@ -35,7 +35,6 @@ let isRendering = '';
* @param {string} options.outputDir
* @param {string} options.inputDir
* @param {string} options.renderMode
* @param {boolean} options.needsLoader
* @param {boolean} options.throwOnError
* @returns {Promise<import('../../types/main.js').renderWorkerResult>}
*/
@@ -44,7 +43,6 @@ export function renderViaWorker({
inputDir,
outputDir,
renderMode = 'development',
needsLoader = false,
throwOnError = false,
}) {
return new Promise((resolve, reject) => {
@@ -92,7 +90,6 @@ export function renderViaWorker({
outputDir,
inputDir,
renderMode,
needsLoader,
throwOnError,
});
});

View File

@@ -24,7 +24,6 @@ import { validateComponentImportString } from '../file-header/validateComponentI
* @param {string} options.sourceFilePath
* @param {string} options.outputDir
* @param {string} options.inputDir
* @param {Boolean} options.needsLoader
* @param {Boolean} options.throwOnError
* @param {'development'|'production'} options.renderMode
*/
@@ -33,7 +32,6 @@ async function renderFile({
outputDir,
inputDir,
renderMode = 'development',
needsLoader = false,
throwOnError = false,
}) {
let fileContent = '';
@@ -144,7 +142,7 @@ async function renderFile({
sourceRelativeFilePath,
outputRelativeFilePath,
url,
needsLoader,
needsLoader: !!data.needsLoader,
});
fileContent = fileContent.trim();
@@ -181,7 +179,7 @@ async function renderFile({
sourceRelativeFilePath,
outputRelativeFilePath: layoutData.openGraphOutputRelativeFilePath,
url: layoutData.openGraphUrl,
needsLoader,
needsLoader: false, // open graph is always only server side rendered
});
openGraphHtml = openGraphHtml.trim();
@@ -223,7 +221,7 @@ async function renderFile({
parentPort?.on('message', message => {
if (message.action === 'renderFile') {
const { sourceFilePath, outputDir, renderMode, inputDir, needsLoader, throwOnError } = message;
renderFile({ sourceFilePath, outputDir, renderMode, inputDir, needsLoader, throwOnError });
const { sourceFilePath, outputDir, renderMode, inputDir, throwOnError } = message;
renderFile({ sourceFilePath, outputDir, renderMode, inputDir, throwOnError });
}
});

View File

@@ -54,6 +54,7 @@ describe('Components', () => {
' // hydrate-able components',
" customElements.define('my-el', await import('@test/components').then(m => m.MyEl));",
'}',
'export const needsLoader = true;',
'/* END - Rocket auto generated - do not touch */',
'',
'export default () => html`<my-el loading="hydrate"></my-el>`;',
@@ -102,6 +103,7 @@ describe('Components', () => {
' // client-only components',
" // 'my-el2': () => import('@test/components').then(m => m.MyEl2),",
'}',
'export const needsLoader = true;',
'/* END - Rocket auto generated - do not touch */',
'',
'export default () => html`<my-el2 loading="client"></my-el2>`;',
@@ -303,6 +305,7 @@ describe('Components', () => {
' // hydrate-able components',
" customElements.define('my-el', await import('@test/components').then(m => m.MyEl));",
'}',
'export const needsLoader = true;',
'/* END - Rocket auto generated - do not touch */',
'',
'export default () => html`<my-el loading="hydrate:onClick"></my-el>`;',
@@ -484,4 +487,112 @@ describe('Components', () => {
].join('\n'),
);
});
it('13: during start it handles errors in components', async () => {
const {
writeSource,
cleanup,
engine,
anEngineEvent,
readOutput,
adjustSource,
setAsOpenedInBrowser,
readSource,
} = await setupTestEngine('fixtures/14-components/13-start-error-in-component/docs');
// working component
await writeSource(
'../src/MyEl.js',
[
"import { LitElement, html } from 'lit';",
'',
'export class MyEl extends LitElement {',
' render() {',
' return html`<p>Hello World</p>`;',
' }',
'}',
].join('\n'),
);
await adjustSource(
'index.rocket.js',
/<my-el loading="hydrate:onVisible">.*?<\/my-el>/,
'<my-el loading="hydrate:onVisible">0</my-el>',
);
await engine.start();
setAsOpenedInBrowser('index.rocket.js');
// trigger render
await adjustSource(
'index.rocket.js',
/<my-el loading="hydrate:onVisible">.*?<\/my-el>/,
'<my-el loading="hydrate:onVisible">1</my-el>',
);
await anEngineEvent('rocketUpdated');
expect(readOutput('index.html')).to.equal(
[
'<my-el loading="hydrate:onVisible"',
' ><template shadowroot="open"><p>Hello World</p></template>1</my-el',
'>',
'<script type="module" src="index-loader-generated.js"></script>',
].join('\n'),
);
expect(readSource('index.rocket.js')).to.equal(
[
'/* START - Rocket auto generated - do not touch */',
"export const sourceRelativeFilePath = 'index.rocket.js';",
"import { html, components } from './recursive.data.js';",
'export { html, components };',
'export async function registerCustomElements() {',
' // hydrate-able components',
" customElements.define('my-el', await import('@test/components').then(m => m.MyEl));",
'}',
'export const needsLoader = true;',
'/* END - Rocket auto generated - do not touch */',
'',
'export default () => html`<my-el loading="hydrate:onVisible">1</my-el>`;',
].join('\n'),
);
// introduce error in component
await writeSource(
'../src/MyEl.js',
[
"import { LitElement, html } from 'lit';",
'',
'export class MyEl extends LitElement {',
' render() {',
' let foo = this.does.not.exist;', // <-- this is the error
' return html`<p>Hello World</p>`;',
' }',
'}',
].join('\n'),
);
await anEngineEvent('rocketUpdated');
expect(readOutput('index.html')).to.include('<p>🛑 Server Error 🛑</p>');
// correct error
await writeSource(
'../src/MyEl.js',
[
"import { LitElement, html } from 'lit';",
'',
'export class MyEl extends LitElement {',
' render() {',
' return html`<p>Hello World</p>`;',
' }',
'}',
].join('\n'),
);
await anEngineEvent('rocketUpdated');
expect(readOutput('index.html')).to.equal(
[
'<my-el loading="hydrate:onVisible"',
' ><template shadowroot="open"><p>Hello World</p></template>1</my-el',
'>',
'<script type="module" src="index-loader-generated.js"></script>',
].join('\n'),
);
await cleanup();
});
});

View File

@@ -4,5 +4,7 @@
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0
"level": 0,
"title": "Unexpected identifier",
"h1": "Unexpected identifier"
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`<my-el loading="hydrate"></my-el>`;

View File

@@ -4,5 +4,6 @@
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0
"level": 0,
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// client-only components
// 'my-el2': () => import('@test/components').then(m => m.MyEl2),
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`<my-el2 loading="client"></my-el2>`;

View File

@@ -4,5 +4,6 @@
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0
"level": 0,
"needsLoader": true
}

View File

@@ -8,6 +8,7 @@ export async function registerCustomElements() {
// client-only components
// 'my-only': () => import('@test/components/MyOnly').then(m => m.MyOnly),
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`

View File

@@ -4,5 +4,6 @@
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0
"level": 0,
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`<my-el loading="hydrate:onClick"></my-el>`;

View File

@@ -4,5 +4,6 @@
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0
"level": 0,
"needsLoader": true
}

View File

@@ -4,5 +4,6 @@
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0
"level": 0,
"needsLoader": true
}

View File

@@ -0,0 +1,12 @@
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = 'index.rocket.js';
import { html, components } from './recursive.data.js';
export { html, components };
export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`<my-el loading="hydrate:onVisible">1</my-el>`;

View File

@@ -0,0 +1,11 @@
{
"name": "index.rocket.js",
"menuLinkText": "index.rocket.js",
"url": "/",
"outputRelativeFilePath": "index.html",
"sourceRelativeFilePath": "index.rocket.js",
"level": 0,
"title": "Cannot read properties of undefined (reading 'not')",
"h1": "Cannot read properties of undefined (reading 'not')",
"needsLoader": true
}

View File

@@ -0,0 +1,5 @@
export { html } from 'lit';
export const components = {
'my-el': '@test/components::MyEl',
};

View File

@@ -0,0 +1,7 @@
{
"name": "@test/components",
"type": "module",
"exports": {
".": "./src/MyEl.js"
}
}

View File

@@ -0,0 +1,7 @@
import { LitElement, html } from 'lit';
export class MyEl extends LitElement {
render() {
return html`<p>Hello World</p>`;
}
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`<my-el loading="hydrate:onClientLoad"></my-el>`;

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`<my-el loading="hydrate:onClick"></my-el>`;

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html` <my-el loading="hydrate:onMedia('(max-width: 320px)')"></my-el> `;

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html`

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}

View File

@@ -6,6 +6,7 @@ export async function registerCustomElements() {
// hydrate-able components
customElements.define('my-el', await import('@test/components').then(m => m.MyEl));
}
export const needsLoader = true;
/* END - Rocket auto generated - do not touch */
export default () => html` <my-el loading="hydrate:onFocus"></my-el> `;

View File

@@ -7,5 +7,6 @@
"level": 0,
"components": {
"my-el": "() => import('@test/components').then(m => m.MyEl)"
}
},
"needsLoader": true
}