Skip to content

Commit

Permalink
Feature/issue 1008 adapter plugin API (#1122)
Browse files Browse the repository at this point in the history
* create adapter plugin API

* add docs for adapter plugin API
  • Loading branch information
thescientist13 authored Jul 23, 2023
1 parent ab09626 commit 137a4c9
Show file tree
Hide file tree
Showing 19 changed files with 369 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ coverage/
node_modules/
packages/**/test/**/yarn.lock
packages/**/test/**/package-lock.json
public/
public/
adapter-outlet/
7 changes: 7 additions & 0 deletions packages/cli/src/commands/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const runProductionBuild = async (compilation) => {
const prerenderPlugin = compilation.config.plugins.find(plugin => plugin.type === 'renderer')
? compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider(compilation)
: {};
const adapterPlugin = compilation.config.plugins.find(plugin => plugin.type === 'adapter')
? compilation.config.plugins.find(plugin => plugin.type === 'adapter').provider(compilation)
: null;

if (!await checkResourceExists(outputDir)) {
await fs.mkdir(outputDir, {
Expand Down Expand Up @@ -114,6 +117,10 @@ const runProductionBuild = async (compilation) => {
await bundleCompilation(compilation);
await copyAssets(compilation);

if (adapterPlugin) {
await adapterPlugin();
}

resolve();
} catch (err) {
reject(err);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lifecycles/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const greenwoodPlugins = (await Promise.all([
});

const optimizations = ['default', 'none', 'static', 'inline'];
const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source', 'renderer'];
const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source', 'renderer', 'adapter'];
const defaultConfig = {
workspace: new URL('./src/', cwd),
devServer: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Use Case
* Run Greenwood with a custom adapter plugin with SSR pages and API routes.
*
* User Result
* Should generate a Greenwood build with expected transformations applied from the plugin.
*
* User Command
* greenwood build
*
* User Config
* async function genericAdapter(compilation, options) { ... }
*
* {
* plugins: [{
* type: 'adapter',
* name: 'plugin-generic-adapter',
* provider: (compilation, options) => genericAdapter
* }]
* }
*
* Custom Workspace
* src/
* api/
* greeting.js
* pages/
* index.js
* components/
* card.js
*/
import chai from 'chai';
import path from 'path';
import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import { Runner } from 'gallinago';
import { fileURLToPath, pathToFileURL } from 'url';
import { JSDOM } from 'jsdom';

const expect = chai.expect;

describe('Build Greenwood With: ', function() {
const LABEL = 'Generic Adapter Plugin with SSR Pages + API Routes';
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(async function() {
await runner.setup(outputPath, getSetupFiles(outputPath));
await runner.runCommand(cliPath, 'build');
});

// test SSR page
describe('Adapting an SSR Page', function() {
let dom;

before(async function() {
const req = new Request(new URL('http://localhost:8080/index'));
const { handler } = await import(new URL('./adapter-output/index.js', pathToFileURL(outputPath)));
const response = await handler(req);
const html = await response.text();

dom = new JSDOM(html);
});

it('should have the expected number of <app-card> components on the page', function() {
const cards = dom.window.document.querySelectorAll('body > app-card');

expect(cards).to.have.lengthOf(1);
});

it('should have the expected static heading content rendered inside the <app-card> component on the page', function() {
const heading = dom.window.document.querySelectorAll('app-card h2');

expect(heading).to.have.lengthOf(1);
expect(heading[0].textContent).to.be.equal('Analog');
});

it('should have the expected static img content rendered inside the <app-card> component on the page', function() {
const img = dom.window.document.querySelectorAll('app-card img');

expect(img).to.have.lengthOf(1);
expect(img[0].getAttribute('src')).to.be.equal('https://www.analogstudios.net/images/analog.png');
});
});

describe('Adapting an API Route', function() {
let data;

before(async function() {
const handler = (await import(new URL('./adapter-output/greeting.js', pathToFileURL(outputPath)))).handler;
const req = new Request(new URL('http://localhost:8080/api/greeting?name=Greenwood'));
const res = await handler(req);

data = await res.json();
});

it('should have the expected message from the API when a query is passed', function() {
expect(data.message).to.be.equal('Hello Greenwood!');
});
});
});

after(function() {
runner.teardown([
...getOutputTeardownFiles(outputPath),
path.join(outputPath, 'adapter-output')
]);
});

});
60 changes: 60 additions & 0 deletions packages/cli/test/cases/build.plugins.adapter/generic-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from 'fs/promises';
import { checkResourceExists } from '../../../../cli/src/lib/resource-utils.js';

function generateOutputFormat(id, type) {
const path = type === 'page'
? `__${id}`
: `api/${id}`;

return `
import { handler as ${id} } from '../public/${path}.js';
export async function handler (request) {
const { url, headers } = request;
const req = new Request(new URL(url, \`http://\${headers.host}\`), {
headers: new Headers(headers)
});
return await ${id}(req);
}
`;
}

async function genericAdapter(compilation) {
const adapterOutputUrl = new URL('./adapter-output/', compilation.context.projectDirectory);
const ssrPages = compilation.graph.filter(page => page.isSSR);
const apiRoutes = compilation.manifest.apis;

if (!await checkResourceExists(adapterOutputUrl)) {
await fs.mkdir(adapterOutputUrl);
}

console.log({ ssrPages, apiRoutes });

for (const page of ssrPages) {
const { id } = page;
const outputFormat = generateOutputFormat(id, 'page');

await fs.writeFile(new URL(`./${id}.js`, adapterOutputUrl), outputFormat);
}

// public/api/
for (const [key] of apiRoutes) {
const id = key.replace('/api/', '');
const outputFormat = generateOutputFormat(id, 'api');

await fs.writeFile(new URL(`./${id}.js`, adapterOutputUrl), outputFormat);
}
}

const greenwoodPluginAdapterGeneric = (options = {}) => [{
type: 'adapter',
name: 'plugin-adapter-generic',
provider: (compilation) => {
return async () => {
await genericAdapter(compilation, options);
};
}
}];

export { greenwoodPluginAdapterGeneric };
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { greenwoodPluginAdapterGeneric } from './generic-adapter.js';

export default {
plugins: [
greenwoodPluginAdapterGeneric()
]
};
12 changes: 12 additions & 0 deletions packages/cli/test/cases/build.plugins.adapter/src/api/greeting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export async function handler(request) {
const params = new URLSearchParams(request.url.slice(request.url.indexOf('?')));
const name = params.has('name') ? params.get('name') : 'World';
const body = { message: `Hello ${name}!` };
const headers = new Headers();

headers.append('Content-Type', 'application/json');

return new Response(JSON.stringify(body), {
headers
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default class Card extends HTMLElement {

selectArtist() {
alert(`selected artist is => ${this.getAttribute('title')}!`);
}

connectedCallback() {
const thumbnail = this.getAttribute('thumbnail');
const title = this.getAttribute('title');

this.innerHTML = `
<div>
<h2>${title}</h2>
<button onclick="this.parentNode.parentNode.selectArtist()">View Artist Details</button>
<img src="${thumbnail}" loading="lazy" width="50%">
<hr/>
</div>
`;
}
}

customElements.define('app-card', Card);
24 changes: 24 additions & 0 deletions packages/cli/test/cases/build.plugins.adapter/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import '../components/card.js';

export default class ArtistsPage extends HTMLElement {
async connectedCallback() {
const artists = [{ name: 'Analog', imageUrl: 'https://www.analogstudios.net/images/analog.png' }];
const html = artists.map(artist => {
const { name, imageUrl } = artist;

return `
<app-card
title="${name}"
thumbnail="${imageUrl}"
>
</app-card>
`;
}).join('');

this.innerHTML = `
<a href="/">&lt; Back</a>
<h1>List of Artists: ${artists.length}</h1>
${html}
`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('Build Greenwood With: ', function() {

describe('Custom Configuration with a bad value for plugin type', function() {
it('should throw an error that plugin.type is not a valid value', async function() {
const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source', 'renderer'];
const pluginTypes = ['copy', 'context', 'resource', 'rollup', 'server', 'source', 'renderer', 'adapter'];

try {
await runner.setup(outputPath);
Expand Down
Loading

0 comments on commit 137a4c9

Please sign in to comment.