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 1064 restore spa fallback handling for client side routing #1065

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>