hamlet-plugin-template 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ charset = utf-8
8
+ end_of_line = crlf
9
+ indent_size = 2
10
+ indent_style = space
11
+ insert_final_newline = true
12
+ trim_trailing_whitespace = true
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # hamlet-plugin-template
2
+
3
+ Base template for creating Hamlet plugins.
4
+
5
+ ## Usage
6
+
7
+ 1. Rename the package in `package.json` (e.g. `@scope/hamlet-feature` or `hamlet-plugin-feature`).
8
+ 2. Implement your plugin in `index.js`.
9
+ 3. Run the tests.
10
+ 4. Publish to npm.
11
+
12
+ ## End-user installation
13
+
14
+ ```bash
15
+ npm install hamlet-plugin-template
16
+ ```
17
+
18
+ ```js
19
+ // hamlet.config.js
20
+ import myPlugin from 'hamlet-plugin-template'
21
+
22
+ export default {
23
+ plugins: [
24
+ myPlugin(),
25
+ // or with options:
26
+ myPlugin({ myOption: true }),
27
+ ],
28
+ }
29
+ ```
30
+
31
+ Plugins are imported and invoked directly from `hamlet.config.js`, similar to Vite or Rollup plugins. Hamlet never resolves plugins by package name—it only consumes the object returned by the plugin factory.
32
+
33
+ ## Plugin contract
34
+
35
+ A plugin must export a default factory function that receives the user's options and returns an object with the following shape:
36
+
37
+ ```js
38
+ export default function hamletPlugin(options = {}) {
39
+ return {
40
+ namespace: 'MyPlugin',
41
+ partials: {},
42
+ helpers: {},
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### namespace
48
+
49
+ Every plugin must define a unique `namespace`.
50
+
51
+ * It must start with a letter.
52
+ * It may contain only letters and digits.
53
+ * It is used to namespace all exported partials and helpers, preventing collisions with other plugins and the user's own project.
54
+
55
+ ### partials
56
+
57
+ An object containing Handlebars partials.
58
+
59
+ Every partial name must be prefixed with `<namespace>.`.
60
+
61
+ Example:
62
+
63
+ ```js
64
+ export default function hamletPlugin(options = {}) {
65
+ return {
66
+ namespace: 'MyPlugin',
67
+ partials: {
68
+ 'MyPlugin.card': '<div>...</div>',
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### helpers
75
+
76
+ An object containing Handlebars helpers.
77
+
78
+ Every helper name must be prefixed with the namespace followed by an uppercase letter.
79
+
80
+ Example:
81
+
82
+ ```js
83
+ export default function hamletPlugin(options = {}) {
84
+ return {
85
+ namespace: 'MyPlugin',
86
+ helpers: {
87
+ MyPluginFormat: value => value.toUpperCase(),
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Helper names cannot contain dots because Handlebars interprets them as property paths rather than helper names.
94
+
95
+ ## Name collisions
96
+
97
+ Hamlet follows a **first registered wins** policy.
98
+
99
+ If a plugin attempts to register a partial or helper whose name already exists (whether provided by Hamlet, another plugin, or the user's project), the duplicate is ignored and a warning is printed.
100
+
101
+ ## Using Hamlet features inside plugin partials
102
+
103
+ Plugin partials can freely use Hamlet's built-in helpers and partials.
104
+
105
+ For example:
106
+
107
+ ```hbs
108
+ {{concat "Hello, " name}}
109
+
110
+ {{#switch value}}
111
+ ...
112
+ {{/switch}}
113
+
114
+ {{> hamlet.snippet}}
115
+ ```
116
+
117
+ ## Tests
118
+
119
+ Before publishing:
120
+
121
+ ```bash
122
+ npm install
123
+ npm test
124
+ ```
125
+
126
+ The test suite verifies that:
127
+
128
+ * The plugin exports a valid factory function.
129
+ * The returned object follows Hamlet's plugin contract.
130
+ * A valid namespace is exported.
131
+ * Partial and helper names follow Hamlet's naming rules.
132
+ * Exported names use the plugin namespace.
133
+ * No exported name collides with Hamlet's reserved names.
134
+ * Partials compile successfully.
135
+ * Helpers are valid functions.
136
+
137
+ Feel free to extend the tests with plugin-specific behavior or option validation.
138
+
139
+ ## Linting
140
+
141
+ This template uses the same ESLint configuration as Hamlet itself.
142
+
143
+ ```bash
144
+ npm run lint
145
+ npm run lint:fix
146
+ ```
@@ -0,0 +1,3 @@
1
+ import antfu from '@antfu/eslint-config'
2
+
3
+ export default antfu()
package/index.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Hamlet plugin template.
3
+ *
4
+ * Export a factory function that receives the options from
5
+ * hamlet.config.js and returns:
6
+ *
7
+ * - namespace: A unique plugin namespace. All exported partials and
8
+ * helpers must use this namespace to avoid collisions.
9
+ *
10
+ * - partials: An object of Handlebars partials. Every partial name
11
+ * must be prefixed with "<namespace>." (e.g. "myPlugin.card").
12
+ *
13
+ * - helpers: An object of Handlebars helpers. Every helper name must
14
+ * be prefixed with the namespace (e.g. "myPluginFormat"). Dots are
15
+ * not allowed because Handlebars interprets them as property paths.
16
+ *
17
+ * The example below exports one partial and one helper. Replace them
18
+ * with your own implementation, or start with empty { partials, helpers }
19
+ * objects while keeping the namespace consistent.
20
+ *
21
+ * @param {object} options - Options from hamlet.config.js.
22
+ * @return {{namespace: string, partials: object, helpers: object}}
23
+ */
24
+ export default function hamletPlugin(options = {}) {
25
+ const { greeting = 'Hello World' } = options
26
+
27
+ return {
28
+ namespace: 'MyPlugin',
29
+
30
+ partials: {
31
+ // Usage in a .hbs file: {{> myPlugin.hello}}
32
+ 'myPlugin.hello': `MyPlugin says: {{myPluginShout greeting}}`,
33
+ },
34
+ helpers: {
35
+ // Usage in a .hbs file: {{myPluginShout "Hello World"}}
36
+ myPluginShout: text => `${text ?? greeting}!`.toUpperCase(),
37
+ },
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "hamlet-plugin-template",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "A plugin template for hamlet",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "hamlet",
9
+ "hamlet-plugin"
10
+ ],
11
+ "exports": {
12
+ ".": "./index.js"
13
+ },
14
+ "main": "index.js",
15
+ "scripts": {
16
+ "test": "vitest run",
17
+ "lint": "eslint .",
18
+ "lint:fix": "eslint . --fix"
19
+ },
20
+ "devDependencies": {
21
+ "@antfu/eslint-config": "^9.1.0",
22
+ "eslint": "^10.6.0",
23
+ "handlebars": "^4.7.9",
24
+ "vitest": "^4.1.9"
25
+ }
26
+ }
@@ -0,0 +1,167 @@
1
+ import assert from 'node:assert/strict'
2
+ import Handlebars from 'handlebars'
3
+ import { it } from 'vitest'
4
+ import plugin from '../index.js'
5
+
6
+ // Reserved names exposed natively by hamlet.
7
+ const RESERVED_HELPERS = new Set([
8
+ 'asset',
9
+ 'currentYear',
10
+ 'helperMissing',
11
+ 'blockHelperMissing',
12
+ 'switch',
13
+ 'case',
14
+ 'default',
15
+ 'eq',
16
+ 'ne',
17
+ 'lt',
18
+ 'gt',
19
+ 'and',
20
+ 'or',
21
+ 'not',
22
+ 'concat',
23
+ 'includes',
24
+ 'capitalize',
25
+ 'first',
26
+ 'last',
27
+ ])
28
+
29
+ const isHamletReserved = name => name.startsWith('hamlet.')
30
+
31
+ const PARTIAL_NAME_PATTERN = /^[A-Z][A-Z0-9]*(?:[._-][A-Z][A-Z0-9]*)*$/i
32
+ const HELPER_NAME_PATTERN = /^[A-Z][A-Z0-9]*(?:[-_][A-Z][A-Z0-9]*)*$/i
33
+ const BLOCKED_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
34
+
35
+ it('plugin exports a function (factory)', () => {
36
+ assert.equal(typeof plugin, 'function', 'default export must be a function that returns { partials, helpers }')
37
+ })
38
+
39
+ it('plugin() returns an object with partials/helpers', () => {
40
+ const result = plugin({})
41
+ assert.equal(typeof result, 'object')
42
+ assert.ok(result.partials === undefined || typeof result.partials === 'object')
43
+ assert.ok(result.helpers === undefined || typeof result.helpers === 'object')
44
+ })
45
+
46
+ it('partials do not collide with reserved hamlet names', () => {
47
+ const { partials = {} } = plugin({})
48
+
49
+ for (const name of Object.keys(partials)) {
50
+ assert.ok(
51
+ !isHamletReserved(name),
52
+ `Partial "${name}" collides with a built-in hamlet partial`,
53
+ )
54
+ }
55
+ })
56
+
57
+ it('helpers do not collide with reserved hamlet helpers', () => {
58
+ const { helpers = {} } = plugin({})
59
+
60
+ for (const name of Object.keys(helpers)) {
61
+ assert.ok(
62
+ !RESERVED_HELPERS.has(name),
63
+ `Helper "${name}" collides with a built-in hamlet helper ("${name}" is reserved)`,
64
+ )
65
+ }
66
+ })
67
+
68
+ it('partial names match hamlet\'s naming policy (dots allowed)', () => {
69
+ const { partials = {} } = plugin({})
70
+
71
+ for (const name of Object.keys(partials)) {
72
+ assert.ok(
73
+ PARTIAL_NAME_PATTERN.test(name),
74
+ `Partial "${name}" does not match hamlet's allowed name pattern `
75
+ + '(letter start, letters/digits, optional ".", "-" or "_" separators)',
76
+ )
77
+ }
78
+ })
79
+
80
+ it('helper names match hamlet\'s naming policy (no dots allowed)', () => {
81
+ const { helpers = {} } = plugin({})
82
+
83
+ for (const name of Object.keys(helpers)) {
84
+ assert.ok(
85
+ HELPER_NAME_PATTERN.test(name),
86
+ `Helper "${name}" does not match hamlet's allowed name pattern, or contains a dot. `
87
+ + 'Helpers cannot be dot-namespaced: Handlebars parses "{{a.b}}" as a property path '
88
+ + 'on the data context, never as a helper name — a dotted helper registers without '
89
+ + 'error but is unreachable from any template (use camelCase or dashes instead).',
90
+ )
91
+ }
92
+ })
93
+
94
+ it('helpers are not nested objects (a common attempt at dot-namespacing)', () => {
95
+ const { helpers = {} } = plugin({})
96
+
97
+ for (const [name, value] of Object.entries(helpers)) {
98
+ assert.ok(
99
+ typeof value === 'function',
100
+ `Helper "${name}" is not a function (got ${typeof value}). If this is an attempt to `
101
+ + 'namespace helpers like { myPlugin: { shout: fn } }, note that Handlebars cannot '
102
+ + 'call nested helpers this way — "{{myPlugin.shout}}" looks up a property path, '
103
+ + 'not a nested helper, and would fail with "Missing helper" at render time.',
104
+ )
105
+ }
106
+ })
107
+
108
+ it('partial and helper names are not on hamlet\'s blocked list', () => {
109
+ const { partials = {}, helpers = {} } = plugin({})
110
+
111
+ for (const name of [...Object.keys(partials), ...Object.keys(helpers)]) {
112
+ assert.ok(
113
+ !BLOCKED_NAMES.has(name),
114
+ `"${name}" is a blocked name (${[...BLOCKED_NAMES].join(', ')}) and will always be rejected by hamlet`,
115
+ )
116
+ }
117
+ })
118
+
119
+ it('partials are valid strings and compile without syntax errors', () => {
120
+ const { partials = {} } = plugin({})
121
+
122
+ for (const [name, template] of Object.entries(partials)) {
123
+ assert.equal(typeof template, 'string', `Partial "${name}" must be a string`)
124
+ assert.doesNotThrow(
125
+ () => Handlebars.compile(template),
126
+ `Partial "${name}" has a Handlebars syntax error`,
127
+ )
128
+ }
129
+ })
130
+
131
+ it('helpers are functions', () => {
132
+ const { helpers = {} } = plugin({})
133
+
134
+ for (const [name, fn] of Object.entries(helpers)) {
135
+ assert.equal(typeof fn, 'function', `Helper "${name}" must be a function`)
136
+ }
137
+ })
138
+
139
+ it('plugin namespaces its exported names', () => {
140
+ const { namespace, partials = {}, helpers = {} } = plugin({})
141
+
142
+ assert.equal(
143
+ typeof namespace,
144
+ 'string',
145
+ 'Plugin must export a string "namespace"',
146
+ )
147
+
148
+ assert.match(
149
+ namespace,
150
+ /^[A-Z][A-Z0-9]*$/i,
151
+ 'Namespace must start with a letter and contain only letters and digits',
152
+ )
153
+
154
+ for (const name of Object.keys(partials)) {
155
+ assert.ok(
156
+ name.startsWith(`${namespace}.`),
157
+ `Partial "${name}" must be prefixed with "${namespace}."`,
158
+ )
159
+ }
160
+
161
+ for (const name of Object.keys(helpers)) {
162
+ assert.ok(
163
+ new RegExp(`^${namespace}[A-Z]`).test(name),
164
+ `Helper "${name}" must be prefixed with "${namespace}" followed by an uppercase letter (e.g. "${namespace}Format")`,
165
+ )
166
+ }
167
+ })