diff --git a/packages/cli/src/lib/resource-utils.js b/packages/cli/src/lib/resource-utils.js index 7feb3091f..d0b2d14e1 100644 --- a/packages/cli/src/lib/resource-utils.js +++ b/packages/cli/src/lib/resource-utils.js @@ -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); diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 3fbfef60d..1221ef18b 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -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'); diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 43eff82c3..1b9d5d036 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -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) { diff --git a/packages/cli/test/cases/develop.spa/develop.spa.spec.js b/packages/cli/test/cases/develop.spa/develop.spa.spec.js index 4421341b1..1329dca85 100644 --- a/packages/cli/test/cases/develop.spa/develop.spa.spec.js +++ b/packages/cli/test/cases/develop.spa/develop.spa.spec.js @@ -14,6 +14,7 @@ * User Workspace * src/ * index.html + * main.css * */ import chai from 'chai'; @@ -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() { diff --git a/packages/cli/test/cases/develop.spa/src/main.css b/packages/cli/test/cases/develop.spa/src/main.css new file mode 100644 index 000000000..f30a1dde2 --- /dev/null +++ b/packages/cli/test/cases/develop.spa/src/main.css @@ -0,0 +1,3 @@ +* { + color: red; +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.spa/serve.spa.spec.js b/packages/cli/test/cases/serve.spa/serve.spa.spec.js new file mode 100644 index 000000000..64db569c0 --- /dev/null +++ b/packages/cli/test/cases/serve.spa/serve.spa.spec.js @@ -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>/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 - /