Closes #234: Enable wildcards to be defined using HTML filename

parent 242c7260
......@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [15.1.0] - 2020-09-02
### Added
- Enable wildcards to be defined using the filename of the HTML file, not just as the `index.html` file in a folder (to be consistent with how `.dynamic` routes work).
## [15.0.1] - 2020-09-02
### Fixed
......
......@@ -1301,6 +1301,8 @@ To do this using wildcard routes:
When Site.js finds a `.wildcard` folder, it adds every first-level sub-folder in it as a route that maps to the `index.html` file in it. In the example above, `/hello/aral` and `hello/what/is/this/about` will both map to the same file.
Similarly, (as of 15.1.0) any HTML file found in the `.wildcard` folder will also be mapped to a wildcard route. So you could have created the same route in `.wildcard/hello.html` instead.
Any path fragment after the route name itself is treated as a positional argument.
Although you could parse the `document.location` yourself to get at the arguments and the route name, Site.js makes it even easier for you by injecting a tiny bit of JavaScript at the top of your page that exposes these as:
......
......@@ -1214,49 +1214,61 @@ class Site {
if (fs.existsSync(wildcardRoutesDirectory)) {
fs.readdirSync(wildcardRoutesDirectory, {withFileTypes: true}).forEach(file => {
if (file.isDirectory()) {
const wildcard = file.name
const wildcardIndexFilePath = path.join(wildcardRoutesDirectory, wildcard, 'index.html')
if (fs.existsSync(wildcardIndexFilePath)) {
this.log(` 🃏 ❨site.js❩ Serving wildcard route: ${clr(`https://${this.prettyLocation()}/${wildcard}/**/*`, 'green')}${clr(`/.wildcard/${wildcard}/index.html`, 'cyan')}`)
// Read the HTML content and inject some javascript to make it easy to access the route
// name and the arguments from window.route and and window.arguments.
const wildcardIndexFilePath = path.join(wildcardRoutesDirectory, wildcard, 'index.html')
wildcards[wildcard] = fs.readFileSync(wildcardIndexFilePath, 'utf-8').replace('<body>', `
<body>
<script>
// Site.js: add window.routeName and window.arguments objects to wildcard route.
__site_js__pathFragments = document.location.pathname.split('/')
window.route = __site_js__pathFragments[1]
window.arguments = __site_js__pathFragments.slice(2).filter(value => value !== '')
delete __site_js__pathFragments
</script>
`)
this.app.use(`/${wildcard}`, (() => {
// Capture the current wildcard
const __wildcard = wildcard
return (request, response, next) => {
const pathFragments = request.path.split('/')
if (pathFragments.length >= 2 && pathFragments[1] !== '') {
// OK, we have a sub-path, so serve the wildcard.
response
.type('html')
.end(wildcards[__wildcard])
} else {
// No sub-path, ignore this request.
next()
}
}
})())
let wildcard = file.name
let wildcardFilePath
let wildcardFilePathPretty
if (file.isDirectory(wildcard)) {
wildcardFilePath = path.join(wildcardRoutesDirectory, wildcard, 'index.html')
wildcardFilePathPretty = `${wildcard}/index.html`
} else {
if (!wildcard.endsWith('.html')) {
this.log(` ❗ ❨site.js❩ Non-HTML file (${wildcard}) found in wildcards directory, ignoring.`)
return // from forEach.
} else {
// We found a directory inside of the .wildcard directory but it doesn’t have an index.html
// file inside it with the content to serve. Warn the person.
this.log(` ❗ ❨site.js❩ Wilcard directory found at /.wildcard/${wildcard} but there is no index.html inside it. Ignoring…`)
wildcardFilePath = path.join(wildcardRoutesDirectory, wildcard)
wildcardFilePathPretty = wildcard
wildcard = wildcard.replace('.html', '')
}
}
if (fs.existsSync(wildcardFilePath)) {
this.log(` 🃏 ❨site.js❩ Serving wildcard route: ${clr(`https://${this.prettyLocation()}/${wildcard}/**/*`, 'green')}${clr(`/.wildcard/${wildcardFilePathPretty}`, 'cyan')}`)
// Read the HTML content and inject some javascript to make it easy to access the route
// name and the arguments from window.route and and window.arguments.
wildcards[wildcard] = fs.readFileSync(wildcardFilePath, 'utf-8').replace('<body>', `
<body>
<script>
// Site.js: add window.routeName and window.arguments objects to wildcard route.
__site_js__pathFragments = document.location.pathname.split('/')
window.route = __site_js__pathFragments[1]
window.arguments = __site_js__pathFragments.slice(2).filter(value => value !== '')
delete __site_js__pathFragments
</script>
`)
this.app.use(`/${wildcard}`, (() => {
// Capture the current wildcard
const __wildcard = wildcard
return (request, response, next) => {
const pathFragments = request.path.split('/')
if (pathFragments.length >= 2 && pathFragments[1] !== '') {
// OK, we have a sub-path, so serve the wildcard.
response
.type('html')
.end(wildcards[__wildcard])
} else {
// No sub-path, ignore this request.
next()
}
}
})())
} else {
// We found a directory inside of the .wildcard directory but it doesn’t have an index.html
// file inside it with the content to serve. Warn the person.
this.log(` ❗ ❨site.js❩ Wilcard directory found at /.wildcard/${wildcard} but there is no index.html inside it. Ignoring…`)
}
})
}
}
......
{
"name": "@small-tech/site.js",
"version": "15.0.1",
"version": "15.1.0",
"description": "Small Web construction set.",
"keywords": [
"web server",
......
......@@ -400,7 +400,7 @@ test ('[site.js] response.html()', async t=> {
test('[site.js] wildcard routes', async t => {
t.plan(2)
t.plan(4)
const site = new Site({path: 'test/site-wildcard-routes'})
await site.serve()
......@@ -413,7 +413,7 @@ test('[site.js] wildcard routes', async t => {
process.exit(1)
}
t.strictEquals(response.statusCode, 200, 'request succeeds')
t.strictEquals(response.statusCode, 200, 'wildcard “hello” request succeeds')
t.strictEquals(dehydrate(response.body).toLowerCase(), dehydrate(`
<!DOCTYPE html>
<html lang='en'>
......@@ -422,16 +422,14 @@ test('[site.js] wildcard routes', async t => {
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Wildcard: hello</title>
</head>
<body>
<script>
// Site.js: add window.routeName and window.arguments objects to wildcard route.
__site_js__pathFragments = document.location.pathname.split('/')
window.route = __site_js__pathFragments[1]
window.arguments = __site_js__pathFragments.slice(2).filter(value => value !== '')
delete __site_js__pathFragments
</script>
<body>
<script>
// Site.js: add window.routeName and window.arguments objects to wildcard route.
__site_js__pathFragments = document.location.pathname.split('/')
window.route = __site_js__pathFragments[1]
window.arguments = __site_js__pathFragments.slice(2).filter(value => value !== '')
delete __site_js__pathFragments
</script>
<script>
document.write(\`<h1><em>\${window.route}</em> wildcard route</h1>\`)
document.write('<p>Called with the following arguments:</p>')
......@@ -444,7 +442,48 @@ test('[site.js] wildcard routes', async t => {
<script src="/instant/client/bundle.js"></script>
</body>
</html>
`).toLowerCase(), 'wildcard route body is as expected')
`).toLowerCase(), 'wildcard “hello” route body is as expected')
// Test routes defined with the name of the HTML file.
try {
response = await secureGet('https://localhost/goodbye/see/you/later')
} catch (error) {
console.log(error)
process.exit(1)
}
t.strictEquals(response.statusCode, 200, 'wildcard “goodbye” request succeeds')
t.strictEquals(dehydrate(response.body).toLowerCase(), dehydrate(`
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Wildcard: goodbye</title>
</head>
<body>
<script>
// Site.js: add window.routeName and window.arguments objects to wildcard route.
__site_js__pathFragments = document.location.pathname.split('/')
window.route = __site_js__pathFragments[1]
window.arguments = __site_js__pathFragments.slice(2).filter(value => value !== '')
delete __site_js__pathFragments
</script>
<script>
document.write(\`<h1><em>\${window.route}</em> wildcard route</h1>\`)
document.write('<p>Called with the following arguments:</p>')
document.write('<ol>')
window.arguments.forEach(argument => {
document.write(\`<li>\${argument}</li>\`)
})
document.write('</ol>')
</script>
<script src="/instant/client/bundle.js"></script>
</body>
</html>
`).toLowerCase(), 'wildcard “goodbye” route body is as expected')
site.server.close()
})
......
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Wildcard: goodbye</title>
</head>
<body>
<script>
document.write(`<h1><em>${window.route}</em> wildcard route</h1>`)
document.write('<p>Called with the following arguments:</p>')
document.write('<ol>')
window.arguments.forEach(argument => {
document.write(`<li>${argument}</li>`)
})
document.write('</ol>')
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment