You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 4-6.RSVP
Socket
Book a DemoInstallSign in
Socket

html-express-js

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

html-express-js - npm Package Compare versions

Comparing version

to
3.0.0

29

package.json
{
"name": "html-express-js",
"version": "2.0.0",
"version": "3.0.0",
"description": "An Express template engine to render HTML views using native JavaScript",

@@ -10,7 +10,15 @@ "main": "src/index.js",

},
"files": [
"src/index.js",
"src/index.d.ts"
],
"scripts": {
"format": "prettier --write '**/*'",
"test": "prettier --check '**/*'",
"format": "prettier --write '**/*' --log-level=warn",
"format-check": "prettier --check '**/*' --ignore-unknown",
"test": "npm run test:src && npm run type-check && npm run format-check",
"test:src": "mocha src/**/*.tests.js",
"start": "node ./example/server.js",
"prepare": "husky install"
"type-check": "tsc -p tsconfig.json",
"prepare": "husky install",
"build": "tsc -p tsconfig.build.json"
},

@@ -38,10 +46,17 @@ "repository": {

"devDependencies": {
"@types/glob": "^8.1.0",
"@types/chai": "^4.3.13",
"@types/express": "^4.17.21",
"@types/mocha": "^10.0.6",
"@types/sinon": "^17.0.3",
"chai": "^5.1.0",
"chokidar": "^3.5.3",
"express": "^4.18.1",
"husky": "^9.0.7",
"mocha": "^10.3.0",
"prettier": "^3.0.3",
"release-it": "^17.0.0",
"reload": "^3.2.0"
"release-it": "^17.1.1",
"reload": "^3.2.0",
"sinon": "^17.0.1",
"typescript": "^5.4.3"
}
}

