Skip to content

Commit

Permalink
Bug/issue 1064 restore spa fallback handling for client side routing (#…
Browse files Browse the repository at this point in the history
…1065)

* restore SPA fallback handling for client side rendering

* add test cases to cover regressions
  • Loading branch information
thescientist13 committed Feb 18, 2023
1 parent 610a810 commit cb62091
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 6 deletions.
2 changes: 1 addition & 1 deletion packages/cli/src/lib/resource-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function resolveForRelativeUrl(url, rootUrl) {
const segments = url.pathname.split('/').filter(segment => segment !== '');
segments.shift();

for (let i = 0, l = segments.length - 1; i < l; i += 1) {
for (let i = 0, l = segments.length; i < l; i += 1) {
const nextSegments = segments.slice(i);
const urlToCheck = new URL(`./${nextSegments.join('/')}`, rootUrl);

Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/lifecycles/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,14 @@ async function getStaticServer(compilation, composable) {
try {
const url = new URL(`http://localhost:8080${ctx.url}`);
const matchingRoute = compilation.graph.find(page => page.route === url.pathname);

if ((matchingRoute && !matchingRoute.isSSR) || url.pathname.split('.').pop() === 'html') {
const pathname = matchingRoute ? matchingRoute.outputPath : url.pathname;
const isSPA = compilation.graph.find(page => page.isSPA);

if (isSPA || (matchingRoute && !matchingRoute.isSSR) || url.pathname.split('.').pop() === 'html') {
const pathname = isSPA
? 'index.html'
: matchingRoute
? matchingRoute.outputPath
: url.pathname;
const body = await fs.readFile(new URL(`./${pathname}`, outputDir), 'utf-8');

ctx.set('Content-Type', 'text/html');
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/plugins/resource/plugin-standard-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ class StandardHtmlResource extends ResourceInterface {
async shouldServe(url) {
const { protocol, pathname } = url;
const hasMatchingPageRoute = this.compilation.graph.find(node => node.route === pathname);
const isSPA = this.compilation.graph.find(node => node.isSPA) && pathname.indexOf('.') < 0;

return protocol.startsWith('http') && (hasMatchingPageRoute || this.compilation.graph[0].isSPA);
return protocol.startsWith('http') && (hasMatchingPageRoute || isSPA);
}

async serve(url) {
Expand Down
40 changes: 39 additions & 1 deletion packages/cli/test/cases/develop.spa/develop.spa.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* User Workspace
* src/
* index.html
* main.css
*
*/
import chai from 'chai';
Expand Down Expand Up @@ -189,8 +190,45 @@ describe('Develop Greenwood With: ', function() {
});
});

// https://github.com/ProjectEvergreen/greenwood/issues/1064
describe('Develop command specific workspace resolution behavior that does not think its a client side route', function() {
let response = {};

before(async function() {
return new Promise((resolve, reject) => {
request.get({
url: `http://127.0.0.1:${port}/events/main.css`
}, (err, res) => {
if (err) {
reject();
}

response = res;

resolve();
});
});
});

it('should return the correct content type', function(done) {
expect(response.headers['content-type']).to.contain('text/css');
done();
});

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

done();
});

it('should return the expected body contents', function(done) {
expect(response.body.replace(/\n/g, '').indexOf('* { color: red;}')).to.equal(0);
done();
});
});

// https://github.com/ProjectEvergreen/greenwood/issues/803
describe('Develop command specific node modules resolution behavior that doesnt think its a client side route', function() {
describe('Develop command specific node modules resolution behavior that does not think its a client side route', function() {
let response = {};

before(async function() {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/test/cases/develop.spa/src/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* {
color: red;
}
192 changes: 192 additions & 0 deletions packages/cli/test/cases/serve.spa/serve.spa.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Use Case
* Run Greenwood serve command for SPA based project.
*
* User Result
* Should start the development server for a SPA with client side routing support.
*
* User Command
* greenwood develop
*
* User Config
* {}
*
* User Workspace
* src/
* index.html
* main.css
*
*/
import chai from 'chai';
import fs from 'fs';
import { getOutputTeardownFiles } from '../../../../../test/utils.js';
import path from 'path';
import request from 'request';
import { runSmokeTest } from '../../../../../test/smoke-test.js';
import { Runner } from 'gallinago';
import { fileURLToPath, URL } from 'url';

const expect = chai.expect;

function removeWhiteSpace(string = '') {
return string
.replace(/\n/g, '')
.replace(/ /g, '');
}

describe('Serve Greenwood With: ', function() {
const LABEL = 'A Single Page Application (SPA)';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
const hostname = 'http://localhost';
const BODY_REGEX = /<body>(.*)<\/body>/s;
const expected = removeWhiteSpace(fs.readFileSync(path.join(outputPath, `src${path.sep}index.html`), 'utf-8').match(BODY_REGEX)[0]);

const port = 8080;
let runner;

before(function() {
this.context = {
hostname: `${hostname}:${port}`
};
runner = new Runner();
});

describe(LABEL, function() {

before(async function() {
await runner.setup(outputPath);

return new Promise(async (resolve) => {
setTimeout(() => {
resolve();
}, 5000);

await runner.runCommand(cliPath, 'serve');
});
});

runSmokeTest(['serve'], LABEL);

describe('Serve command specific HTML behaviors for client side routing at root - /', function() {
let response = {};

before(async function() {
return new Promise((resolve, reject) => {
request.get({
url: `http://127.0.0.1:${port}/`,
headers: {
accept: 'text/html'
}
}, (err, res) => {
if (err) {
reject();
}

response = res;

resolve();
});
});
});

it('should return the correct content type', function(done) {
expect(response.headers['content-type']).to.contain('text/html');
done();
});

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

done();
});

it('should return the expected body contents', function(done) {
expect(removeWhiteSpace(response.body.match(BODY_REGEX)[0])).to.equal(expected);
done();
});
});

describe('Serve command specific HTML behaviors for client side routing at 1 level route - /<resource>', function() {
let response = {};

before(async function() {
return new Promise((resolve, reject) => {
request.get({
url: `http://127.0.0.1:${port}/artists/`,
headers: {
accept: 'text/html'
}
}, (err, res) => {
if (err) {
reject();
}

response = res;
resolve();
});
});
});

it('should return the correct content type', function(done) {
expect(response.headers['content-type']).to.contain('text/html');
done();
});

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

done();
});

it('should return the expected body contents', function(done) {
expect(removeWhiteSpace(response.body.match(BODY_REGEX)[0])).to.equal(expected);
done();
});
});

describe('Serve command specific HTML behaviors for client side routing at 1 level route - /<resource>/:id', function() {
let response = {};

before(async function() {
return new Promise((resolve, reject) => {
request.get({
url: `http://127.0.0.1:${port}/artists/1`,
headers: {
accept: 'text/html'
}
}, (err, res) => {
if (err) {
reject();
}

response = res;

resolve();
});
});
});

it('should return the correct content type', function(done) {
expect(response.headers['content-type']).to.contain('text/html');
done();
});

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

done();
});

it('should return the expected body contents', function(done) {
expect(removeWhiteSpace(response.body.match(BODY_REGEX)[0])).to.equal(expected);
done();
});
});
});

after(function() {
runner.stopCommand();
runner.teardown(getOutputTeardownFiles(outputPath));
});
});
12 changes: 12 additions & 0 deletions packages/cli/test/cases/serve.spa/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>

<head>
<title>My Super SPA</title>
</head>

<body>
<h1>SPA mode activated!</h1>
<router></router>
</body>

</html>

0 comments on commit cb62091

Please sign in to comment.