Skip to content

Commit

Permalink
Bump to v1.0.1, add docs, use tsc+tsconfig.json
Browse files Browse the repository at this point in the history
Greatly expanded the README documentation and updated some of the JSDoc
comments. Had to adjust a couple of JSDoc @type casts as well with
regard to Handlebars.CompileOptions and Handlebars.PrecompileOptions.

Also switched from jsconfig.json to tsconfig.json, as it seems better
supported out of the box. Adjusted `pnpm typecheck` to reflect this as
well.

Bumped vitest to v1.2.0.
  • Loading branch information
mbland committed Jan 17, 2024
1 parent 0356767 commit 1982b03
Show file tree
Hide file tree
Showing 11 changed files with 608 additions and 242 deletions.
218 changes: 208 additions & 10 deletions README.md
Expand Up @@ -2,8 +2,8 @@

[Rollup][] plugin to precompile [Handlebars][] templates into [JavaScript modules][]

_**Note**: I still need to add more documentation, but the plugin is fully
functional and tested._
_**Note**: I still need to add a little bit more documentation, but the plugin
is fully functional and tested._

Source: <https://github.com/mbland/rollup-plugin-handlebars-precompiler>

Expand All @@ -15,23 +15,32 @@ Source: <https://github.com/mbland/rollup-plugin-handlebars-precompiler>

## Installation

Add this package to your project's `devDependencies`, e.g., using [pnpm][]:
Add both this package and Handlebars to your project's `devDependencies`, e.g.,
using [pnpm][]:

```sh
pnpm add -D rollup-plugin-handlebars-precompiler
pnpm add -D handlebars rollup-plugin-handlebars-precompiler
```

Even though `handlebars` is a production dependency of this plugin, your project
needs to install it directly. The JavaScript modules generated by this plugin
need to access the Handlebars runtime at
`node_modules/handlebars/lib/handlebars.runtime.js` and have it bundled into
your own project.

## Features

- Generates JavaScript/ECMAScript/ES6 modules (ESM) only.
- [Modules are supported by all current browsers][esm-caniuse].
- [Modules are supported by all currently supported versions of
Node.js][esm-node].
- Supports TypeScript type checking, including [Visual Studio Code JavaScript
type checking][] and [IntelliJ IDEA/WebStorm JavaScript type checking][].
- Client code imports template files directly, as though they were JavaScript
modules to begin with, as modules generated by this plugin will:
- Import the Handlebars runtime
- Import Handlebars helper files specified in the plugin configuration
- Automatically detect and register partials and automatically import them
- Automatically detect and register [partials][] and automatically import them
where needed
- Provides a convenient syntax for both accessing individual top-level child
nodes and adding the entire template to the DOM at once.
Expand All @@ -43,12 +52,185 @@ pnpm add -D rollup-plugin-handlebars-precompiler

Each generated Handlebars template module exports two functions:

- `RawTemplate()` emits the raw string from applying a Handlebars template.
- The default export emits a [DocumentFragment][] created from the result of
`RawTemplate()`.
- `RawTemplate()` emits the raw HTML string generated by applying a Handlebars
template.
- The default export, conventionally imported as `Template()`, emits a
[DocumentFragment][] created from the result of `RawTemplate()`.

This provides you with two options of using a given template:

```js
import Template, { RawTemplate } from './component.hbs'

const appElem = document.querySelector('#app')
const context = {
message: 'Hello, World!',
url: 'https://en.wikipedia.org/wiki/%22Hello,_World!%22_program'
}
const compilerOpts = {}

// Use the DocumentFragment to append the entire template to a DOM Node at once.
// You can also extract individual element references from .children before
// doing so.
const tmpl = Template(context)
const [ firstChild, secondChild ] = tmpl.children
appElem.appendChild(tmpl)

// Use the RawTemplate() string to render the template manually.
const newElem = document.createElement('div')
newElem.innerHTML = RawTemplate(context)
appElem.appendChild(newElem)
```

Both `Template()` and `RawTemplate()` take an optional [Handlebars runtime
options][] argument:

```js
const runtimeOpts = { data: { '@foo': 'bar' } }
const tmpl = Template(context, runtimeOpts)
```

### Automatic helper and partial module imports

The plugin configuration, described below, specifies which project files contain
[custom helper function modules][custom-helpers] and [partial templates][partials].

