Skip to content

Commit

Permalink
custom elements as layouts
Browse files Browse the repository at this point in the history
  • Loading branch information
thescientist13 committed May 19, 2024
1 parent 31f8f35 commit d5fb5c6
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 33 deletions.
102 changes: 88 additions & 14 deletions packages/cli/src/lib/layout-utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import fs from 'fs/promises';
import htmlparser from 'node-html-parser';
import { checkResourceExists } from './resource-utils.js';
import { Worker } from 'worker_threads';

async function getCustomPageLayoutsFromPlugins(compilation, layoutName) {
// TODO confirm context plugins work for SSR
// TODO support context plugins for more than just HTML files
const contextPlugins = compilation.config.plugins.filter((plugin) => {
return plugin.type === 'context';
}).map((plugin) => {
return plugin.provider(compilation);
});

async function getCustomPageLayoutsFromPlugins(contextPlugins, layoutName) {
const customLayoutLocations = [];
const layoutDir = contextPlugins
.map(plugin => plugin.layouts)
Expand All @@ -21,19 +30,21 @@ async function getCustomPageLayoutsFromPlugins(contextPlugins, layoutName) {
return customLayoutLocations;
}

async function getPageLayout(filePath, context, layout, contextPlugins = []) {
async function getPageLayout(filePath, compilation, layout) {
const { context } = compilation;
const { layoutsDir, userLayoutsDir, pagesDir, projectDirectory } = context;
const customPluginDefaultPageLayouts = await getCustomPageLayoutsFromPlugins(contextPlugins, 'page');
const customPluginPageLayouts = await getCustomPageLayoutsFromPlugins(contextPlugins, layout);
const customPluginDefaultPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, 'page');
const customPluginPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, layout);
const extension = filePath.split('.').pop();
const is404Page = filePath.startsWith('404') && extension === 'html';
const hasCustomLayout = await checkResourceExists(new URL(`./${layout}.html`, userLayoutsDir));
const hasCustomStaticLayout = await checkResourceExists(new URL(`./${layout}.html`, userLayoutsDir));
const hasCustomDynamicLayout = await checkResourceExists(new URL(`./${layout}.js`, userLayoutsDir));
const hasPageLayout = await checkResourceExists(new URL('./page.html', userLayoutsDir));
const hasCustom404Page = await checkResourceExists(new URL('./404.html', pagesDir));
const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(`./${filePath}`, projectDirectory));
let contents;

if (layout && (customPluginPageLayouts.length > 0 || hasCustomLayout)) {
if (layout && (customPluginPageLayouts.length > 0 || hasCustomStaticLayout)) {
// use a custom layout, usually from markdown frontmatter
contents = customPluginPageLayouts.length > 0
? await fs.readFile(new URL(`./${layout}.html`, customPluginPageLayouts[0]), 'utf-8')
Expand All @@ -47,6 +58,33 @@ async function getPageLayout(filePath, context, layout, contextPlugins = []) {
contents = customPluginDefaultPageLayouts.length > 0
? await fs.readFile(new URL('./page.html', customPluginDefaultPageLayouts[0]), 'utf-8')
: await fs.readFile(new URL('./page.html', userLayoutsDir), 'utf-8');
} else if (hasCustomDynamicLayout && !is404Page) {
const routeModuleLocationUrl = new URL(`./${layout}.js`, userLayoutsDir);
const routeWorkerUrl = compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl;

await new Promise(async (resolve, reject) => {
const worker = new Worker(new URL('./ssr-route-worker.js', import.meta.url));

worker.on('message', (result) => {

if (result.body) {
contents = result.body;
}
resolve();
});
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});

worker.postMessage({
executeModuleUrl: routeWorkerUrl.href,
moduleUrl: routeModuleLocationUrl.href,
compilation: JSON.stringify(compilation)
});
});
} else if (is404Page && !hasCustom404Page) {
contents = await fs.readFile(new URL('./404.html', layoutsDir), 'utf-8');
} else {
Expand All @@ -58,16 +96,52 @@ async function getPageLayout(filePath, context, layout, contextPlugins = []) {
}

/* eslint-disable-next-line complexity */
async function getAppLayout(pageLayoutContents, context, customImports = [], contextPlugins, enableHud, frontmatterTitle) {
const { layoutsDir, userLayoutsDir } = context;
const userAppLayoutUrl = new URL('./app.html', userLayoutsDir);
const customAppLayoutsFromPlugins = await getCustomPageLayoutsFromPlugins(contextPlugins, 'app');
const hasCustomUserAppLayout = await checkResourceExists(userAppLayoutUrl);
async function getAppLayout(pageLayoutContents, compilation, customImports = [], frontmatterTitle) {
const enableHud = compilation.config.devServer.hud;
const { layoutsDir, userLayoutsDir } = compilation.context;
const userStaticAppLayoutUrl = new URL('./app.html', userLayoutsDir);
// TODO support more than just .js files
const userDynamicAppLayoutUrl = new URL('./app.js', userLayoutsDir);
const userHasStaticAppLayout = await checkResourceExists(userStaticAppLayoutUrl);
const userHasDynamicAppLayout = await checkResourceExists(userDynamicAppLayoutUrl);
const customAppLayoutsFromPlugins = await getCustomPageLayoutsFromPlugins(compilation, 'app');
let dynamicAppLayoutContents;

if (userHasDynamicAppLayout) {
const routeWorkerUrl = compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl;

await new Promise(async (resolve, reject) => {
const worker = new Worker(new URL('./ssr-route-worker.js', import.meta.url));

worker.on('message', (result) => {

if (result.body) {
dynamicAppLayoutContents = result.body;
}
resolve();
});
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});

worker.postMessage({
executeModuleUrl: routeWorkerUrl.href,
moduleUrl: userDynamicAppLayoutUrl.href,
compilation: JSON.stringify(compilation)
});
});
}

let appLayoutContents = customAppLayoutsFromPlugins.length > 0
? await fs.readFile(new URL('./app.html', customAppLayoutsFromPlugins[0]))
: hasCustomUserAppLayout
? await fs.readFile(userAppLayoutUrl, 'utf-8')
: await fs.readFile(new URL('./app.html', layoutsDir), 'utf-8');
: userHasStaticAppLayout
? await fs.readFile(userStaticAppLayoutUrl, 'utf-8')
: userHasDynamicAppLayout
? dynamicAppLayoutContents
: await fs.readFile(new URL('./app.html', layoutsDir), 'utf-8');
let mergedLayoutContents = '';

const pageRoot = pageLayoutContents && htmlparser.parse(pageLayoutContents, {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/ssr-route-worker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// https://github.com/nodejs/modules/issues/307#issuecomment-858729422
import { parentPort } from 'worker_threads';

async function executeModule({ executeModuleUrl, moduleUrl, compilation, page, prerender = false, htmlContents = null, scripts = '[]', request }) {
async function executeModule({ executeModuleUrl, moduleUrl, compilation = '{}', page = '{}', prerender = false, htmlContents = null, scripts = '[]', request }) {
const { executeRouteModule } = await import(executeModuleUrl);
const data = await executeRouteModule({ moduleUrl, compilation: JSON.parse(compilation), page: JSON.parse(page), prerender, htmlContents, scripts: JSON.parse(scripts), request });

Expand Down
11 changes: 2 additions & 9 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,6 @@ async function bundleApiRoutes(compilation) {
}

async function bundleSsrPages(compilation, optimizePlugins) {
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api
// TODO context plugins for SSR ?
// const contextPlugins = compilation.config.plugins.filter((plugin) => {
// return plugin.type === 'context';
// }).map((plugin) => {
// return plugin.provider(compilation);
// });
const { context, config } = compilation;
const ssrPages = compilation.graph.filter(page => page.isSSR && !page.prerender);
const ssrPrerenderPagesRouteMapper = {};
Expand All @@ -227,8 +220,8 @@ async function bundleSsrPages(compilation, optimizePlugins) {
const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [], request });
let staticHtml = '';

staticHtml = data.layout ? data.layout : await getPageLayout(staticHtml, compilation.context, layout, []);
staticHtml = await getAppLayout(staticHtml, context, imports, [], false, title);
staticHtml = data.layout ? data.layout : await getPageLayout(staticHtml, compilation, layout);
staticHtml = await getAppLayout(staticHtml, compilation, imports, title);
staticHtml = await getUserScripts(staticHtml, compilation);
staticHtml = await (await interceptPage(new URL(`http://localhost:8080${route}`), new Request(new URL(`http://localhost:8080${route}`)), getPluginInstances(compilation), staticHtml)).text();

Expand Down
11 changes: 2 additions & 9 deletions packages/cli/src/plugins/resource/plugin-standard-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,13 @@ class StandardHtmlResource extends ResourceInterface {
});
}

// get context plugins
const contextPlugins = this.compilation.config.plugins.filter((plugin) => {
return plugin.type === 'context';
}).map((plugin) => {
return plugin.provider(this.compilation);
});

if (isSpaRoute) {
body = await fs.readFile(new URL(`./${isSpaRoute.filename}`, userWorkspace), 'utf-8');
} else {
body = ssrLayout ? ssrLayout : await getPageLayout(filePath, context, layout, contextPlugins);
body = ssrLayout ? ssrLayout : await getPageLayout(filePath, this.compilation, layout);
}

body = await getAppLayout(body, context, customImports, contextPlugins, config.devServer.hud, title);
body = await getAppLayout(body, this.compilation, customImports, title);
body = await getUserScripts(body, this.compilation);

if (processedMarkdown) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Use Case
* Run Greenwood build command with no config and dynamic (e.g. .js) custom page (and app) layouts.
*
* User Result
* Should generate a bare bones Greenwood build with custom page layout.
*
* User Command
* greenwood build
*
* User Config
* None (Greenwood Default)
*
* User Workspace
* src/
* pages/
* index.md
* layouts/
* app.js
* page.js
*/
import chai from 'chai';
import { JSDOM } from 'jsdom';
import path from 'path';
import { runSmokeTest } from '../../../../../test/smoke-test.js';
import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import { Runner } from 'gallinago';
import { fileURLToPath, URL } from 'url';

const expect = chai.expect;

describe('Build Greenwood With: ', function() {
const LABEL = 'Default Greenwood Configuration and Workspace w/Custom Dynamic App and Page Layouts using JavaScript';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
let runner;

before(function() {
this.context = {
publicDir: path.join(outputPath, 'public')
};
runner = new Runner();
});

describe(LABEL, function() {
before(function() {
runner.setup(outputPath, getSetupFiles(outputPath));
runner.runCommand(cliPath, 'build');
});

runSmokeTest(['public'], LABEL);

describe('Custom App and Page Layout', function() {
let dom;

before(async function() {
dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html'));
});

it('should have the expected <title> tag from the dynamic app layout', function() {
const title = dom.window.document.querySelectorAll('head title');

expect(title.length).to.equal(1);
expect(title[0].textContent).to.equal('App Layout');
});

it('should have the expected <h1> tag from the dynamic app layout', function() {
const heading = dom.window.document.querySelectorAll('h1');

expect(heading.length).to.equal(1);
expect(heading[0].textContent).to.equal('App Layout');
});

it('should have the expected <h2> tag from the dynamic page layout', function() {
const heading = dom.window.document.querySelectorAll('h2');

expect(heading.length).to.equal(1);
expect(heading[0].textContent).to.equal('Page Layout');
});

it('should have the expected content from the index.md', function() {
const heading = dom.window.document.querySelectorAll('h3');
const paragraph = dom.window.document.querySelectorAll('p');

expect(heading.length).to.equal(1);
expect(heading[0].textContent).to.equal('Home Page');

expect(paragraph.length).to.equal(1);
expect(paragraph[0].textContent).to.equal('Coffey was here');
});

it('should have the expected <footer> tag from the dynamic app layout', function() {
const year = new Date().getFullYear();
const footer = dom.window.document.querySelectorAll('footer');

expect(footer.length).to.equal(1);
expect(footer[0].textContent).to.equal(`${year}`);
});
});
});

after(function() {
runner.teardown(getOutputTeardownFiles(outputPath));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default class AppLayout extends HTMLElement {
async connectedCallback() {
const year = new Date().getFullYear();

this.innerHTML = `
<!DOCTYPE html>
<html>
<head>
<title>App Layout</title>
</head>
<body>
<h1>App Layout</h1>
<page-outlet></page-outlet>
<footer>${year}</footer>
</body>
</html>
`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default class PageLayout extends HTMLElement {
async connectedCallback() {
this.innerHTML = `
<body>
<h2>Page Layout</h2>
<content-outlet></content-outlet>
</body>
`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Home Page

Coffey was here

0 comments on commit d5fb5c6

Please sign in to comment.