Skip to content

Commit

Permalink
Allow renderFile path to be relative, allow views option to be string…
Browse files Browse the repository at this point in the history
…, move loadFile to file-handlers.ts, refactor file handling, better JSDoc
  • Loading branch information
nebrelbug committed Sep 4, 2020
1 parent 98c60dc commit 30101c5
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 85 deletions.
166 changes: 115 additions & 51 deletions src/file-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,111 @@
import EtaErr from './err'
import compile from './compile'
import { getConfig } from './config'
import { getPath, readFile, loadFile } from './file-utils'
import { getPath, readFile } from './file-utils'
import { copyProps } from './utils'
import { promiseImpl } from './polyfills'

/* TYPES */

import { EtaConfig, PartialConfig } from './config'
import { EtaConfig, PartialConfig, EtaConfigWithFilename } from './config'
import { TemplateFunction } from './compile'

export type CallbackFn = (err: Error | null, str?: string) => void

interface FileOptions extends EtaConfig {
filename: string
}

interface DataObj {
/** Express.js settings may be stored here */
settings?: {
[key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any
}
[key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any
}

interface PartialConfigWithFilename extends Partial<EtaConfig> {
filename: string
}

/* END TYPES */

/**
* Reads a template, compiles it into a function, caches it if caching isn't disabled, returns the function
*
* @param filePath Absolute path to template file
* @param options Eta configuration overrides
* @param noCache Optionally, make Eta not cache the template
*/

export function loadFile(
filePath: string,
options: PartialConfigWithFilename,
noCache?: boolean
): TemplateFunction {
var config = getConfig(options)
var template = readFile(filePath)
try {
var compiledTemplate = compile(template, config)
if (!noCache) {
config.templates.define((config as EtaConfigWithFilename).filename, compiledTemplate)
}
return compiledTemplate
} catch (e) {
throw EtaErr('Loading file: ' + filePath + ' failed:\n\n' + e.message)
}
}

/**
* Get the template from a string or a file, either compiled on-the-fly or
* read from cache (if enabled), and cache the template if needed.
*
* If `options.cache` is true, this function reads the file from
* `options.filename` so it must be set prior to calling this function.
*
* @param {Options} options compilation options
* @param {String} [template] template source
* @return {(TemplateFunction|ClientFunction)}
* Depending on the value of `options.client`, either type might be returned.
* @static
* @param options compilation options
* @return Eta template function
*/

function handleCache(options: FileOptions): TemplateFunction {
function handleCache(options: EtaConfigWithFilename): TemplateFunction {
var filename = options.filename

if (options.cache) {
var func = options.templates.get(filename)
if (func) {
return func
} else {
return loadFile(filename, options)
}

return loadFile(filename, options)
}

return compile(readFile(filename), options)
// Caching is disabled, so pass noCache = true
return loadFile(filename, options, true)
}

/**
* Try calling handleCache with the given options and data and call the
* callback with the result. If an error occurs, call the callback with
* the error. Used by renderFile().
*
* @param {Options} options compilation options
* @param {Object} data template data
* @param {RenderFileCallback} cb callback
* @static
* @param data template data
* @param options compilation options
* @param cb callback
*/

function tryHandleCache(options: FileOptions, data: object, cb: CallbackFn | undefined) {
var result
if (!cb) {
function tryHandleCache(data: object, options: EtaConfigWithFilename, cb: CallbackFn | undefined) {
if (cb) {
try {
// Note: if there is an error while rendering the template,
// It will bubble up and be caught here
var templateFn = handleCache(options)
templateFn(data, options, cb)
} catch (err) {
return cb(err)
}
} else {
// No callback, try returning a promise
if (typeof promiseImpl === 'function') {
return new promiseImpl(function (resolve: Function, reject: Function) {
return new promiseImpl<string>(function (resolve: Function, reject: Function) {
try {
result = handleCache(options)(data, options)
var templateFn = handleCache(options)
var result = templateFn(data, options)
resolve(result)
} catch (err) {
reject(err)
Expand All @@ -83,12 +116,6 @@ function tryHandleCache(options: FileOptions, data: object, cb: CallbackFn | und
} else {
throw EtaErr("Please provide a callback function, this env doesn't support Promises")
}
} else {
try {
handleCache(options)(data, options, cb)
} catch (err) {
return cb(err)
}
}
}

Expand All @@ -97,31 +124,66 @@ function tryHandleCache(options: FileOptions, data: object, cb: CallbackFn | und
*
* If `options.cache` is `true`, then the template is cached.
*
* @param {String} path path for the specified file
* @param {Options} options compilation options
* @return {(TemplateFunction|ClientFunction)}
* Depending on the value of `options.client`, either type might be returned
* @static
* This returns a template function and the config object with which that template function should be called.
*
* @remarks
*
* It's important that this returns a config object with `filename` set.
* Otherwise, the included file would not be able to use relative paths
*
* @param path path for the specified file (if relative, specify `views` on `options`)
* @param options compilation options
* @return [Eta template function, new config object]
*/

// TODO: error if file path doesn't exist
function includeFile(path: string, options: EtaConfig): [TemplateFunction, EtaConfig] {
// the below creates a new options object, using the parent filepath of the old options object and the path
var newFileOptions = getConfig({ filename: getPath(path, options) }, options)
// TODO: make sure properties are currectly copied over
return [handleCache(newFileOptions as FileOptions), newFileOptions]
return [handleCache(newFileOptions as EtaConfigWithFilename), newFileOptions]
}

/**
* Render a template from a filepath.
*
* @param filepath Path to template file. If relative, specify `views` on the config object
*
* This can take two different function signatures:
*
* - `renderFile(filename, dataAndConfig, [cb])`
* - Eta will merge `dataAndConfig` into `eta.defaultConfig`
* - `renderFile(filename, data, [config], [cb])`
*
* Note that renderFile does not immediately return the rendered result. If you pass in a callback function, it will be called with `(err, res)`. Otherwise, `renderFile` will return a `Promise` that resolves to the render result.
*
* **Examples**
*
* ```js
* eta.renderFile("./template.eta", data, {cache: true}, function (err, rendered) {
* if (err) console.log(err)
* console.log(rendered)
* })
*
* let rendered = await eta.renderFile("./template.eta", data, {cache: true})
*
* let rendered = await eta.renderFile("./template", {...data, cache: true})
* ```
*/

function renderFile(filename: string, data: DataObj, config?: PartialConfig, cb?: CallbackFn): any

function renderFile(filename: string, data: DataObj, cb?: CallbackFn): any

function renderFile(filename: string, data: DataObj, config?: PartialConfig, cb?: CallbackFn) {
// Here we have some function overloading.
// Essentially, the first 2 arguments to renderFile should always be the filename and data
// However, with Express, configuration options will be passed along with the data.
// Thus, Express will call renderFile with (filename, dataAndOptions, cb)
// And we want to also make (filename, data, options, cb) available

var Config: FileOptions
/*
Here we have some function overloading.
Essentially, the first 2 arguments to renderFile should always be the filename and data
However, with Express, configuration options will be passed along with the data.
Thus, Express will call renderFile with (filename, dataAndOptions, cb)
And we want to also make (filename, data, options, cb) available
*/

var renderConfig: EtaConfigWithFilename
var callback: CallbackFn | undefined

// First, assign our callback function to `callback`
Expand All @@ -137,33 +199,35 @@ function renderFile(filename: string, data: DataObj, config?: PartialConfig, cb?

// If there is a config object passed in explicitly, use it
if (typeof config === 'object') {
Config = getConfig((config as PartialConfig) || {}) as FileOptions
renderConfig = getConfig((config as PartialConfig) || {}) as EtaConfigWithFilename
} else {
// Otherwise, get the config from the data object
// And then grab some config options from data.settings
// Which is where Express sometimes stores them
Config = getConfig((data as PartialConfig) || {}) as FileOptions
renderConfig = getConfig((data as PartialConfig) || {}) as EtaConfigWithFilename
if (data.settings) {
// Pull a few things from known locations
if (data.settings.views) {
Config.views = data.settings.views
renderConfig.views = data.settings.views
}
if (data.settings['view cache']) {
Config.cache = true
renderConfig.cache = true
}
// Undocumented after Express 2, but still usable, esp. for
// items that are unsafe to be passed along with data, like `root`
var viewOpts = data.settings['view options']

if (viewOpts) {
copyProps(Config, viewOpts)
copyProps(renderConfig, viewOpts)
}
}
}

Config.filename = filename // Set filename option
// Set the filename option on the template
// This will first try to resolve the file path (see getPath for details)
renderConfig.filename = getPath(filename, renderConfig)

return tryHandleCache(Config, data, callback)
return tryHandleCache(data, renderConfig, callback)
}

export { includeFile, renderFile }
65 changes: 31 additions & 34 deletions src/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,26 @@ var _BOM = /^\uFEFF/
// express is set like: app.engine('html', require('eta').renderFile)

import EtaErr from './err'
import Compile from './compile'
import { getConfig } from './config'

/* TYPES */

import { EtaConfig, PartialConfig } from './config'
import { TemplateFunction } from './compile'

interface PartialFileConfig extends PartialConfig {
filename: string
}

interface FileConfig extends EtaConfig {
filename: string
}
import { EtaConfig } from './config'

/* END TYPES */

/**
* Get the path to the included file from the parent file path and the
* specified path.
*
* @param {String} name specified path
* @param {String} parentfile parent file path
* @param {Boolean} [isDir=false] whether parent file path is a directory
* @return {String}
* If `name` does not have an extension, it will default to `.eta`
*
* @param name specified path
* @param parentfile parent file path
* @param isDirectory whether parentfile is a directory
* @return absolute path to template
*/

function getWholeFilePath(name: string, parentfile: string, isDirectory?: boolean) {
function getWholeFilePath(name: string, parentfile: string, isDirectory?: boolean): string {
var includePath = path.resolve(
isDirectory ? parentfile : path.dirname(parentfile), // returns directory the parent file is in
name // file
Expand All @@ -45,11 +36,17 @@ function getWholeFilePath(name: string, parentfile: string, isDirectory?: boolea
}

/**
* Get the path to the included file by Options
* Get the absolute path to an included template
*
* If this is called with an absolute path (for example, starting with '/' or 'C:\') then Eta will return the filepath.
*
* If this is called with a relative path, Eta will:
* - Look relative to the current template (if the current template has the `filename` property)
* - Look inside each directory in options.views
*
* @param {String} path specified path
* @param {Options} options compilation options
* @return {String}
* @param path specified path
* @param options compilation options
* @return absolute path to template
*/

function getPath(path: string, options: EtaConfig) {
Expand All @@ -71,7 +68,9 @@ function getPath(path: string, options: EtaConfig) {
}
}
// Then look in any views directories
// TODO: write tests for if views is a string
if (!includePath) {
// Loop through each views directory and search for the file.
if (
Array.isArray(views) &&
views.some(function (v) {
Expand All @@ -80,6 +79,12 @@ function getPath(path: string, options: EtaConfig) {
})
) {
includePath = filePath
} else if (typeof views === 'string') {
// Search for the file if views is a single directory
filePath = getWholeFilePath(path, views, true)
if (fs.existsSync(filePath)) {
includePath = filePath
}
}
}
if (!includePath) {
Expand All @@ -89,20 +94,12 @@ function getPath(path: string, options: EtaConfig) {
return includePath
}

/**
* Reads a file synchronously
*/

function readFile(filePath: string) {
return readFileSync(filePath).toString().replace(_BOM, '') // TODO: is replacing BOM's necessary?
}

function loadFile(filePath: string, options: PartialFileConfig): TemplateFunction {
var config = getConfig(options)
var template = readFile(filePath)
try {
var compiledTemplate = Compile(template, config)
config.templates.define((config as FileConfig).filename, compiledTemplate)
return compiledTemplate
} catch (e) {
throw EtaErr('Loading file: ' + filePath + ' failed')
}
}

export { getPath, readFile, loadFile }
export { getPath, readFile }

0 comments on commit 30101c5

Please sign in to comment.