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.
@@ -0,0 +1,2 @@
1
+ package*.json
2
+ node_modules
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ singleQuote: true,
3
+ };
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ git: {
3
+ commitMessage: '${version}',
4
+ tagName: 'v${version}',
5
+ },
6
+ github: {
7
+ release: true,
8
+ releaseName: '${version}',
9
+ },
10
+ hooks: {
11
+ 'before:init': ['npm test'],
12
+ },
13
+ };
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,15 @@
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>404</title>
9
+ </head>
10
+
11
+ <body>
12
+ Not found!
13
+ </body>
14
+ </html>
15
+ `;
@@ -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,3 @@
1
+ body {
2
+ background-color: lightgray;
3
+ }
@@ -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
+ }