These custom helper modules, and modules generated for explicitly used partials,
are automatically imported by any generated modules that need them. There's no
need to import them explicitly in any other code.

### Dynamic partials supported, but not automatically imported

Any code using a template that contains [dynamic partials][] will need to import
the generated modules for those partials directly. However, those generated
modules will automatically register any imported partials via
`Handlebars.registerPartial()`.

### TypeScript or TypeScript-based JavaScript type checking

To enable TypeScript type checking, copy
[lib/template.d.ts](./lib/template.d.ts) from this package into your project:

```sh
cp node_modules/rollup-plugin-handlebars-precompiler/lib/template.d.ts .
```

This is an [ambient module][] defining the types exported from each generated
module. Edit the contents to replace `.hbs` with your project's Handlebars
template file extension if necessary. If you want, you can change the file name
and locate it anywhere in your project that you wish (that TypeScript will find
it).

This is necessary because the precompiled modules are generated in _your_
project, not in `rollup-plugin-handlebars-precompiler`, so that's where
TypeScript needs to find the type declarations.

### Configuration

The function exported by this plugin takes a configuration object as an
argument. For example, in a [vite.config.js][] configuration file:

```js
import HandlebarsPrecompiler from 'rollup-plugin-handlebars-precompiler'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
HandlebarsPrecompiler({
helpers: ['components/helpers.js']
})
]
})
```

All of the configuration parameters are optional:

- **helpers** _(string[])_: paths to modules containing [custom Handlebars
helper functions][custom-helpers]

- **include** _(string | string[])_: one or more [picomatch patterns][] matching
Handlebars template files to transform
- _Default_: `['**/*.hbs', '**/*.handlebars', '**/*.mustache']`

- **exclude** _(string | string[])_: one or more [picomatch patterns][] matching Handlebars
template files to exclude from transformation
- _Default_: `'node_modules/**'`

- **partials** _(string | string[])_: one or more [picomatch patterns][]
matching Handlebars template files containing partials
- _Default_: `'**/_*'`

- **partialName** _((string) => string)_: function to transform a partial file
name into the name used to apply the partial in other templates
- _Default_:
1. Extracts the basename
2. Removes the file extension, if present
3. Strips leading non-alphanumeric characters
- _Example_: `components/_my_partial.hbs` yields `my_partial`

- **partialPath** _((string, string) => string)_: function to transform a
partial's name and that of the module importing it into its import path
- _Default_:
1. Expects a partial to reside in the same directory as another Handlebars
template that uses it
2. Adds `./_` prefix to the partial name
3. Adds the file extension of the module importing it
- _Example_: (`my_partial`, `components/other_template.hbs`) yields
`./_my_partial.hbs`

- **compiler** _(Handlebars.PrecompileOptions)_: [Handlebars compiler options][]
passed through to `Handlebars.parse()` and `Handlebars.precompile()`

- **sourcemap** or **sourceMap** _(boolean)_: disables source map generation when false

As for why both **sourcemap** and **sourceMap** are supported, it's because:

- [Rollup - Plugin Development - Source Code Transformations][] specifies that
source maps can be disabled via `sourceMap: false`.
- [Rollup - Troubleshooting - Warning: "Sourcemap is likely to be incorrect"][]
specifies that source maps can be disabled via `sourcemap: false`.

### Defining helper modules

Modules specified by the configuration as containing [custom
helpers][custom-helpers] should export a default function that takes the
[Handlebars runtime object][runtime] as an argument. It should then call
[Handlebars.registerHelper()][] or any other runtime functions as needed.

Here's an example from [test/large/components/helpers.js][], adapted from
[Handlebars - Expressions - Helpers with Hash Arguments][]:

```js
export default function(Handlebars) {
const linkHelper = function(text, options) {
const attrs = Object.keys(options.hash).map(key => {
return `${Handlebars.escapeExpression(key)}=` +
`"${Handlebars.escapeExpression(options.hash[key])}"`
})
return new Handlebars.SafeString(
`<a ${attrs.join(' ')}>${Handlebars.escapeExpression(text)}</a>`
)
}
Handlebars.registerHelper('link', linkHelper)
}
```

### Defining a partial template discovery schema

