Skip to content

Commit

Permalink
Add strict type checking via TypeScript
Browse files Browse the repository at this point in the history
The code is still JavaScript, but now we get strict type checking in
Visual Studio Code and in continuous integration via `tsc` in `pnpm
typecheck`.

The docs generated by 'jsdoc' are a little funky, and we don't get as
much documentation in Visual Studio Code as I expected. I believe I can
fix these issues at some point with this foundation in place.

The actual changes include:

- Added @types/{chai,node}, jsdoc, and typescript as devDependencies.

- Added JSDoc-based @typedefs, including the standalone lib/types.js
  based on: "Stack Overflow: How to 'import' a typedef from one file to
  another in JSDoc using Node.js?"

  - https://stackoverflow.com/a/76872194

- Set .eslintrc to disable the no-undefined-types rule by extending
  "plugin:jsdoc/recommended-typescript-flavor-error". This is because
  the Handlebars types in lib/parser.js weren't trivial to replicate,
  and TypeScript finds those types just fine. This was based on advice
  from:

  > ...the config plugin:jsdoc/recommended-typescript-error should
  > disable the jsdoc/no-undefined-types rule because TypeScript itself
  > is responsible for reporting errors about invalid JSDoc types.
  >
  > - gajus/eslint-plugin-jsdoc#888 (comment)

  And:

  > If you are not using TypeScript syntax (your source files are still
  > .js files) but you are using the TypeScript flavor within JSDoc
  > (i.e., the default "typescript" mode in eslint-plugin-jsdoc) and you
  > are perhaps using allowJs and checkJs options of TypeScript's
  > tsconfig.json), you may use:
  >
  > ```json
  > {
  >   "extends": ["plugin:jsdoc/recommended-typescript-flavor"]
  > }
  > ```
  >
  > ...or to report with failing errors instead of mere warnings:
  >
  > ```json
  > {
  >   "extends": ["plugin:jsdoc/recommended-typescript-flavor-error"]
  > }
  > ```
  >
  > - https://github.com/gajus/eslint-plugin-jsdoc#eslintrc

  More background:

  - https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-undefined-types.md
  - gajus/eslint-plugin-jsdoc#99
  - gajus/eslint-plugin-jsdoc#1098
  - jsdoc/jsdoc#1537

- At the same time, extending "recommended-typescript-flavor-error"
  required adding the `// eslint-disable-next-line no-unused-vars`
  directive before each set of imports from lib/types.js.

- Added test/vitest.d.ts so TypeScript could find the custom toStartWith
  and toEndWith expect extension matchers.

- Added `pnpm typecheck && pnpm jsdoc` to `pnpm test:ci`.
  • Loading branch information
