Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug/issue 1223 support nested pages and API for and adapters #1237

Merged
111 changes: 73 additions & 38 deletions packages/cli/src/config/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,34 @@
};
}

function greenwoodSyncApiRoutesOutputPath(compilation) {
return {
name: 'greenwood-sync-api-routes-output-paths',
generateBundle(options, bundle) {
const { basePath } = compilation.config;
const { apisDir } = compilation.context;

// map rollup bundle names back to original SSR pages for syncing input <> output bundle names
Object.keys(bundle).forEach((key) => {
if (bundle[key].exports?.find(exp => exp === 'handler')) {
const ext = bundle[key].facadeModuleId.split('.').pop();
const relativeFacade = new URL(`file://${bundle[key].facadeModuleId}`).pathname.replace(apisDir.pathname, `${basePath}/`).replace(`.${ext}`, '');
const route = `/api${relativeFacade}`;

if (compilation.manifest.apis.has(route)) {
const api = compilation.manifest.apis.get(route);

compilation.manifest.apis.set(route, {
...api,
outputPath: `/api/${key}`
});
}
}
});
}
};
}

function getMetaImportPath(node) {
return node.arguments[0].value.split('/').join(path.sep)
.replace(/\\/g, '/'); // handle Windows style paths
Expand Down Expand Up @@ -321,7 +349,7 @@
}
}
} else {
// TODO figure out how to handle URL chunk from SSR pages

Check warning on line 352 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO figure out how to handle URL chunk...'

Check warning on line 352 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO figure out how to handle URL chunk...'

Check warning on line 352 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO figure out how to handle URL chunk...'

Check warning on line 352 in packages/cli/src/config/rollup.config.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO figure out how to handle URL chunk...'
// https://github.com/ProjectEvergreen/greenwood/issues/1163
}

Expand Down Expand Up @@ -429,50 +457,57 @@
};

const getRollupConfigForApis = async (compilation) => {
const { outputDir, pagesDir } = compilation.context;
const { outputDir, pagesDir, apisDir } = compilation.context;

return [...compilation.manifest.apis.values()]
.map(api => normalizePathnameForWindows(new URL(`.${api.path}`, pagesDir)))
.map(filepath => ({
input: filepath,
output: {
dir: `${normalizePathnameForWindows(outputDir)}/api`,
entryFileNames: '[name].js',
chunkFileNames: '[name].[hash].js'
},
plugins: [
greenwoodResourceLoader(compilation),
// support node export conditions for SSR pages
// https://github.com/ProjectEvergreen/greenwood/issues/1118
// https://github.com/rollup/plugins/issues/362#issuecomment-873448461
nodeResolve({
exportConditions: ['node'],
preferBuiltins: true
}),
commonjs(),
greenwoodImportMetaUrl(compilation)
],
onwarn: (errorObj) => {
const { code, message } = errorObj;
.map((filepath) => {
// account for windows pathname shenanigans by "casting" filepath to a URL first
const ext = filepath.split('.').pop();
const entryName = new URL(`file://${filepath}`).pathname.replace(apisDir.pathname, '').replace(/\//g, '-').replace(`.${ext}`, '');

switch (code) {

case 'CIRCULAR_DEPENDENCY':
// let this through for WCC + sucrase
// Circular dependency: ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js ->
// ../../../../../node_modules/sucrase/dist/esm/parser/traverser/util.js -> ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js
// Circular dependency: ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js ->
// ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/readWord.js -> ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js
// https://github.com/ProjectEvergreen/greenwood/pull/1212
// https://github.com/lit/lit/issues/449#issuecomment-416688319
break;
default:
// otherwise, log all warnings from rollup
console.debug(message);
return {
input: filepath,
output: {
dir: `${normalizePathnameForWindows(outputDir)}/api`,
entryFileNames: `${entryName}.js`,
chunkFileNames: `${entryName}.[hash].js`
},
plugins: [
greenwoodResourceLoader(compilation),
// support node export conditions for SSR pages
// https://github.com/ProjectEvergreen/greenwood/issues/1118
// https://github.com/rollup/plugins/issues/362#issuecomment-873448461
nodeResolve({
exportConditions: ['node'],
preferBuiltins: true
}),
commonjs(),
greenwoodImportMetaUrl(compilation),
greenwoodSyncApiRoutesOutputPath(compilation)
],
onwarn: (errorObj) => {
const { code, message } = errorObj;

switch (code) {

case 'CIRCULAR_DEPENDENCY':
// let this through for WCC + sucrase
// Circular dependency: ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js ->
// ../../../../../node_modules/sucrase/dist/esm/parser/traverser/util.js -> ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js
// Circular dependency: ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js ->
// ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/readWord.js -> ../../../../../node_modules/sucrase/dist/esm/parser/tokenizer/index.js
// https://github.com/ProjectEvergreen/greenwood/pull/1212
// https://github.com/lit/lit/issues/449#issuecomment-416688319
break;
default:
// otherwise, log all warnings from rollup
console.debug(message);

}
}
}
}));
};
});
};

const getRollupConfigForSsr = async (compilation, input) => {
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ function getPluginInstances(compilation) {
}

async function emitResources(compilation) {
const { outputDir } = compilation.context;
const { resources, graph } = compilation;
const { outputDir, scratchDir } = compilation.context;
const { resources, graph, manifest } = compilation;

// https://stackoverflow.com/a/56150320/417806
await fs.writeFile(new URL('./resources.json', outputDir), JSON.stringify(resources, (key, value) => {
Expand All @@ -49,6 +49,17 @@ async function emitResources(compilation) {
}
}));

await fs.writeFile(new URL('./manifest.json', scratchDir), JSON.stringify(manifest, (key, value) => {
if (value instanceof Map) {
return {
dataType: 'Map',
value: [...value]
};
} else {
return value;
}
}));

await fs.writeFile(new URL('./graph.json', outputDir), JSON.stringify(graph));
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lifecycles/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const generateGraph = async (compilation) => {
*/
apiRoutes.set(route, {
filename: filename,
outputPath: `/api/${filename.replace(`.${extension}`, '.js')}`,
outputPath: relativeApiPath,
path: relativeApiPath,
route,
isolation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
* card.js
* pages/
* api/
* nested/
* endpoint.js
* greeting.js
* blog/
* first-post.js
* index.js
* index.js
*/
import chai from 'chai';
Expand Down Expand Up @@ -98,7 +103,7 @@ describe('Build Greenwood With: ', function() {
let response;

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

response = await handler(req);
Expand All @@ -113,6 +118,75 @@ describe('Build Greenwood With: ', function() {
expect(data.message).to.be.equal('Hello Greenwood!');
});
});

describe('Adapting a nested API Route', function() {
let response;
let body;

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

response = await handler(req);
body = await response.text();
});

it('should have the expected content-type for the response', function() {
expect(response.headers.get('content-type')).to.be.equal('text/html');
});

it('should have the expected message from the API when a query is passed', function() {
expect(body).to.be.equal('I am a nested API route!');
});
});

describe('Adapting a nested SSR Page (duplicate name)', function() {
let dom;
let response;

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

response = await handler(req);
dom = new JSDOM(await response.text());
});

it('should have the expected content-type for the response', function() {
expect(response.headers.get('content-type')).to.be.equal('text/html');
});

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

expect(heading).to.have.lengthOf(1);
expect(heading[0].textContent).to.equal('Duplicated and nested SSR page should work!');
});
});

describe('Adapting a nested SSR Page', function() {
let dom;
let response;

before(async function() {
const req = new Request(new URL('http://localhost:8080/blog/first-post/'));
const handler = (await import(new URL('./adapter-output/blog-first-post.js', pathToFileURL(outputPath)))).handler;

response = await handler(req);
dom = new JSDOM(await response.text());
});

it('should have the expected content-type for the response', function() {
expect(response.headers.get('content-type')).to.be.equal('text/html');
});

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

expect(heading).to.have.lengthOf(1);
expect(heading[0].textContent).to.equal('Nested SSR First Post page should work!');
});
});
});