Partials are first identified as Handlebars templates via the **include**
configuration option. Then the **partials**, **partialName**, and
**partialPath** options define a schema used to identify partial template files,
register them with the Handlebars runtime, and generate imports.

The default behavior:

- considers any Handlebars file name starting with `_` to contain a partial
- expects a partial to reside in the same directory as the template that uses it
- expects the partial to use the same Handlebars file extension as the template
that includes it

Most of the time, you'll want to use the default export, imported as
`Template()` by convention.
If you choose to use a different schema for organizing partials, make sure to
update any of these configuration options as necessary.

## Development

Expand Down Expand Up @@ -126,9 +308,25 @@ level explanation.
[npm-rphp]: https://www.npmjs.com/package/rollup-plugin-handlebars-precompiler
[Handlebars source maps]: https://handlebarsjs.com/api-reference/compilation.html#handlebars-precompile-template-options
[DocumentFragment]: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
[Handlebars runtime options]: https://handlebarsjs.com/api-reference/runtime-options.html
[custom-helpers]: https://handlebarsjs.com/guide/#custom-helpers
[ambient module]: https://www.typescriptlang.org/docs/handbook/modules/reference.html#ambient-modules
[runtime]: https://handlebarsjs.com/api-reference/runtime.html
[Handlebars.registerHelper()]: https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerhelper-name-helper
[test/large/components/helpers.js]: ./test/large/components/helpers.js
[Handlebars - Expressions - Helpers with Hash Arguments]: https://handlebarsjs.com/guide/expressions.html#helpers-with-hash-arguments
[pnpm]: https://pnpm.io/
[esm-caniuse]: https://caniuse.com/es6-module
[esm-node]: https://nodejs.org/docs/latest-v18.x/api/esm.html
[Visual Studio Code JavaScript type checking]: https://code.visualstudio.com/docs/nodejs/working-with-javascript
[IntelliJ IDEA/WebStorm JavaScript type checking]: https://blog.jetbrains.com/webstorm/2019/09/using-typescript-to-check-your-javascript-code/
[partials]: https://handlebarsjs.com/guide/partials.html
[dynamic partials]: https://handlebarsjs.com/guide/partials.html#dynamic-partials
[vite.config.js]: https://vitejs.dev/config/
[picomatch patterns]: https://github.com/micromatch/picomatch#globbing-features
[Handlebars compiler options]: https://handlebarsjs.com/api-reference/compilation.html
[Rollup - Plugin Development - Source Code Transformations]: https://rollupjs.org/plugin-development/#source-code-transformations
[Rollup - Troubleshooting - Warning: "Sourcemap is likely to be incorrect"]: https://rollupjs.org/troubleshooting/#warning-sourcemap-is-likely-to-be-incorrect
[Vitest]: https://vitest.dev/
[GitHub Actions]: https://docs.github.com/actions
[mbland/tomcat-servlet-testing-example]: https://github.com/mbland/tomcat-servlet-testing-example
Expand Down
5 changes: 4 additions & 1 deletion lib/index.js
Expand Up @@ -111,7 +111,10 @@ export default class PluginImpl {
this.#partialName = options.partialName || DEFAULT_PARTIAL_NAME
this.#partialPath = options.partialPath || DEFAULT_PARTIAL_PATH

const compilerOpts = { ...options.compiler }
// options.compiler is of type CompileOptions to encourage users that have
// type checking enabled not to include srcName and destName from
// PrecompileOptions. We still take defensive measures here.
const compilerOpts = /** @type {PrecompileOptions} */({...options.compiler})
delete compilerOpts.srcName
delete compilerOpts.destName
this.#compilerOpts = (id) => ({ srcName: id, ...compilerOpts })
Expand Down
9 changes: 6 additions & 3 deletions lib/template.d.ts
Expand Up @@ -5,7 +5,10 @@
*/

declare module "*.hbs" {
export const RawTemplate: HandlebarsTemplateDelegate
export default function (context: any, options?: RuntimeOptions):
DocumentFragment
export const RawTemplate: Handlebars.TemplateDelegate
export interface TemplateRenderer {
(context: any, options?: Handlebars.RuntimeOptions): DocumentFragment
}
const Template: TemplateRenderer
export default Template
}
20 changes: 11 additions & 9 deletions lib/types.js
Expand Up @@ -59,22 +59,24 @@ export let PartialPath

/**
* @typedef {object} PluginOptions
* @property {string[]} [helpers] - an array of file paths to modules containing
* Handlebars helper functions
* @property {(string | string[])} [include] - one or more patterns matching
* Handlebars template files to transform
* @property {(string | string[])} [exclude] - one or more patterns matching
* Handlebars template files to exclude from transformation
* @property {(string | string[])} [partials] - one or more patterns matching
* Handlebars template files containing partials
* @property {string[]} [helpers] - paths to modules containing Handlebars
* helper functions
* @property {(string | string[])} [include] - one or more picomatch patterns
* matching Handlebars template files to transform
* @property {(string | string[])} [exclude] - one or more picomatch patterns
* matching Handlebars template files to exclude from transformation
* @property {(string | string[])} [partials] - one or more picomatch patterns
* matching Handlebars template files containing partials
* @property {PartialName} [partialName] - function to transform a partial file
* name into the name used to apply the partial in other templates
* @property {PartialPath} [partialPath] - function to transform a partial's
* name and that of the module importing it into its import path
* @property {PrecompileOptions} [compiler] - Handlebars compiler options passed
* @property {CompileOptions} [compiler] - Handlebars compiler options passed
* through to Handlebars.parse() and Handlebars.precompile()
* @property {boolean} [sourcemap] - disables source map generation when false
* @property {boolean} [sourceMap] - disables source map generation when false
* @see https://handlebarsjs.com/guide/#custom-helpers
* @see https://github.com/micromatch/picomatch#globbing-features
* @see https://handlebarsjs.com/api-reference/compilation.html
*/
/** @type {PluginOptions} */
Expand Down
29 changes: 14 additions & 15 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "rollup-plugin-handlebars-precompiler",
"version": "1.0.0",
"version": "1.0.1",
"description": "Rollup plugin to precompile Handlebars templates into JavaScript modules",
"main": "index.js",
"types": "types/index.d.ts",
Expand All @@ -9,13 +9,12 @@
"test": "vitest",
"test:ci": "pnpm lint && pnpm typecheck && pnpm jsdoc && vitest run -c ci/vitest.config.js",
"jsdoc": "jsdoc-cli-wrapper -c jsdoc.json .",
"typecheck": "npx -p typescript tsc -p jsconfig.json --noEmit --pretty",
"prepack": "npx -p typescript tsc ./index.js --allowJs --declaration --declarationMap --emitDeclarationOnly --outDir types"
"typecheck": "npx tsc",
"prepack": "npx rimraf types && npx tsc ./index.js --allowJs --declaration --declarationMap --emitDeclarationOnly --outDir types"
},
"files": [
"index.js",
"lib/*",
"types/*"
"lib/**",
"types/**"
],
"keywords": [
"rollup",
Expand All @@ -33,22 +32,22 @@
"bugs": "https://github.com/mbland/rollup-plugin-handlebars-precompiler/issues",
"devDependencies": {
"@stylistic/eslint-plugin-js": "^1.5.3",
"@types/chai": "^4.3.11",
"@types/node": "^20.10.8",
"@vitest/coverage-istanbul": "^1.1.3",
"@vitest/coverage-v8": "^1.1.3",
"@vitest/ui": "^1.1.3",
"@types/node": "^20.11.4",
"@vitest/coverage-istanbul": "^1.2.0",
"@vitest/coverage-v8": "^1.2.0",
"@vitest/ui": "^1.2.0",
"eslint": "^8.56.0",
"eslint-plugin-jsdoc": "^46.10.1",
"eslint-plugin-vitest": "^0.3.20",
"jsdoc": "^4.0.2",
"jsdoc-cli-wrapper": "^1.0.4",
"jsdoc-cli-wrapper": "^1.0.6",
"jsdoc-plugin-typescript": "^2.2.1",
"jsdom": "^23.2.0",
"rollup": "^4.9.4",
"test-page-opener": "^1.0.5",
"rimraf": "^5.0.5",
"rollup": "^4.9.5",
"test-page-opener": "^1.0.6",
"typescript": "^5.3.3",
"vitest": "^1.1.3"
"vitest": "^1.2.0"
},
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
Expand Down

0 comments on commit 1982b03

Please sign in to comment.