Skip to content

Commit eb5b9a8

Browse files
committed
Add strict type checking via TypeScript
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`.
1 parent 4400723 commit eb5b9a8

14 files changed

+515
-84
lines changed

.eslintrc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"env" : {
2+
"env": {
33
"node": true,
4-
"es2023" : true
4+
"es2023": true
55
},
66
"parserOptions": {
77
"ecmaVersion": "latest",
@@ -14,7 +14,7 @@
1414
],
1515
"extends": [
1616
"eslint:recommended",
17-
"plugin:jsdoc/recommended"
17+
"plugin:jsdoc/recommended-typescript-flavor-error"
1818
],
1919
"overrides": [
2020
{
@@ -25,7 +25,7 @@
2525
]
2626
}
2727
],
28-
"rules" : {
28+
"rules": {
2929
"@stylistic/js/comma-dangle": [
3030
"error", "never"
3131
],

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ node_modules/
1010
out/
1111
pnpm-debug.log
1212
tmp/
13+
types/
1314
*.log
15+
*.tgz

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
_**Status**: I've still got a bit of work to do before publishing v1.0.0. I need
66
to add tests based on the mbland/tomcat-servlet-testing-example project from
77
whence this came and add more documentation. I plan to finish this by
8-
2024-01-08._
8+
2024-01-11._
99

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

ci/vitest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineConfig, mergeConfig } from 'vitest/config'
2-
import baseConfig from '../vitest.config'
2+
import baseConfig from '../vitest.config.js'
33

44
export default mergeConfig(baseConfig, defineConfig({
55
test: {

index.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,56 @@
3737
*/
3838

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

4143
/**
4244
* A Rollup plugin object for precompiling Handlebars templates.
4345
* @module rollup-plugin-handlebars-precompiler
4446
*/
4547

48+
/**
49+
* @typedef {object} RollupPlugin
50+
* @property {string} name - plugin name
51+
* @property {Function} resolveId - resolves the plugin's own import ID
52+
* @property {Function} load - emits the plugin's helper module code
53+
* @property {Function} transform - emits JavaScript code compiled from
54+
* Handlebars templates
55+
* @see https://rollupjs.org/plugin-development/
56+
*/
57+
4658
/**
4759
* Returns a Rollup plugin object for precompiling Handlebars templates.
4860
* @function default
49-
* @param {object} options object containing Handlebars compiler API options
50-
* @returns {object} a Rollup plugin that precompiles Handlebars templates
61+
* @param {PluginOptions} options - plugin configuration options
62+
* @returns {RollupPlugin} - the configured plugin object
5163
*/
5264
export default function HandlebarsPrecompiler(options) {
5365
const p = new PluginImpl(options)
5466
return {
5567
name: PLUGIN_NAME,
56-
resolveId(id) { if (p.shouldEmitHelpersModule(id)) return id },
57-
load(id) { if (p.shouldEmitHelpersModule(id)) return p.helpersModule() },
58-
transform(code, id) { if (p.isTemplate(id)) return p.compile(code, id) }
68+
69+
/**
70+
* @param {string} id - import identifier to resolve
71+
* @returns {(string | undefined)} - the plugin ID if id matches it
72+
* @see https://rollupjs.org/plugin-development/#resolveid
73+
*/
74+
resolveId: function (id) {
75+
return p.shouldEmitHelpersModule(id) ? id : undefined
76+
},
77+
78+
/**
79+
* @param {string} id - import identifier to load
80+
* @returns {(string | undefined)} - the plugin helper module if id matches
81+
* @see https://rollupjs.org/plugin-development/#load
82+
*/
83+
load: function (id) {
84+
return p.shouldEmitHelpersModule(id) ? p.helpersModule() : undefined
85+
},
86+
87+
/** @type {Transform} */
88+
transform: function (code, id) {
89+
return p.isTemplate(id) ? p.compile(code, id) : undefined
90+
}
5991
}
6092
}

jsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"checkJs": true,
4+
"lib": [
5+
"ES2022"
6+
],
7+
"module": "node16",
8+
"target": "es2020",
9+
"strict": true
10+
},
11+
"exclude": [
12+
"node_modules/**",
13+
"coverage*/**",
14+
"jsdoc/**"
15+
]
16+
}

lib/index.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,27 @@
3737
*/
3838

3939
import collectPartials from './partials.js'
40+
import {
41+
// eslint-disable-next-line no-unused-vars
42+
Compiled, PartialName, PartialPath, PluginOptions, SourceMap, Transform
43+
} from './types.js'
4044
import { createFilter } from '@rollup/pluginutils'
4145
import Handlebars from 'handlebars'
4246

4347
export const PLUGIN_NAME = 'handlebars-precompiler'
4448
const DEFAULT_INCLUDE = ['**/*.hbs', '**/*.handlebars', '**/*.mustache']
4549
const DEFAULT_EXCLUDE = 'node_modules/**'
4650
const DEFAULT_PARTIALS = '**/_*'
47-
const DEFAULT_PARTIAL_NAME = id => {
51+
52+
/** @type {PartialName} */
53+
const DEFAULT_PARTIAL_NAME = function (id) {
4854
return id.replace(/.*\//, '') // extract the basename
4955
.replace(/\.[^.]*$/, '') // remove the file extension, if present
5056
.replace(/^[^[:alnum:]]*/, '') // strip leading non-alphanumeric characters
5157
}
52-
const DEFAULT_PARTIAL_PATH = (partialName, importerPath) => {
58+
59+
/** @type {PartialPath} */
60+
const DEFAULT_PARTIAL_PATH = function (partialName, importerPath) {
5361
return `./_${partialName}.${importerPath.replace(/.*\./, '')}`
5462
}
5563

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

69+
/**
70+
* @callback CompilerOpts
71+
* @param {string} id - import ID of module to compile
72+
* @returns {object} - Handlebars compiler options based on id
73+
*/
74+
75+
/**
76+
* @callback AdjustSourceMap
77+
* @param {string} map - the Handlebars source map as a JSON string
78+
* @param {number} numLinesBeforeTmpl - number of empty lines to add to the
79+
* beginning of the source mappings to account for the generated code before
80+
* the precompiled template
81+
* @returns {SourceMap} - potentially modified Handlebars source map
82+
*/
83+
6184
/**
6285
* Rollup Handlebars precompiler implementation
6386
*/
@@ -67,10 +90,15 @@ export default class PluginImpl {
6790
#isPartial
6891
#partialName
6992
#partialPath
93+
/** @type {CompilerOpts} */
7094
#compilerOpts
95+
/** @type {AdjustSourceMap} */
7196
#adjustSourceMap
7297

73-
constructor(options = {}) {
98+
/**
99+
* @param {PluginOptions} options - plugin configuration options
100+
*/
101+
constructor(options = /** @type {PluginOptions} */ ({})) {
74102
this.#helpers = options.helpers || []
75103
this.#isTemplate = createFilter(
76104
options.include || DEFAULT_INCLUDE,
@@ -101,6 +129,10 @@ export default class PluginImpl {
101129
}
102130
}
103131

132+
/**
133+
* @param {string} id - import identifier
134+
* @returns {boolean} - true if id is the plugin's import identifier
135+
*/
104136
shouldEmitHelpersModule(id) { return id === PLUGIN_ID }
105137

106138
helpersModule() {
@@ -118,12 +150,17 @@ export default class PluginImpl {
118150
].join('\n')
119151
}
120152

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

159+
/** @type {Transform} */
123160
compile(code, id) {
124161
const opts = this.#compilerOpts(id)
125162
const ast = Handlebars.parse(code, opts)
126-
const compiled = Handlebars.precompile(ast, opts)
163+
const compiled = /** @type {Compiled} */ (Handlebars.precompile(ast, opts))
127164
const { code: tmpl = compiled, map: srcMap } = compiled
128165

129166
const beforeTmpl = [
@@ -143,6 +180,10 @@ export default class PluginImpl {
143180
}
144181
}
145182

183+
/**
184+
* @param {string} id - id of the partial to register
185+
* @returns {string} - Handlebars.registerPartial statement for the partial
186+
*/
146187
#partialRegistration(id) {
147188
return `Handlebars.registerPartial('${this.#partialName(id)}', RawTemplate)`
148189
}

lib/partials.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,28 @@ import Handlebars from 'handlebars'
4343
* @see https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md
4444
*/
4545
class PartialCollector extends Handlebars.Visitor {
46+
/** @type {string[]} */
4647
partials = []
4748

49+
/**
50+
* @param {hbs.AST.PartialStatement} partial - partial name to evaluate
51+
*/
4852
PartialStatement(partial) {
4953
this.collect(partial.name)
50-
return super.PartialStatement(partial)
54+
super.PartialStatement(partial)
5155
}
52-
56+
/**
57+
* @param {hbs.AST.PartialBlockStatement} partial - partial name to evaluate
58+
*/
5359
PartialBlockStatement(partial) {
5460
this.collect(partial.name)
55-
return super.PartialBlockStatement(partial)
61+
super.PartialBlockStatement(partial)
5662
}
5763

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

6575
/**
6676
* Returns the partial names parsed from a Handlebars template
77+
* @param {hbs.AST.Program} ast - abstract syntax tree for a Handlebars template
78+
* returned by Handlebars.parse()
79+
* @returns {string[]} - a list of partial names parsed from the template
6780
* @see https://handlebarsjs.com/guide/partials.html
6881
* @see https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md
69-
* @param {object} ast - abstract syntax tree for a Handlebars template returned
70-
* by Handlebars.parse()
71-
* @returns {string[]} - a list of partial names parsed from the template
7282
*/
7383
export default function collectPartials(ast) {
7484
const collector = new PartialCollector()

0 commit comments

Comments
 (0)