html-express-js
Advanced tools
Comparing version
{ | ||
"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" | ||
} | ||
} |
105
README.md
@@ -10,3 +10,4 @@  | ||
- 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 | ||
``` |
130
src/index.js
@@ -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); | ||
}, | ||
}; | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
169
76.04%11362
-5.28%14
100%4
-80%164
-31.67%1
Infinity%