after(function() {
Expand Down
19 changes: 10 additions & 9 deletions packages/cli/test/cases/build.plugins.adapter/generic-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { checkResourceExists } from '../../../../cli/src/lib/resource-utils.js';

function generateOutputFormat(id, type) {
const path = type === 'page'
? `${id}.route`
: `api/${id}`;
? `/${id}.route`
: id;
const ref = id.replace(/-/g, '').replace(/\//g, '');

return `
import { handler as ${id} } from '../public/${path}.js';
import { handler as ${ref} } 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);
return await ${ref}(req);
}
`;
}
Expand All @@ -30,18 +31,18 @@ async function genericAdapter(compilation) {
}

for (const page of ssrPages) {
const { id } = page;
const { outputPath } = page;
const id = outputPath.replace('.route.js', '');
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');
const { outputPath } = apiRoutes.get(key);
const outputFormat = generateOutputFormat(outputPath.replace('.js', ''), 'api');

await fs.writeFile(new URL(`./${id}.js`, adapterOutputUrl), outputFormat);
await fs.writeFile(new URL(`.${outputPath.replace('/api/', '/api-')}`, adapterOutputUrl), outputFormat);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export async function handler() {
return new Response('I am a nested API route!', {
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default class BlogFirstPostPage extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<h1>Nested SSR First Post page should work!</h1>
`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default class BlogPage extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<h1>Duplicated and nested SSR page should work!</h1>
`;
}
}
41 changes: 34 additions & 7 deletions packages/cli/test/cases/develop.default/develop.default.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
* header.js
* pages/
* api/
* fragment.js
* greeting.js
* missing.js
* nothing.js
* submit-form-data.js
* submit-json.js
* index.html
* nested/
* endpoint.js
* fragment.js
* greeting.js
* missing.js
* nothing.js
* submit-form-data.js
* submit-json.js
* index.html
* styles/
* main.css
* package.json
Expand Down Expand Up @@ -1288,6 +1290,31 @@ describe('Develop Greenwood With: ', function() {
});
});

describe('Develop command nested API specific behaviors', function() {
let response = {};
let body;

before(async function() {
response = await fetch(`${hostname}:${port}/api/nested/endpoint`);
body = await response.clone().text();
});

it('should return a 200 status', function(done) {
expect(response.status).to.equal(200);
done();
});

it('should return the expected content type header', function(done) {
expect(response.headers.get('content-type')).to.equal('text/html');
done();
});

it('should return the expected response message', function(done) {
expect(body).to.contain('I am a nested API route');
done();
});
});

describe('Fetching graph.json client side', function() {
let response;
let graph;
Expand Down
Loading
Loading