@@ -10,3 +10,4 @@ ![build](https://img.shields.io/travis/markcellus/html-express-js)

- Serves HTML documents using template literals
- Supports includes in served HTML documents
- Supports includes in HTML documents
- Allows shared global state throughout templates

@@ -21,3 +22,3 @@ ## Installation

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.
The following is a high level example of how the package can be used as an Express template engine. See [example](/example) directory for all details of a working implementation.

@@ -27,3 +28,3 @@ Set up your Express app to use this engine:

```js
import htmlExpress, { staticIndexHandler } from 'html-express-js';
import htmlExpress from 'html-express-js';

@@ -33,9 +34,13 @@ const app = express();

const viewsDir = `${__dirname}/public`;
const { engine, staticIndexHandler } = htmlExpress({
viewsDir, // root views directory to serve all index.js files
includesDir: `${viewsDir}/includes`, // OPTIONAL: where all includes reside
notFoundView: '404/index', // OPTIONAL: relative to viewsDir above
});
// set up engine
app.engine(
'js',
htmlExpress({
includesDir: 'includes', // where all includes reside
}),
);
app.engine('js', engine);
// use engine

@@ -45,3 +50,3 @@ app.set('view engine', 'js');

// set directory where all index.js pages are served
app.set('views', `${__dirname}/public`);
app.set('views', viewsDir);

@@ -59,8 +64,3 @@ // render HTML in public/homepage.js with data

// and, if not found, route to the 404/index.js view
app.use(
staticIndexHandler({
viewsDir: `${__dirname}/public`, // root views directory to serve all index.js files
notFoundView: '404/index', // relative to viewsDir above
}),
);
app.use(staticIndexHandler());
```

@@ -102,1 +102,74 @@

```
## Advanced usage
### Injecting and using state based on a request
The following shows an example of showing a logged out state based on the cookie on a request.
```js
import htmlExpress from 'html-express-js';
const app = express();
const __dirname = resolve();
const viewsDir = `${__dirname}/public`;
const { engine, staticIndexHandler } = htmlExpress({
viewsDir,
/**
* Inject global state into all views based on cookie
*/
buildRequestState: (req) => {
if (req.cookies['authed']) {
return {
loggedIn: true,
};
}
},
});
app.engine('js', engine);
app.set('view engine', 'js');
app.set('views', viewsDir);
app.get('/', function (req, res, next) {
res.render('homepage');
});
```
```js
// public/homepage.js
import { html } from 'html-express-js';
export const view = (data, state) => {
const { loggedIn } = state;
return html`
<!doctype html>
<html lang="en">
<head>
<title>${data.title}</title>
</head>
<body>
${loggedIn ? `<a href="/logout">Logout</a>` : 'Not logged in'}
</body>
</html>
`;
};
```
## Development
Run site in examples directory
```bash
npm start
```
Run tests
```bash
npm test
```

@@ -6,2 +6,19 @@ import { basename, extname } from 'path';

/**
* @callback HTMLExpressBuildStateHandler
* @param {import('express').Request} req
* @returns {Record<string, any>}
*/
/**
* @typedef {object} HTMLExpressOptions
* @property {string} viewsDir - The directory that houses any potential index files
* @property {string} [includesDir] - The directory that houses all of the includes
* that will be available on the includes property of each static page.
* @property {string} [notFoundView] - The path of a file relative to the views
* directory that should be served as 404 when no matching index page exists. Defaults to `404/index`.
* @property {HTMLExpressBuildStateHandler} [buildRequestState] - A callback function that allows for
* building a state object from request information, that will be merged with default state and made available to all views
*/
/**
* Renders an HTML template in a file.

@@ -30,10 +47,11 @@ *

* @param {object} data - Data to be made available in view
* @param {object} instanceOptions - Options passed to original instantiation
* @param {object} options - Options passed to original instantiation
* @param {HTMLExpressOptions['includesDir']} options.includesDir
* @param {Record<string, any>} [options.state]
* @returns {Promise<string>} HTML with includes available (appended to state)
*/
async function renderHtmlFile(filePath, data = {}, instanceOptions = {}) {
const state = {
includes: {},
};
const { includesDir } = instanceOptions;
async function renderHtmlFile(filePath, data = {}, options) {
const { includesDir } = options || {};
const state = options.state || {};
state.includes = {};

@@ -53,3 +71,5 @@ const includeFilePaths = await glob(`${includesDir}/*.js`);

/**
* Template literal that supports string interpolating in passed HTML.
* Template literal that supports string
* interpolating in passed HTML.
*
* @param {*} strings

@@ -65,18 +85,19 @@ * @param {...any} data

}
const html = rawHtml.replace(/[\n\r]/g, '');
return html;
return rawHtml;
}
/**
* @callback HTMLExpressStaticIndexHandler
* @param {HTMLExpressOptions} [options]
* @returns {import('express').RequestHandler}
*/
/**
* Attempts to render index.js pages when requesting to
* directories and fallback to 404/index.js if doesnt exist.
*
* @param {object} [options]
* @param {object} options.viewsDir - The directory that houses any potential index files
* @param {string} [options.notFoundView] - The path of a file relative to the views
* directory that should be served as 404 when no matching index page exists. Defaults to `404/index`.
* @returns {import('express').RequestHandler} - Middleware function
* @type {HTMLExpressStaticIndexHandler}
*/
export function staticIndexHandler(options) {
const notFoundView = options.notFoundView || `404/index`;
function staticIndexHandler(options) {
const { viewsDir, notFoundView, includesDir, buildRequestState } = options;

@@ -89,7 +110,19 @@ return async function (req, res, next) {

}
const sanitizedPath = rawPath.replace('/', ''); // remove beginning slash
const path = sanitizedPath ? `${sanitizedPath}/index` : 'index';
const pathWithoutPrecedingSlash = rawPath.replace('/', ''); // remove beginning slash
const path = pathWithoutPrecedingSlash
? `${pathWithoutPrecedingSlash}/index`
: 'index';
const requestState = buildRequestState ? buildRequestState(req) : {};
const renderOptions = {
includesDir,
state: requestState,
};
res.setHeader('Content-Type', 'text/html');
try {
await stat(`${options.viewsDir}/${path}.js`); // check if file exists
res.render(path);
const absoluteFilePath = `${viewsDir}/${path}.js`;
await stat(absoluteFilePath); // check if file exists
const html = await renderHtmlFile(absoluteFilePath, {}, renderOptions);
res.send(html);
} catch (e) {

@@ -99,4 +132,11 @@ if (e.code !== 'ENOENT') {

}
const notFoundViewPath = notFoundView || `404/index`;
const notFoundAbsoluteFilePath = `${viewsDir}/${notFoundViewPath}.js`;
const html = await renderHtmlFile(
notFoundAbsoluteFilePath,
{},
renderOptions,
);
res.status(404);
res.render(notFoundView);
res.send(html);
}

@@ -107,21 +147,37 @@ };

/**
* Returns a template engine view function.
* Returns an object containing both static
* index handler and the template engine callback.
*
* @param {object} [opts]
* @param {object} [opts.includesDir]
* @returns {(path: string, options: object, callback: (e: any, rendered?: string) => void) => void}
* @param {HTMLExpressOptions} [opts]
* @returns {{
* staticIndexHandler: HTMLExpressStaticIndexHandler,
* engine: Parameters<import('express').Application['engine']>[1],
* }}
*/
export default function (opts = {}) {
return async (filePath, data, callback) => {
const viewsDir = data.settings.views;
const includePath = opts.includesDir || 'includes';
const sanitizedOptions = {
viewsDir,
includesDir: `${viewsDir}/${includePath}`,
};
const html = await renderHtmlFile(filePath, data, sanitizedOptions);
return callback(null, html);
export default function (opts) {
const { buildRequestState, notFoundView, viewsDir } = opts;
const includesDir = opts.includesDir
? opts.includesDir
: `${viewsDir}/includes`;
return {
staticIndexHandler: (options) => {
return staticIndexHandler({
includesDir,
viewsDir,
notFoundView,
buildRequestState,
...options,
});
},
engine: async (filePath, data, callback) => {
const html = await renderHtmlFile(
filePath,
{},
{
includesDir,
},
);
return callback(null, html);
},
};
}