html-express-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -0
- package/.prettierrc.cjs +3 -0
- package/.release-it.cjs +13 -0
- package/README.md +81 -0
- package/example/app.js +33 -0
- package/example/public/404.js +15 -0
- package/example/public/dashboard.js +17 -0
- package/example/public/hello.js +17 -0
- package/example/public/includes/head.js +12 -0
- package/example/public/main.css +3 -0
- package/example/server.js +20 -0
- package/package.json +39 -0
- package/src/index.js +95 -0
package/.prettierignore
ADDED
package/.prettierrc.cjs
ADDED
package/.release-it.cjs
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# html-express-js
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
- Serves HTML documents using template literals + Lit
|
|
6
|
+
- Supports includes in served HTML documents
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm install html-express-js
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
The following shows at a high level how the package can be used as an Express template engine. See [example](/example) directory for all details of a working implementation.
|
|
17
|
+
|
|
18
|
+
Set up your Express app to use this engine:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import htmlExpress from 'html-express-js';
|
|
22
|
+
|
|
23
|
+
const app = express();
|
|
24
|
+
const __dirname = resolve();
|
|
25
|
+
|
|
26
|
+
// set up engine
|
|
27
|
+
app.engine(
|
|
28
|
+
'js',
|
|
29
|
+
htmlExpress({
|
|
30
|
+
includesDir: 'includes', // where all includes reside
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
// use engine
|
|
34
|
+
app.set('view engine', 'js');
|
|
35
|
+
|
|
36
|
+
// set directory where all index.js pages are served
|
|
37
|
+
app.set('views', `${__dirname}/public`);
|
|
38
|
+
|
|
39
|
+
// render HTML in public/dashboard.js with data
|
|
40
|
+
app.get('/', function (req, res, next) {
|
|
41
|
+
res.render('homepage', {
|
|
42
|
+
title: 'Awesome Homepage',
|
|
43
|
+
name: 'Bob',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then you can create the associated files:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// public/includes/head.js
|
|
52
|
+
import { html } from 'html-express-js';
|
|
53
|
+
|
|
54
|
+
export const view = () => html`
|
|
55
|
+
<meta charset="utf-8" />
|
|
56
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
57
|
+
<meta
|
|
58
|
+
name="viewport"
|
|
59
|
+
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
|
|
60
|
+
/>
|
|
61
|
+
`;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
// public/homepage.js
|
|
66
|
+
import { html } from 'html-express-js';
|
|
67
|
+
|
|
68
|
+
export const view = (data, state) => html`
|
|
69
|
+
<!DOCTYPE html>
|
|
70
|
+
<html lang="en">
|
|
71
|
+
<head>
|
|
72
|
+
${state.includes.head}
|
|
73
|
+
<title>${data.title}</title>
|
|
74
|
+
</head>
|
|
75
|
+
|
|
76
|
+
<body>
|
|
77
|
+
<h1>This is the homepage</h1>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
`;
|
|
81
|
+
```
|
package/example/app.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import litExpress from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
const __dirname = resolve();
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
app.engine(
|
|
10
|
+
'js',
|
|
11
|
+
litExpress({
|
|
12
|
+
includesDir: 'includes',
|
|
13
|
+
notFoundView: '404/index',
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
app.set('view engine', 'js');
|
|
18
|
+
app.set('views', `${__dirname}/example/public`);
|
|
19
|
+
|
|
20
|
+
app.get('/', async function (req, res) {
|
|
21
|
+
res.render('dashboard');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.get('/hello', async function (req, res) {
|
|
25
|
+
res.render('hello', {
|
|
26
|
+
name: 'world',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// serve all other static files like CSS, images, etc
|
|
31
|
+
app.use(express.static(`${__dirname}/example/public`));
|
|
32
|
+
|
|
33
|
+
export default app;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { html } from '../../src/index.js';
|
|
2
|
+
|
|
3
|
+
export const view = (data, state) => html`
|
|
4
|
+
<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
${state.includes.head}
|
|
8
|
+
<title>Dashboard</title>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<h1>This is the dashboard!</h1>
|
|
13
|
+
|
|
14
|
+
<p>Click <a href="/hello">here</a> to go hello route.</p>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { html } from '../../src/index.js';
|
|
2
|
+
|
|
3
|
+
export const view = (data, state) => html`
|
|
4
|
+
<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
${state.includes.head}
|
|
8
|
+
<title>Hello!</title>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<h1>Hello, ${data.name}!</h1>
|
|
13
|
+
|
|
14
|
+
<p>Click <a href="/">here</a> to go back to the dashboard.</p>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
`;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { html } from '../../../src/index.js';
|
|
2
|
+
|
|
3
|
+
export const view = (/*data, state*/) => html`
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
6
|
+
<meta
|
|
7
|
+
name="viewport"
|
|
8
|
+
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<link rel="stylesheet" href="/main.css" />
|
|
12
|
+
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import app from './app.js';
|
|
2
|
+
import reload from 'reload';
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
|
+
|
|
5
|
+
const port = 2222;
|
|
6
|
+
|
|
7
|
+
// reload browser on file changes
|
|
8
|
+
reload(app, { verbose: true })
|
|
9
|
+
.then(function (reloadReturned) {
|
|
10
|
+
chokidar.watch(['./src', './example']).on('all', (/*event, path*/) => {
|
|
11
|
+
reloadReturned.reload();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.listen(port, function () {
|
|
15
|
+
console.log(`Server started at http://localhost:${port}`);
|
|
16
|
+
});
|
|
17
|
+
})
|
|
18
|
+
.catch(function (err) {
|
|
19
|
+
console.error('Reload could not start, could not start example app', err);
|
|
20
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "html-express-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An Express template engine to render HTML views using native JavaScript",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"format": "prettier --write '**/*'",
|
|
9
|
+
"test": "prettier --check '**/*'",
|
|
10
|
+
"start": "node ./example/server.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/markcellus/html-express-js.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"lit",
|
|
18
|
+
"express",
|
|
19
|
+
"views",
|
|
20
|
+
"template",
|
|
21
|
+
"engine"
|
|
22
|
+
],
|
|
23
|
+
"author": "Mark",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/markcellus/html-express-js/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/markcellus/html-express-js#readme",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"glob": "^8.0.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"chokidar": "^3.5.3",
|
|
34
|
+
"express": "^4.18.1",
|
|
35
|
+
"prettier": "^2.7.0",
|
|
36
|
+
"release-it": "^15.0.0",
|
|
37
|
+
"reload": "^3.2.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import g from 'glob';
|
|
4
|
+
|
|
5
|
+
const glob = promisify(g);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders an HTML template in a file.
|
|
9
|
+
*
|
|
10
|
+
* @private
|
|
11
|
+
* @param {string} path - The path to html file
|
|
12
|
+
* @param {object} [data]
|
|
13
|
+
* @param {object} [state] - Page-level attributes
|
|
14
|
+
* @returns {string} HTML
|
|
15
|
+
*/
|
|
16
|
+
async function renderHtmlFileTemplate(path, data, state) {
|
|
17
|
+
const { view } = await import(path);
|
|
18
|
+
const rendered = view(data, state);
|
|
19
|
+
let html = '';
|
|
20
|
+
for (const chunk of rendered) {
|
|
21
|
+
html += chunk;
|
|
22
|
+
}
|
|
23
|
+
return html;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders a Lit JS HTML file and adds all includes to state object.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} filePath - The path to html file
|
|
30
|
+
* @param {object} data - Data to be made available in view
|
|
31
|
+
* @param {object} options - Options passed to lit express
|
|
32
|
+
* @param {object} options.includesDir
|
|
33
|
+
* @param {object} options.viewsDir
|
|
34
|
+
* @param {object} options.notFoundView
|
|
35
|
+
* @returns {string} HTML with includes available (appended to state)
|
|
36
|
+
*/
|
|
37
|
+
async function renderHtmlFile(filePath, data = {}, options = {}) {
|
|
38
|
+
const state = {
|
|
39
|
+
includes: {},
|
|
40
|
+
};
|
|
41
|
+
const { includesDir } = options;
|
|
42
|
+
|
|
43
|
+
const includeFilePaths = await glob(`${includesDir}/*.js`);
|
|
44
|
+
for await (const includePath of includeFilePaths) {
|
|
45
|
+
const key = basename(includePath, '.js');
|
|
46
|
+
state.includes[key] = await renderHtmlFileTemplate(
|
|
47
|
+
includePath,
|
|
48
|
+
data,
|
|
49
|
+
state
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return await renderHtmlFileTemplate(filePath, data, state);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Template literal that supports string interpolating in passed HTML.
|
|
57
|
+
* @param {*} strings
|
|
58
|
+
* @param {...any} data
|
|
59
|
+
* @returns {string} - HTML string
|
|
60
|
+
*/
|
|
61
|
+
export function html(strings, ...data) {
|
|
62
|
+
let rawHtml = '';
|
|
63
|
+
for (const [i, str] of strings.entries()) {
|
|
64
|
+
const exp = data[i] || '';
|
|
65
|
+
rawHtml += str + exp;
|
|
66
|
+
}
|
|
67
|
+
const html = rawHtml.replace(/[\n\r]/g, '');
|
|
68
|
+
return html;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns a template engine view function.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} opts
|
|
75
|
+
* @param {object} [opts.includesDir]
|
|
76
|
+
* @param {object} [opts.notFoundView]
|
|
77
|
+
* @returns {Function}
|
|
78
|
+
*/
|
|
79
|
+
export default function litExpress(opts) {
|
|
80
|
+
return async (filePath, data, callback) => {
|
|
81
|
+
const viewsDir = data.settings.views;
|
|
82
|
+
const includePath = opts.includesDir || 'includes';
|
|
83
|
+
|
|
84
|
+
const sanitizedOptions = {
|
|
85
|
+
viewsDir,
|
|
86
|
+
includesDir: `${viewsDir}/${includePath}`,
|
|
87
|
+
notFoundView: opts.notFoundView
|
|
88
|
+
? `${viewsDir}/${opts.notFoundView}.js`
|
|
89
|
+
: `${viewsDir}/404/index.js`,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const html = await renderHtmlFile(filePath, data, sanitizedOptions);
|
|
93
|
+
return callback(null, html);
|
|
94
|
+
};
|
|
95
|
+
}
|