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

allow multiple 'application roots' #190

Merged
merged 4 commits into from
May 7, 2020
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
21 changes: 20 additions & 1 deletion docs/src/content/configuration/index.raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,26 @@ module.exports = {

#### templateRenderers

...
Template renderes react to the `*.xyz.js` pattern. The `xyz` part refers to the renderer.

An example of specifying an `mjml` renderer which handles `*.mjml.js` files:
```js
module.exports = {
kaliber: {
templateRenderers: {
mjml: '/mjml-renderer'
}
}
}
```

Note that template if a template file exports a function it will retain the same name. If it exports
a non function, the result of the renderer will be placed in a file without the `.js` part.

- `index.html.js` with function exported results in `index.html.js`
- `index.html` with a non function exported results in `index.html`

See [template-renderers](/template-renderers) for more details.

#### serveMiddleware

Expand Down
16 changes: 13 additions & 3 deletions docs/src/content/server/index.raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@ A high level overview of the express modules and handlers we have defined:

### Project types

`index.html`, `404.html` and `500.html` - [Static site](/how-to/static-site)
`index.html` - [Single page application](/how-to/single-page-application)
`index.html.js` - [Server side rendering](/server-side-rendering) (might render a single page application)
- `index.html.js` - [Server side rendering](/server-side-rendering) (might render a single page application)
- `index.html`, `404.html` and `500.html` - [Static site](/how-to/static-site)
- `index.html` - [Single page application](/how-to/single-page-application)

### File resolution

- If a file exists: serve using `express.static`
- If not, look for `index.html.js` and serve that
- If not, look for `404.html` and serve that
- If not, look for `index.html` and serve that

This proces is repeated for each directory in the path. So if the path is `/a/b/something`, the server first
looks for these files in `/a/b/`, then in `/a/` and finally in `/` .

### Port

Expand Down
12 changes: 12 additions & 0 deletions example/src/dynamic1/TestUrlApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function TestUrlApp() {
const [pathname, setPathname] = React.useState('unknown')

React.useEffect(
() => { setPathname(window.location.pathname) },
[]
)

return (
<div>{pathname}</div>
)
}
11 changes: 11 additions & 0 deletions example/src/dynamic1/index.html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import head from '/partials/head'
import TestUrlApp from './TestUrlApp?universal'

export default (
<html lang='en'>
{head('Dynamic page')}
<body>
<TestUrlApp />
</body>
</html>
)
5 changes: 5 additions & 0 deletions example/src/dynamic2/TestUrlApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function TestUrlApp({ initialPath }) {
return (
<div>{initialPath}</div>
)
}
13 changes: 13 additions & 0 deletions example/src/dynamic2/index.html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import head from '/partials/head'
import TestUrlApp from './TestUrlApp?universal'

export default function Index({ location }) {
return (
<html lang='en'>
{head('Dynamic page')}
<body>
<TestUrlApp initialPath={location.pathname} />
</body>
</html>
)
}
10 changes: 10 additions & 0 deletions example/src/dynamic3/404.html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import head from '/partials/head'

export default (
<html lang='en'>
{head('404')}
<body>
Not found in dynamic
</body>
</html>
)
11 changes: 5 additions & 6 deletions example/src/partials/head.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ export default function (title) {
return (
<head>
<title>{title}</title>
{ stylesheet }
{ rollbar({}, `_rollbarConfig.checkIgnore = ${rollbarCheckIgnore}`) }
{ rollbar() }
{ polyfill(['default', 'es2015', 'es2016', 'es2017', 'fetch']) }
{ javascript }
<link href='https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.css' rel='stylesheet' type='text/css' />
{stylesheet}
{rollbar({}, `_rollbarConfig.checkIgnore = ${rollbarCheckIgnore}`)}
{rollbar()}
{polyfill(['default', 'es2015', 'es2016', 'es2017', 'fetch'])}
{javascript}
</head>
)
}
93 changes: 61 additions & 32 deletions library/lib/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ const blockedTemplatesRegex = new RegExp(`^(${blockedTemplateFiles.join('|')})$`
const app = express()

const target = resolve(process.cwd(), 'target')
const dir = publicPath.slice(1)
const index = resolve(target, dir, 'index.html')
const indexWithRouting = resolve(target, dir, 'index.html.js')
const notFound = resolve(target, dir, '404.html')
const internalServerError = resolve(target, dir, '500.html')
const publicPathDir = publicPath.slice(1)
const index = 'index.html'
const indexWithRouting = 'index.html.js'
const notFound = '404.html'
const internalServerError = '500.html'

const port = process.env.PORT
const isProduction = process.env.NODE_ENV === 'production'
Expand All @@ -50,21 +50,7 @@ app.use(express.static(target, {
}))

app.use((req, res, next) => {
fileExists(indexWithRouting)
.then(fileFound => fileFound ? serveIndexWithRouting(req, res, next) : next())
.catch(next)
})

app.use((req, res, next) => {
fileExists(notFound)
.then(fileFound => fileFound ? res.status(404).sendFile(notFound) : next())
.catch(next)
})

app.use((req, res, next) => {
fileExists(index)
.then(fileFound => fileFound ? res.status(200).sendFile(index) : next())
.catch(next)
resolveFile(req, res, next).catch(next)
})

app.use((err, req, res, next) => {
Expand All @@ -73,33 +59,76 @@ app.use((err, req, res, next) => {
console.error(err)
const response = res.status(500)
if (isProduction) {
fileExists(internalServerError)
.then(() => response.sendFile(internalServerError))
findFile(req.path, internalServerError)
.then(file => file ? response.sendFile(file) : next())
.catch(next)
} else response.send(`<pre>${err.toString()}</pre>`)
})

app.listen(port, () => console.log(`Server listening at port ${port}`))

function fileExists (path) {
async function resolveFile(req, res, next) {
try {
const { path } = req
/** @type {Array<[string, (file:any) => any]>} */
const combinations = [
[indexWithRouting, file => serveIndexWithRouting(req, res, file)],
[notFound, file => res.status(404).sendFile(file)],
[index, file => res.status(200).sendFile(file)],
]

const dirs = possibleDirectories(path)
for (const dir of dirs) {
for (const [file, handler] of combinations) {
const filePath = resolve(dir, file)
if (await fileExists(filePath)) return handler(filePath)
}
}

next()
} catch (e) {
next(e)
}
}

async function findFile(path, file) {
const dirs = possibleDirectories(path)
for (path of dirs) {
const filePath = resolve(path, file)
if (await fileExists(filePath)) return filePath
}
return null
}

async function fileExists(path) {
return isProduction
? (!fileExists.cache || fileExists.cache[path] === undefined)
? accessFile(path).then(fileFound => (addPathToCache(path, fileFound), fileFound))
: Promise.resolve(fileExists.cache[path])
? accessFile(path).then(exists => (addPathToCache(path, exists), exists))
: fileExists.cache[path]
: accessFile(path)

function accessFile (path) {
return new Promise(resolve => access(path, err => resolve(!err)))
function addPathToCache(path, exists) {
fileExists.cache = Object.assign({}, fileExists.cache, { [path]: exists })
}
}

function addPathToCache (path, fileFound) {
fileExists.cache = Object.assign({}, fileExists.cache, { [path]: fileFound })
}
async function accessFile(path) {
return new Promise(resolve => access(path, err => resolve(!err)))
}

function possibleDirectories(path) {
const pathSections = path.split('/').filter(Boolean)
const possibleDirectories = []
let sectionCount = pathSections.length
do {
possibleDirectories.push(resolve(...[target, publicPathDir, ...pathSections.slice(0, sectionCount)]))
} while (sectionCount--)
return possibleDirectories
}

function serveIndexWithRouting (req, res, next) {
function serveIndexWithRouting(req, res, file) {
const envRequire = isProduction ? require : require('import-fresh')
const template = envRequire(indexWithRouting)
const template = envRequire(file)

const routes = template.routes
const location = parsePath(req.url)
Expand Down