mbland committed Jan 10, 2024
1 parent 4400723 commit eb5b9a8
Show file tree
Hide file tree
Showing 14 changed files with 515 additions and 84 deletions.
8 changes: 4 additions & 4 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"env" : {
"env": {
"node": true,
"es2023" : true
"es2023": true
},
"parserOptions": {
"ecmaVersion": "latest",
Expand All @@ -14,7 +14,7 @@
],
"extends": [
"eslint:recommended",
"plugin:jsdoc/recommended"
"plugin:jsdoc/recommended-typescript-flavor-error"
],
"overrides": [
{
Expand All @@ -25,7 +25,7 @@
]
}
],
"rules" : {
"rules": {
"@stylistic/js/comma-dangle": [
"error", "never"
],
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ node_modules/
out/
pnpm-debug.log
tmp/
types/
*.log
*.tgz
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
_**Status**: I've still got a bit of work to do before publishing v1.0.0. I need
to add tests based on the mbland/tomcat-servlet-testing-example project from
whence this came and add more documentation. I plan to finish this by
2024-01-08._
2024-01-11._

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

Expand Down
2 changes: 1 addition & 1 deletion ci/vitest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import baseConfig from '../vitest.config'
import baseConfig from '../vitest.config.js'

export default mergeConfig(baseConfig, defineConfig({
test: {
Expand Down
42 changes: 37 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,56 @@
*/

import PluginImpl, { PLUGIN_NAME } from './lib/index.js'
// eslint-disable-next-line no-unused-vars
import { PluginOptions, Transform } from './lib/types.js'

/**
* A Rollup plugin object for precompiling Handlebars templates.
* @module rollup-plugin-handlebars-precompiler
*/

/**
* @typedef {object} RollupPlugin
* @property {string} name - plugin name
* @property {Function} resolveId - resolves the plugin's own import ID
* @property {Function} load - emits the plugin's helper module code
* @property {Function} transform - emits JavaScript code compiled from
* Handlebars templates
* @see https://rollupjs.org/plugin-development/
*/

/**
* Returns a Rollup plugin object for precompiling Handlebars templates.
* @function default
* @param {object} options object containing Handlebars compiler API options
* @returns {object} a Rollup plugin that precompiles Handlebars templates
* @param {PluginOptions} options - plugin configuration options
* @returns {RollupPlugin} - the configured plugin object
*/
export default function HandlebarsPrecompiler(options) {
const p = new PluginImpl(options)
return {
name: PLUGIN_NAME,
resolveId(id) { if (p.shouldEmitHelpersModule(id)) return id },
load(id) { if (p.shouldEmitHelpersModule(id)) return p.helpersModule() },
transform(code, id) { if (p.isTemplate(id)) return p.compile(code, id) }

/**
* @param {string} id - import identifier to resolve
* @returns {(string | undefined)} - the plugin ID if id matches it
* @see https://rollupjs.org/plugin-development/#resolveid
*/
resolveId: function (id) {
return p.shouldEmitHelpersModule(id) ? id : undefined
},

/**
* @param {string} id - import identifier to load
* @returns {(string | undefined)} - the plugin helper module if id matches
* @see https://rollupjs.org/plugin-development/#load
*/
load: function (id) {
return p.shouldEmitHelpersModule(id) ? p.helpersModule() : undefined
},

/** @type {Transform} */
transform: function (code, id) {
return p.isTemplate(id) ? p.compile(code, id) : undefined
}
}
}
16 changes: 16 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"checkJs": true,
"lib": [
"ES2022"
],
"module": "node16",
"target": "es2020",
"strict": true
},
"exclude": [
"node_modules/**",
"coverage*/**",
"jsdoc/**"
]
}
49 changes: 45 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,27 @@
*/

import collectPartials from './partials.js'
import {
// eslint-disable-next-line no-unused-vars
Compiled, PartialName, PartialPath, PluginOptions, SourceMap, Transform
} from './types.js'
import { createFilter } from '@rollup/pluginutils'
import Handlebars from 'handlebars'

export const PLUGIN_NAME = 'handlebars-precompiler'
const DEFAULT_INCLUDE = ['**/*.hbs', '**/*.handlebars', '**/*.mustache']
const DEFAULT_EXCLUDE = 'node_modules/**'
const DEFAULT_PARTIALS = '**/_*'
const DEFAULT_PARTIAL_NAME = id => {

/** @type {PartialName} */
const DEFAULT_PARTIAL_NAME = function (id) {
return id.replace(/.*\//, '') // extract the basename
.replace(/\.[^.]*$/, '') // remove the file extension, if present
.replace(/^[^[:alnum:]]*/, '') // strip leading non-alphanumeric characters
}
const DEFAULT_PARTIAL_PATH = (partialName, importerPath) => {

/** @type {PartialPath} */
const DEFAULT_PARTIAL_PATH = function (partialName, importerPath) {
return `./_${partialName}.${importerPath.replace(/.*\./, '')}`
}

Expand All @@ -58,6 +66,21 @@ const HANDLEBARS_PATH = 'handlebars/lib/handlebars.runtime'
const IMPORT_HANDLEBARS = `import Handlebars from '${HANDLEBARS_PATH}'`
const IMPORT_HELPERS = `import Render from '${PLUGIN_ID}'`

/**
* @callback CompilerOpts
* @param {string} id - import ID of module to compile
* @returns {object} - Handlebars compiler options based on id
*/

/**
* @callback AdjustSourceMap
* @param {string} map - the Handlebars source map as a JSON string
* @param {number} numLinesBeforeTmpl - number of empty lines to add to the
* beginning of the source mappings to account for the generated code before
* the precompiled template
* @returns {SourceMap} - potentially modified Handlebars source map
*/

/**
* Rollup Handlebars precompiler implementation
*/
Expand All @@ -67,10 +90,15 @@ export default class PluginImpl {
#isPartial
#partialName
#partialPath
/** @type {CompilerOpts} */
#compilerOpts
/** @type {AdjustSourceMap} */
#adjustSourceMap

constructor(options = {}) {
/**
* @param {PluginOptions} options - plugin configuration options
*/
constructor(options = /** @type {PluginOptions} */ ({})) {
this.#helpers = options.helpers || []
this.#isTemplate = createFilter(
options.include || DEFAULT_INCLUDE,
Expand Down Expand Up @@ -101,6 +129,10 @@ export default class PluginImpl {
}
}

/**
* @param {string} id - import identifier
* @returns {boolean} - true if id is the plugin's import identifier
*/
shouldEmitHelpersModule(id) { return id === PLUGIN_ID }

helpersModule() {
Expand All @@ -118,12 +150,17 @@ export default class PluginImpl {
].join('\n')
}

/**
* @param {string} id - import identifier
* @returns {boolean} - true if id matches the filter for template files
*/
isTemplate(id) { return this.#isTemplate(id) }

/** @type {Transform} */
compile(code, id) {
const opts = this.#compilerOpts(id)
const ast = Handlebars.parse(code, opts)
const compiled = Handlebars.precompile(ast, opts)
const compiled = /** @type {Compiled} */ (Handlebars.precompile(ast, opts))
const { code: tmpl = compiled, map: srcMap } = compiled

const beforeTmpl = [
Expand All @@ -143,6 +180,10 @@ export default class PluginImpl {
}
}

/**
* @param {string} id - id of the partial to register
* @returns {string} - Handlebars.registerPartial statement for the partial
*/
#partialRegistration(id) {
return `Handlebars.registerPartial('${this.#partialName(id)}', RawTemplate)`
}
Expand Down
22 changes: 16 additions & 6 deletions lib/partials.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,28 @@ import Handlebars from 'handlebars'
* @see https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md
*/
class PartialCollector extends Handlebars.Visitor {
/** @type {string[]} */
partials = []

/**
* @param {hbs.AST.PartialStatement} partial - partial name to evaluate
*/
PartialStatement(partial) {
this.collect(partial.name)
return super.PartialStatement(partial)
super.PartialStatement(partial)
}

/**
* @param {hbs.AST.PartialBlockStatement} partial - partial name to evaluate
*/
PartialBlockStatement(partial) {
this.collect(partial.name)
return super.PartialBlockStatement(partial)
super.PartialBlockStatement(partial)
}

/**
* @param {hbs.AST.PathExpression | hbs.AST.SubExpression} n - potential
* partial name to collect
*/
collect(n) {
if (n.type === 'PathExpression' && n.original !== '@partial-block') {
this.partials.push(n.original)
Expand All @@ -64,11 +74,11 @@ class PartialCollector extends Handlebars.Visitor {

/**
* Returns the partial names parsed from a Handlebars template
* @param {hbs.AST.Program} ast - abstract syntax tree for a Handlebars template
* returned by Handlebars.parse()
* @returns {string[]} - a list of partial names parsed from the template
* @see https://handlebarsjs.com/guide/partials.html
* @see https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md
* @param {object} ast - abstract syntax tree for a Handlebars template returned
* by Handlebars.parse()
* @returns {string[]} - a list of partial names parsed from the template
*/
export default function collectPartials(ast) {
const collector = new PartialCollector()
Expand Down
Loading

0 comments on commit eb5b9a8

Please sign in to comment.